reladraw 0.19.1 → 0.21.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 +2 -2
- package/SYNTAX.md +59 -8
- package/dist/ast.d.ts +26 -2
- package/dist/ast.js +15 -2
- package/dist/checks.d.ts +1 -0
- package/dist/checks.js +79 -5
- package/dist/cli.js +35 -12
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +8 -0
- package/dist/constrain.d.ts +56 -0
- package/dist/constrain.js +95 -0
- package/dist/grammar.js +2 -0
- package/dist/model.d.ts +16 -1
- package/dist/model.js +7 -0
- package/dist/page.d.ts +14 -0
- package/dist/page.js +488 -0
- package/dist/parser.js +69 -4
- package/dist/place.d.ts +74 -1
- package/dist/place.js +303 -18
- package/dist/render.d.ts +17 -0
- package/dist/render.js +101 -29
- package/dist/resolve.d.ts +14 -0
- package/dist/resolve.js +34 -7
- package/dist/themes.d.ts +1 -0
- package/dist/themes.js +1 -1
- package/package.json +1 -1
package/dist/place.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describePlacement, describePoint, fractionAxis, nameTarget, PART_SIDES, partWordHint } from './ast.js';
|
|
1
|
+
import { describeLimit, describePlacement, describePoint, fractionAxis, nameTarget, PART_SIDES, partWordHint } from './ast.js';
|
|
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';
|
|
@@ -107,6 +107,8 @@ export class Placer {
|
|
|
107
107
|
sizes = new Map();
|
|
108
108
|
frames = new Map();
|
|
109
109
|
outlines = new Map();
|
|
110
|
+
/** A container's box while a limit tries it out (`roomFor`), in place of its final one. */
|
|
111
|
+
trials = new Map();
|
|
110
112
|
working = new Set();
|
|
111
113
|
/** What is being worked out, innermost last, for reporting a circle. */
|
|
112
114
|
trail = [];
|
|
@@ -128,6 +130,8 @@ export class Placer {
|
|
|
128
130
|
origins = new Map();
|
|
129
131
|
/** Containers whose anchor is being placed on the page, so a box outside them is measured on the page rather than in their frame. */
|
|
130
132
|
onPage = new Set();
|
|
133
|
+
/** Boxes that anchor their frame on one axis only, when no box in it says nothing at all (`splitAnchors`). */
|
|
134
|
+
axisAnchors = new Set();
|
|
131
135
|
/** Named points and regions being worked out, innermost last, to refuse one defined through itself. */
|
|
132
136
|
expanding = [];
|
|
133
137
|
constructor(roots, byName, facts,
|
|
@@ -175,6 +179,8 @@ export class Placer {
|
|
|
175
179
|
/** Work out everything, and write it onto the nodes. */
|
|
176
180
|
run() {
|
|
177
181
|
this.checkAnchors(this.roots, undefined);
|
|
182
|
+
for (const node of this.byName.values())
|
|
183
|
+
this.checkLimitTargets(node);
|
|
178
184
|
for (const node of this.byName.values()) {
|
|
179
185
|
for (const axis of BOTH_AXES) {
|
|
180
186
|
this.size(node, axis);
|
|
@@ -189,9 +195,76 @@ export class Placer {
|
|
|
189
195
|
}
|
|
190
196
|
for (const node of this.byName.values())
|
|
191
197
|
this.checkYielded(node);
|
|
198
|
+
for (const node of this.byName.values())
|
|
199
|
+
this.checkLimitsMet(node);
|
|
192
200
|
for (const node of this.byName.values())
|
|
193
201
|
this.finish(node);
|
|
194
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* What a limit may relate (design.org 5.6): two things in one box that
|
|
205
|
+
* nothing else ties together, since the box's own spacing between them is
|
|
206
|
+
* the only distance it may lengthen.
|
|
207
|
+
*/
|
|
208
|
+
checkLimitTargets(node) {
|
|
209
|
+
for (const limit of node.limits ?? []) {
|
|
210
|
+
const parent = node.parent;
|
|
211
|
+
const said = `"${node.name} ${describeLimit(limit)}"`;
|
|
212
|
+
const named = this.byName.get(limit.target.name);
|
|
213
|
+
if (!named) {
|
|
214
|
+
throw new SourceError(`${said}: "${limit.target.name}" does not exist${partWordHint(limit.target.name, this.byName.keys())}`, limit.line);
|
|
215
|
+
}
|
|
216
|
+
const ownText = parent !== undefined && named === parent && limit.target.part === 'text';
|
|
217
|
+
const sibling = parent !== undefined && named.parent === parent && named !== node && limit.target.part === undefined;
|
|
218
|
+
if (!parent || !(ownText || sibling)) {
|
|
219
|
+
throw new SourceError(`${said}: a limit keeps clear of something in the same box, a sibling or ` +
|
|
220
|
+
`${parent ? `"${parent.name} text"` : 'the box\'s own text'}, since that box is what makes room`, limit.line);
|
|
221
|
+
}
|
|
222
|
+
if (ownText && !this.hasTitle(parent)) {
|
|
223
|
+
throw new SourceError(`${said}: "${parent.name}" has no text`, limit.line);
|
|
224
|
+
}
|
|
225
|
+
const axis = limitAxis(limit);
|
|
226
|
+
const own = this.groupOf(node, parent, axis);
|
|
227
|
+
if (own !== 'frame' && own === this.groupOf(ownText ? 'text' : named, parent, axis)) {
|
|
228
|
+
const how = own === 'title' ? `with "${parent.name}"'s text` : 'against each other';
|
|
229
|
+
throw new SourceError(`${said} could never change anything: "${node.name}" and "${nameTarget(limit.target)}" are placed ${how}, ` +
|
|
230
|
+
'so the distance between them is already stated — a limit relates two things in one box that nothing else ties together, ' +
|
|
231
|
+
`such as a child placed against "${parent.name}"'s side and one of its other children`, limit.line);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Which of the things a box holds that nothing ties to each other a member
|
|
237
|
+
* belongs to, on one axis: its title, what is placed against the box's
|
|
238
|
+
* sides, or the rest, placed against each other.
|
|
239
|
+
*/
|
|
240
|
+
groupOf(member, parent, axis) {
|
|
241
|
+
const titleTied = this.anchoredOnTitle(parent) || (parent.titlePlacements?.length ?? 0) > 0;
|
|
242
|
+
if (member === 'text' || member === parent.heading || joinedToText(member))
|
|
243
|
+
return titleTied ? 'content' : 'title';
|
|
244
|
+
return this.framedOn(member, axis) ? 'frame' : 'content';
|
|
245
|
+
}
|
|
246
|
+
/** A limit the box couldn't make room for: no side helps, or a stated size holds it in. */
|
|
247
|
+
checkLimitsMet(node) {
|
|
248
|
+
for (const limit of node.limits ?? []) {
|
|
249
|
+
const parent = node.parent;
|
|
250
|
+
const axis = limitAxis(limit);
|
|
251
|
+
const frame = this.frame(parent, axis);
|
|
252
|
+
if (this.shortfall(parent, axis, { subject: node, limit }, frame) <= LIMIT_EPSILON)
|
|
253
|
+
continue;
|
|
254
|
+
const said = `"${describeLimit(limit)}"`;
|
|
255
|
+
if (frame.boxSize !== this.outline(parent, axis).boxSize) {
|
|
256
|
+
throw new SourceError(`"${node.name} ${describeLimit(limit)}", but "${parent.name}"'s stated ${axis === 'x' ? 'width' : 'height'} leaves no room for it — ` +
|
|
257
|
+
'make it bigger, or leave the size unstated so the box can make room', limit.line);
|
|
258
|
+
}
|
|
259
|
+
const named = this.byName.get(limit.target.name);
|
|
260
|
+
const framed = [node, named].find((member) => member !== parent && this.framedOn(member, axis));
|
|
261
|
+
const where = framed
|
|
262
|
+
? this.statements(framed, axis).map((statement) => `"${framed.name} ${describePlacement(statement.placement)}"`).join(' and ')
|
|
263
|
+
: undefined;
|
|
264
|
+
throw new SourceError(`"${node.name}" can't keep to ${said}${where ? ` with ${where}` : ''}: "${parent.name}" holds both, and growing it ` +
|
|
265
|
+
'never moves them apart — keep one of the two', limit.line);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
195
268
|
// --- the quantities ------------------------------------------------------
|
|
196
269
|
/** Where a node starts on the page. */
|
|
197
270
|
page(node, axis) {
|
|
@@ -215,7 +288,7 @@ export class Placer {
|
|
|
215
288
|
if (this.frameAnchor(node, axis))
|
|
216
289
|
return this.anchorOnPage(node, axis);
|
|
217
290
|
const above = node.parent ? this.origin(node.parent, axis) : 0;
|
|
218
|
-
if (this.floats(node))
|
|
291
|
+
if (this.floats(node, axis))
|
|
219
292
|
return above;
|
|
220
293
|
this.refusePlacedTwice(node, axis);
|
|
221
294
|
return above + this.start(node, axis) - this.frame(node, axis).boxStart;
|
|
@@ -275,9 +348,11 @@ export class Placer {
|
|
|
275
348
|
* goes, and nothing inside it is placed against a box outside it in its
|
|
276
349
|
* stead. Its frame's origin is its own container's.
|
|
277
350
|
*/
|
|
278
|
-
floats(node) {
|
|
351
|
+
floats(node, axis) {
|
|
279
352
|
const parent = node.parent;
|
|
280
|
-
if (node.
|
|
353
|
+
if (node.children.length === 0)
|
|
354
|
+
return false;
|
|
355
|
+
if (node.placements.length > 0 && !this.axisAnchors.has(`${node.name}\n${axis}`))
|
|
281
356
|
return false;
|
|
282
357
|
if (parent && (parent.heading === node || joinedToText(node) || this.facts.get(parent).layout || this.vennPart(node) !== undefined)) {
|
|
283
358
|
return false;
|
|
@@ -363,6 +438,9 @@ export class Placer {
|
|
|
363
438
|
/** A container's box and title in its own frame, at the size it ends up. */
|
|
364
439
|
frame(node, axis) {
|
|
365
440
|
const key = `${node.name}\n${axis}`;
|
|
441
|
+
const trial = this.trials.get(key);
|
|
442
|
+
if (trial)
|
|
443
|
+
return trial;
|
|
366
444
|
const known = this.frames.get(key);
|
|
367
445
|
if (known)
|
|
368
446
|
return known;
|
|
@@ -376,10 +454,91 @@ export class Placer {
|
|
|
376
454
|
const known = this.outlines.get(key);
|
|
377
455
|
if (known)
|
|
378
456
|
return known;
|
|
379
|
-
const worked = this.guard(`outline\n${key}`, node, axis, undefined, () => this.
|
|
457
|
+
const worked = this.guard(`outline\n${key}`, node, axis, undefined, () => this.roomFor(node, axis));
|
|
380
458
|
this.outlines.set(key, worked);
|
|
381
459
|
return worked;
|
|
382
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* A container's outline with room made for its children's limits
|
|
463
|
+
* (design.org 5.6). The only distance a limit may lengthen is between what
|
|
464
|
+
* the box holds and its sides, which every placement inside leaves unstated.
|
|
465
|
+
* Each side's room moves a limit's two ends apart at a fixed rate (all of
|
|
466
|
+
* it for a corner, half for the middle of a side), so one trial of each
|
|
467
|
+
* side gives the rate, and the room is the shortfall over the rate. A side
|
|
468
|
+
* that doesn't help is never used; with both helping, as for two corner
|
|
469
|
+
* children, the box grows evenly. A limit no side helps is left unmet, and
|
|
470
|
+
* `checkLimits` refuses it.
|
|
471
|
+
*/
|
|
472
|
+
roomFor(node, axis) {
|
|
473
|
+
const limits = this.limitsIn(node, axis);
|
|
474
|
+
let room = NO_ROOM;
|
|
475
|
+
let frame = this.workOutFrame(node, axis);
|
|
476
|
+
for (let round = 0; round < limits.length * 2; round++) {
|
|
477
|
+
const short = (at) => Math.max(...limits.map((each) => this.shortfall(node, axis, each, at)));
|
|
478
|
+
const owed = short(frame);
|
|
479
|
+
if (owed <= LIMIT_EPSILON)
|
|
480
|
+
break;
|
|
481
|
+
const rate = (more) => (owed - short(this.workOutFrame(node, axis, more))) / owed;
|
|
482
|
+
const low = rate({ low: room.low + owed, high: room.high });
|
|
483
|
+
const high = rate({ low: room.low, high: room.high + owed });
|
|
484
|
+
if (low > LIMIT_EPSILON && high > LIMIT_EPSILON) {
|
|
485
|
+
const both = rate({ low: room.low + owed, high: room.high + owed });
|
|
486
|
+
room = { low: room.low + owed / both, high: room.high + owed / both };
|
|
487
|
+
}
|
|
488
|
+
else if (low > LIMIT_EPSILON) {
|
|
489
|
+
room = { ...room, low: room.low + owed / low };
|
|
490
|
+
}
|
|
491
|
+
else if (high > LIMIT_EPSILON) {
|
|
492
|
+
room = { ...room, high: room.high + owed / high };
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
frame = this.workOutFrame(node, axis, room);
|
|
498
|
+
}
|
|
499
|
+
return frame;
|
|
500
|
+
}
|
|
501
|
+
/** The limits a container's children state on one axis. */
|
|
502
|
+
limitsIn(node, axis) {
|
|
503
|
+
return node.children.flatMap((subject) => (subject.limits ?? []).filter((limit) => limitAxis(limit) === axis).map((limit) => ({ subject, limit })));
|
|
504
|
+
}
|
|
505
|
+
/** How much nearer than its gap a limit's two ends are, in a trial frame of their container. */
|
|
506
|
+
shortfall(node, axis, { subject, limit }, at) {
|
|
507
|
+
const own = this.memberAt(subject, node, axis, at);
|
|
508
|
+
const other = limit.target.name === node.name ? { start: at.textStart, size: at.textSize } : this.memberAt(this.byName.get(limit.target.name), node, axis, at);
|
|
509
|
+
const gap = this.limitGap(subject, limit);
|
|
510
|
+
const toward = limit.way === 'above' || limit.way === 'left';
|
|
511
|
+
return toward ? own.start + own.size + gap - other.start : other.start + other.size + gap - own.start;
|
|
512
|
+
}
|
|
513
|
+
/** The distance a limit keeps: its gap, or the usual one; beside a text, as beside any text. */
|
|
514
|
+
limitGap(subject, limit) {
|
|
515
|
+
if (limit.gap !== undefined)
|
|
516
|
+
return gapPixels(limit.gap, `"${subject.name}"`, limit.line);
|
|
517
|
+
const parent = subject.parent;
|
|
518
|
+
if (limit.target.name === parent.name)
|
|
519
|
+
return this.hasTitle(parent) ? UNIT : 0;
|
|
520
|
+
return GAPS['normal'];
|
|
521
|
+
}
|
|
522
|
+
/** Where a child of `node` is on one axis, in a trial frame of `node`. */
|
|
523
|
+
memberAt(member, node, axis, at) {
|
|
524
|
+
const size = this.size(member, axis);
|
|
525
|
+
if (member === node.heading)
|
|
526
|
+
return { start: at.titleStart, size };
|
|
527
|
+
if (joinedToText(member))
|
|
528
|
+
return { start: at.textStart + this.unitStart(member, axis), size };
|
|
529
|
+
if (!this.framedOn(member, axis))
|
|
530
|
+
return { start: this.start(member, axis), size };
|
|
531
|
+
// Placed against the box: where the trial box puts it, worked out
|
|
532
|
+
// without remembering, since the box isn't final.
|
|
533
|
+
const key = `${node.name}\n${axis}`;
|
|
534
|
+
this.trials.set(key, at);
|
|
535
|
+
try {
|
|
536
|
+
return { start: this.placed(member, axis), size };
|
|
537
|
+
}
|
|
538
|
+
finally {
|
|
539
|
+
this.trials.delete(key);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
383
542
|
remember(memo, node, axis, work) {
|
|
384
543
|
const key = `${node.name}\n${axis}`;
|
|
385
544
|
const known = memo.get(key);
|
|
@@ -454,7 +613,7 @@ export class Placer {
|
|
|
454
613
|
}
|
|
455
614
|
// A container that anchors its frame shares its container's origin, so
|
|
456
615
|
// its box starts where its outline does.
|
|
457
|
-
if (this.floats(node))
|
|
616
|
+
if (this.floats(node, axis))
|
|
458
617
|
return this.frame(node, axis).boxStart;
|
|
459
618
|
if (node.placements.length === 0 || this.acrossParts(node)) {
|
|
460
619
|
// The anchor of its frame. One placed against a box in another part of
|
|
@@ -473,13 +632,14 @@ export class Placer {
|
|
|
473
632
|
placed(node, axis) {
|
|
474
633
|
const statements = this.statements(node, axis);
|
|
475
634
|
if (statements.length === 0)
|
|
476
|
-
return this.lone(node, axis);
|
|
635
|
+
return this.axisAnchors.has(`${node.name}\n${axis}`) ? 0 : this.lone(node, axis);
|
|
477
636
|
if (statements.length === 1) {
|
|
478
637
|
const [only] = statements;
|
|
479
638
|
return this.apply(node, axis, only);
|
|
480
639
|
}
|
|
481
640
|
const [first, second] = statements;
|
|
482
641
|
if (first.kind !== 'align' && second.kind !== 'align' && first.kind !== second.kind) {
|
|
642
|
+
this.refuseBoxAndSibling(node, axis, first.placement, second.placement);
|
|
483
643
|
// Opposite placements: centered between the two at its own size. Each
|
|
484
644
|
// target stays where its own statement put it.
|
|
485
645
|
const after = first.kind === 'after' ? first : second;
|
|
@@ -494,6 +654,36 @@ export class Placer {
|
|
|
494
654
|
return pins.start;
|
|
495
655
|
throw this.overStated(node, axis, statements);
|
|
496
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* Opposite placements center a box between two others, but not when one of
|
|
659
|
+
* them is its own container's box: `inside c top-right right of c.k` would
|
|
660
|
+
* be neither in the corner nor a gap from `c.k` (design.org 5.3). Refused,
|
|
661
|
+
* naming the two ways out: the corner's other half beside the sibling, or
|
|
662
|
+
* the corner kept and the sibling held off with `stays`.
|
|
663
|
+
*/
|
|
664
|
+
refuseBoxAndSibling(node, axis, first, second) {
|
|
665
|
+
const parent = node.parent;
|
|
666
|
+
if (!parent || this.againstBox(first, parent) === this.againstBox(second, parent))
|
|
667
|
+
return;
|
|
668
|
+
const [box, other] = this.againstBox(first, parent) ? [first, second] : [second, first];
|
|
669
|
+
if (other.kind !== 'offset')
|
|
670
|
+
return;
|
|
671
|
+
const ways = [];
|
|
672
|
+
const corner = box.targets[0]?.part;
|
|
673
|
+
if (box.kind === 'offset' && box.written !== undefined && corner !== undefined && corner.includes('-')) {
|
|
674
|
+
const [down, across] = corner.split('-');
|
|
675
|
+
const kept = axis === 'x' ? down : across;
|
|
676
|
+
ways.push(`to sit ${describePlacement(other)} at the ${kept} of ${parent.name}, write "${describePlacement(other)} ${box.written} ${parent.name} ${kept}"`);
|
|
677
|
+
}
|
|
678
|
+
const way = component(other.direction, axis);
|
|
679
|
+
const straight = axis === 'x' ? (way === 'after' ? 'right' : 'left') : way === 'after' ? 'below' : 'above';
|
|
680
|
+
const of = axis === 'x' ? ' of' : '';
|
|
681
|
+
if (other.targets.length === 1) {
|
|
682
|
+
ways.push(`to stay ${describePlacement(box)} and clear of ${nameTarget(other.targets[0])}, write "stays ${straight}${of} ${nameTarget(other.targets[0])}"`);
|
|
683
|
+
}
|
|
684
|
+
throw new SourceError(`"${node.name}" says twice where it goes ${AXIS_SAID[axis]}, with "${describePlacement(box)}" and "${describePlacement(other)}"` +
|
|
685
|
+
(ways.length > 0 ? `; ${ways.join('; ')}` : ' — keep one'), other.line);
|
|
686
|
+
}
|
|
497
687
|
edgeOf(node, axis, statement) {
|
|
498
688
|
return this.guard(`edge\n${node.name}\n${axis}\n${statement.placement.line}`, node, axis, statement.placement, () => statement.kind === 'align' ? 0 : statement.edge());
|
|
499
689
|
}
|
|
@@ -991,7 +1181,7 @@ export class Placer {
|
|
|
991
1181
|
text = { start: frame.textStart, size: frame.textSize };
|
|
992
1182
|
}
|
|
993
1183
|
else if (named.parent === parent) {
|
|
994
|
-
if (this.
|
|
1184
|
+
if (this.framedOn(named, axis) && !this.framedOn(node, axis)) {
|
|
995
1185
|
throw new SourceError(`"${node.name}" is placed against "${named.name}", which is placed against "${parent.name}" itself. ` +
|
|
996
1186
|
`Nothing may be placed against a thing placed against its container's side or corner — place ` +
|
|
997
1187
|
`"${node.name}" against another child`, placement.line);
|
|
@@ -1049,7 +1239,27 @@ export class Placer {
|
|
|
1049
1239
|
const parent = node.parent;
|
|
1050
1240
|
if (!parent || this.facts.get(parent).layout || this.facts.get(parent).venn)
|
|
1051
1241
|
return false;
|
|
1052
|
-
return node.placements.some((placement) =>
|
|
1242
|
+
return node.placements.some((placement) => this.againstBox(placement, parent));
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Framed on one axis: what says where it goes on that axis is against its
|
|
1246
|
+
* container's box. `right of c.k inside c top` is framed down, so its height
|
|
1247
|
+
* is measured from the finished box, and is content across, so the box holds
|
|
1248
|
+
* it there (design.org 5.6).
|
|
1249
|
+
*/
|
|
1250
|
+
framedOn(node, axis) {
|
|
1251
|
+
if (!this.framed(node))
|
|
1252
|
+
return false;
|
|
1253
|
+
const parent = node.parent;
|
|
1254
|
+
const statements = this.statements(node, axis);
|
|
1255
|
+
if (statements.length > 0)
|
|
1256
|
+
return statements.some((statement) => this.againstBox(statement.placement, parent));
|
|
1257
|
+
// Nothing states this axis, so a lone direction on the other one centers it on its target.
|
|
1258
|
+
return node.placements.some((placement) => placement.kind === 'offset' && component(placement.direction, axis) === undefined && this.againstBox(placement, parent));
|
|
1259
|
+
}
|
|
1260
|
+
/** Whether a placement names its container's box, any part but the text. */
|
|
1261
|
+
againstBox(placement, parent) {
|
|
1262
|
+
return placement.targets.some((target) => target.name === parent.name && target.part !== 'text');
|
|
1053
1263
|
}
|
|
1054
1264
|
/**
|
|
1055
1265
|
* A child placed against its container's text, or against its heading,
|
|
@@ -1111,10 +1321,13 @@ export class Placer {
|
|
|
1111
1321
|
/**
|
|
1112
1322
|
* The children a container's box is the outline of, besides its title: not
|
|
1113
1323
|
* its heading, not what is joined to its text, and not those placed against
|
|
1114
|
-
* the box.
|
|
1324
|
+
* the box. Given an axis, a child placed against the box only on the other
|
|
1325
|
+
* axis is content on this one.
|
|
1115
1326
|
*/
|
|
1116
|
-
contentOf(node) {
|
|
1117
|
-
return node.children.filter((child) => child !== node.heading &&
|
|
1327
|
+
contentOf(node, axis) {
|
|
1328
|
+
return node.children.filter((child) => child !== node.heading &&
|
|
1329
|
+
!joinedToText(child) &&
|
|
1330
|
+
!(axis === undefined ? this.framed(child) : this.framedOn(child, axis)));
|
|
1118
1331
|
}
|
|
1119
1332
|
/** Whether a node has a title: its text, what is joined to it, or a heading. */
|
|
1120
1333
|
hasTitle(node) {
|
|
@@ -1307,12 +1520,12 @@ export class Placer {
|
|
|
1307
1520
|
* not placed against the box itself, and the title when it sits inside the
|
|
1308
1521
|
* border or on it.
|
|
1309
1522
|
*/
|
|
1310
|
-
workOutFrame(node, axis) {
|
|
1523
|
+
workOutFrame(node, axis, room = NO_ROOM) {
|
|
1311
1524
|
const facts = this.facts.get(node);
|
|
1312
1525
|
const title = this.titleOf(node);
|
|
1313
1526
|
const hasTitle = this.hasTitle(node);
|
|
1314
1527
|
const titleSize = this.titleSize(node, axis);
|
|
1315
|
-
const content = this.contentOf(node);
|
|
1528
|
+
const content = this.contentOf(node, axis);
|
|
1316
1529
|
const anchoredOnTitle = this.anchoredOnTitle(node);
|
|
1317
1530
|
let low = Infinity;
|
|
1318
1531
|
let high = -Infinity;
|
|
@@ -1343,6 +1556,12 @@ export class Placer {
|
|
|
1343
1556
|
high = Math.max(high, band);
|
|
1344
1557
|
}
|
|
1345
1558
|
}
|
|
1559
|
+
// Room a child's limit asks for (design.org 5.6), between what the box
|
|
1560
|
+
// holds and its sides, so a title against a side moves out with it.
|
|
1561
|
+
if (!empty) {
|
|
1562
|
+
low -= room.low;
|
|
1563
|
+
high += room.high;
|
|
1564
|
+
}
|
|
1346
1565
|
// Where the title sits in the frame. When something is placed against it,
|
|
1347
1566
|
// it is the frame's anchor and sits at the origin; otherwise it is placed
|
|
1348
1567
|
// against the children by its side.
|
|
@@ -1358,6 +1577,10 @@ export class Placer {
|
|
|
1358
1577
|
low = titleStart;
|
|
1359
1578
|
high = titleStart + titleSize;
|
|
1360
1579
|
}
|
|
1580
|
+
if (empty) {
|
|
1581
|
+
low -= room.low;
|
|
1582
|
+
high += room.high;
|
|
1583
|
+
}
|
|
1361
1584
|
}
|
|
1362
1585
|
else if (title.how === 'center' || title.side === 'center') {
|
|
1363
1586
|
titleStart = (low + high) / 2 - titleSize / 2;
|
|
@@ -1590,7 +1813,7 @@ export class Placer {
|
|
|
1590
1813
|
innerOther(node, axis) {
|
|
1591
1814
|
const key = `inner\n${node.name}\n${axis}`;
|
|
1592
1815
|
return this.guard(key, node, axis, undefined, () => {
|
|
1593
|
-
const content = this.contentOf(node);
|
|
1816
|
+
const content = this.contentOf(node, axis);
|
|
1594
1817
|
let low = Infinity;
|
|
1595
1818
|
let high = -Infinity;
|
|
1596
1819
|
for (const child of content) {
|
|
@@ -2015,9 +2238,8 @@ export class Placer {
|
|
|
2015
2238
|
const content = members.filter((member) => !this.framed(member));
|
|
2016
2239
|
const unplaced = content.filter((member) => this.unplaced(member));
|
|
2017
2240
|
const onTitle = content.find((member) => this.onTitle(member));
|
|
2018
|
-
if (
|
|
2019
|
-
|
|
2020
|
-
}
|
|
2241
|
+
if (unplaced.length === 0 && !(parent && this.anchoredOnTitle(parent)))
|
|
2242
|
+
this.splitAnchors(content, parent);
|
|
2021
2243
|
if (unplaced.length > 1) {
|
|
2022
2244
|
const names = unplaced.map((node) => `"${node.name}"`).join(', ');
|
|
2023
2245
|
throw new SourceError(parent
|
|
@@ -2034,6 +2256,62 @@ export class Placer {
|
|
|
2034
2256
|
for (const member of all)
|
|
2035
2257
|
this.checkAnchors(member.children, member);
|
|
2036
2258
|
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Every box in a frame says something about where it goes, so the frame is
|
|
2261
|
+
* anchored one axis at a time (design.org 5.3): across by the one box that
|
|
2262
|
+
* says nothing across, up and down by the one that says nothing up and
|
|
2263
|
+
* down. A lone direction's centering counts as saying nothing when its
|
|
2264
|
+
* target's place on that axis is worked out from the box itself, as with
|
|
2265
|
+
* `b right of a` and `a even with b.1`: the centering gives way to what was
|
|
2266
|
+
* written. An axis nobody leaves free is a circle, reported when placing,
|
|
2267
|
+
* and an axis a container is placed on by what it holds needs no anchor.
|
|
2268
|
+
*/
|
|
2269
|
+
splitAnchors(members, parent) {
|
|
2270
|
+
const free = BOTH_AXES.map((axis) => parent && this.frameAnchor(parent, axis)
|
|
2271
|
+
? []
|
|
2272
|
+
: members.filter((member) => this.statements(member, axis).length === 0 && this.loneGivesWay(member, axis)));
|
|
2273
|
+
if (!parent && free.every((on) => on.length === 0)) {
|
|
2274
|
+
throw new SourceError('every node is placed relative to another, so nothing anchors the diagram', 1);
|
|
2275
|
+
}
|
|
2276
|
+
BOTH_AXES.forEach((axis, index) => {
|
|
2277
|
+
const on = free[index];
|
|
2278
|
+
// With the other axis bound all round there is a circle, which placing reports better.
|
|
2279
|
+
if (on.length > 1 && free.every((other) => other.length > 0)) {
|
|
2280
|
+
const names = on.map((node) => `"${node.name}"`).join(', ');
|
|
2281
|
+
throw new SourceError(`exactly one node may say nothing about where it goes ${AXIS_SAID[axis]}, but ${on.length} do: ${names}`, on[1].line);
|
|
2282
|
+
}
|
|
2283
|
+
if (on.length === 1)
|
|
2284
|
+
this.axisAnchors.add(`${on[0].name}\n${axis}`);
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
/** Whether a box with nothing stated on this axis is left free there: no lone direction centers it, or the one that would is placed from the box itself. */
|
|
2288
|
+
loneGivesWay(node, axis) {
|
|
2289
|
+
const offsets = node.placements.filter((placement) => placement.kind === 'offset' && component(placement.direction, axis) === undefined);
|
|
2290
|
+
if (offsets.length === 0)
|
|
2291
|
+
return true;
|
|
2292
|
+
if (offsets.length > 1)
|
|
2293
|
+
return false;
|
|
2294
|
+
return this.namedFrom(offsets[0]).some((named) => this.placedFrom(named, axis, node, new Set()));
|
|
2295
|
+
}
|
|
2296
|
+
/** The boxes a placement names, through points and regions. */
|
|
2297
|
+
namedFrom(placement) {
|
|
2298
|
+
return placement.targets
|
|
2299
|
+
.flatMap((target) => this.boxesNamed(target, []))
|
|
2300
|
+
.flatMap((name) => this.byName.get(name) ?? []);
|
|
2301
|
+
}
|
|
2302
|
+
/** Whether a box's place on one axis is worked out, however indirectly, from `from` or something inside it. */
|
|
2303
|
+
placedFrom(node, axis, from, seen) {
|
|
2304
|
+
if (node === from || isWithin(node, from))
|
|
2305
|
+
return true;
|
|
2306
|
+
if (seen.has(node))
|
|
2307
|
+
return false;
|
|
2308
|
+
seen.add(node);
|
|
2309
|
+
const stated = this.statements(node, axis).map((statement) => statement.placement);
|
|
2310
|
+
const placements = stated.length > 0
|
|
2311
|
+
? stated
|
|
2312
|
+
: node.placements.filter((placement) => placement.kind === 'offset' && component(placement.direction, axis) === undefined);
|
|
2313
|
+
return placements.some((placement) => this.namedFrom(placement).some((named) => this.placedFrom(named, axis, from, seen)));
|
|
2314
|
+
}
|
|
2037
2315
|
/**
|
|
2038
2316
|
* A box in a venn's part may be placed against a box in another part, and
|
|
2039
2317
|
* then it says where its whole part goes up and down: so it must be the
|
|
@@ -2173,6 +2451,13 @@ export class Placer {
|
|
|
2173
2451
|
return { low, high };
|
|
2174
2452
|
}
|
|
2175
2453
|
}
|
|
2454
|
+
const NO_ROOM = { low: 0, high: 0 };
|
|
2455
|
+
/** Less than this is no distance at all, for a limit worked out in floating point. */
|
|
2456
|
+
const LIMIT_EPSILON = 0.01;
|
|
2457
|
+
/** The axis a limit keeps to: up and down for above and below. */
|
|
2458
|
+
function limitAxis(limit) {
|
|
2459
|
+
return limit.way === 'above' || limit.way === 'below' ? 'y' : 'x';
|
|
2460
|
+
}
|
|
2176
2461
|
/**
|
|
2177
2462
|
* `top even with 2 below design top` on a venn's set: the other set's same
|
|
2178
2463
|
* side, moved up or down. Anything else, such as its bottom or a sideways
|
package/dist/render.d.ts
CHANGED
|
@@ -10,3 +10,20 @@ export interface RenderOptions {
|
|
|
10
10
|
}
|
|
11
11
|
/** Turn solved geometry into a standalone SVG document. */
|
|
12
12
|
export declare function render(layout: Layout, options?: RenderOptions): string;
|
|
13
|
+
/**
|
|
14
|
+
* The SVG as `render` gives it, and the same drawing with each thing wrapped in
|
|
15
|
+
* a group naming it and its statement's line, for a page that points at things
|
|
16
|
+
* (`--page`). Drawn once, so the two can't disagree.
|
|
17
|
+
*/
|
|
18
|
+
export declare function renderTagged(layout: Layout, options?: RenderOptions): {
|
|
19
|
+
svg: string;
|
|
20
|
+
tagged: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The colors a drawing is made in. A theme passed in — the command line's
|
|
24
|
+
* `--theme` — beats the one the file names, so one source renders in either.
|
|
25
|
+
* `diagram background:` and `text:` are the author overruling a color of
|
|
26
|
+
* whichever theme that is, and a color written by hand wins over any theme, so
|
|
27
|
+
* they are folded in afterwards and everything downstream sees one theme.
|
|
28
|
+
*/
|
|
29
|
+
export declare function themeOf(layout: Layout, options?: RenderOptions): Theme;
|