reladraw 0.15.1 → 0.17.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 +11 -7
- package/SYNTAX.md +818 -680
- package/dist/ast.d.ts +342 -18
- package/dist/ast.js +138 -27
- package/dist/checks.d.ts +58 -0
- package/dist/checks.js +891 -0
- package/dist/cli.js +88 -12
- package/dist/constants.d.ts +15 -55
- package/dist/constants.js +30 -78
- package/dist/grammar.d.ts +2 -2
- package/dist/grammar.js +70 -10
- package/dist/index.d.ts +20 -0
- package/dist/index.js +18 -1
- package/dist/lexer.d.ts +12 -0
- package/dist/lexer.js +34 -0
- package/dist/lines.d.ts +96 -0
- package/dist/lines.js +1968 -0
- package/dist/linetexts.d.ts +44 -0
- package/dist/linetexts.js +412 -0
- package/dist/model.d.ts +187 -36
- package/dist/model.js +23 -1
- package/dist/parser.js +1811 -107
- package/dist/place.d.ts +501 -0
- package/dist/place.js +2226 -0
- package/dist/render.d.ts +3 -1
- package/dist/render.js +691 -2493
- package/dist/resolve.d.ts +1 -1
- package/dist/resolve.js +1195 -1789
- package/dist/size.d.ts +64 -0
- package/dist/size.js +222 -0
- package/dist/themes.d.ts +17 -0
- package/dist/themes.js +28 -3
- package/package.json +2 -2
- package/dist/constrain.d.ts +0 -56
- package/dist/constrain.js +0 -95
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { LayoutEdge, LayoutEdgeText } from './model.js';
|
|
2
|
+
import { type Point, type Route } from './lines.js';
|
|
3
|
+
/**
|
|
4
|
+
* A text's distance from what it is placed against when nothing says:
|
|
5
|
+
* from the node for `at: start`, from a box for `above s1`, off the line for
|
|
6
|
+
* `right of edge`. Half a unit, because a text names the thing beside it and
|
|
7
|
+
* reads as detached any further off.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TEXT_GAP = 6;
|
|
10
|
+
/** How far the hole a text knocks in its line reaches past the words, either side. */
|
|
11
|
+
export declare const KNOCKOUT_PAD = 5;
|
|
12
|
+
/**
|
|
13
|
+
* The room the patch behind a text leaves round it, across and up and down:
|
|
14
|
+
* by default the knockout's reach either side and none above or below, and
|
|
15
|
+
* `padding:` in its bracket all round, as a box's padding is.
|
|
16
|
+
*/
|
|
17
|
+
export declare function patchPadding(text: LayoutEdgeText, subject: string, line: number): {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
};
|
|
21
|
+
/** One text, placed. */
|
|
22
|
+
export interface PlacedText {
|
|
23
|
+
text: LayoutEdgeText;
|
|
24
|
+
/** The middle of the words. */
|
|
25
|
+
center: Point;
|
|
26
|
+
/** Degrees clockwise, as SVG turns: 0 reads left to right, -90 up, 90 down. */
|
|
27
|
+
angle: number;
|
|
28
|
+
/** What the text covers on the page, its knockout included, as an upright box. */
|
|
29
|
+
box: {
|
|
30
|
+
minX: number;
|
|
31
|
+
minY: number;
|
|
32
|
+
maxX: number;
|
|
33
|
+
maxY: number;
|
|
34
|
+
};
|
|
35
|
+
/** Moved off its line by `right of edge` and the rest, rather than sitting on it. */
|
|
36
|
+
beside: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Every text on one line, placed. Texts placed along the line, by a box, or
|
|
40
|
+
* near an end cut the line into stretches, and the texts with no place of
|
|
41
|
+
* their own spread evenly within the stretch they were written in: one at
|
|
42
|
+
* the middle, three at a quarter, a half and three-quarters (design.org 7.3).
|
|
43
|
+
*/
|
|
44
|
+
export declare function placeLineTexts(edge: LayoutEdge, route: Route): PlacedText[];
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { arrowLength } from './constants.js';
|
|
2
|
+
import { SourceError } from './errors.js';
|
|
3
|
+
import { gapPixels } from './place.js';
|
|
4
|
+
import { traceRoute } from './lines.js';
|
|
5
|
+
/**
|
|
6
|
+
* A text's distance from what it is placed against when nothing says:
|
|
7
|
+
* from the node for `at: start`, from a box for `above s1`, off the line for
|
|
8
|
+
* `right of edge`. Half a unit, because a text names the thing beside it and
|
|
9
|
+
* reads as detached any further off.
|
|
10
|
+
*/
|
|
11
|
+
export const TEXT_GAP = 6;
|
|
12
|
+
/** How far the hole a text knocks in its line reaches past the words, either side. */
|
|
13
|
+
export const KNOCKOUT_PAD = 5;
|
|
14
|
+
/**
|
|
15
|
+
* The room the patch behind a text leaves round it, across and up and down:
|
|
16
|
+
* by default the knockout's reach either side and none above or below, and
|
|
17
|
+
* `padding:` in its bracket all round, as a box's padding is.
|
|
18
|
+
*/
|
|
19
|
+
export function patchPadding(text, subject, line) {
|
|
20
|
+
const written = text.textAttrs['padding'];
|
|
21
|
+
if (written === undefined)
|
|
22
|
+
return { x: KNOCKOUT_PAD, y: 0 };
|
|
23
|
+
const by = gapPixels(written, `${subject}: the text "${text.text}" padding`, line);
|
|
24
|
+
return { x: by, y: by };
|
|
25
|
+
}
|
|
26
|
+
function measured(points) {
|
|
27
|
+
const lengths = [0];
|
|
28
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
29
|
+
const [a, b] = [points[index - 1], points[index]];
|
|
30
|
+
lengths.push(lengths[index - 1] + Math.hypot(b.x - a.x, b.y - a.y));
|
|
31
|
+
}
|
|
32
|
+
return { points, lengths, total: lengths[lengths.length - 1] };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `on: longest level`: the line's longest straight run left to right, level
|
|
36
|
+
* pieces in a row at one height counted as one, the first of two the same
|
|
37
|
+
* length. A curve has no straight pieces, so a curved line has none, and
|
|
38
|
+
* asking for it there is refused rather than falling back to the middle.
|
|
39
|
+
*/
|
|
40
|
+
function longestLevel(route, about) {
|
|
41
|
+
let best = [];
|
|
42
|
+
let run = [];
|
|
43
|
+
let from = route.start;
|
|
44
|
+
for (const piece of route.pieces) {
|
|
45
|
+
const level = !('c1' in piece) && Math.abs(piece.to.y - from.y) < 0.5 && Math.abs(piece.to.x - from.x) >= 0.5;
|
|
46
|
+
if (!level)
|
|
47
|
+
run = [];
|
|
48
|
+
else if (run.length > 0 && Math.abs(run[0].y - from.y) < 0.5)
|
|
49
|
+
run.push(piece.to);
|
|
50
|
+
else
|
|
51
|
+
run = [from, piece.to];
|
|
52
|
+
if (run.length > 0 && Math.abs(run[run.length - 1].x - run[0].x) > Math.abs((best[best.length - 1]?.x ?? 0) - (best[0]?.x ?? 0)) + 0.5)
|
|
53
|
+
best = [...run];
|
|
54
|
+
from = piece.to;
|
|
55
|
+
}
|
|
56
|
+
if (best.length === 0) {
|
|
57
|
+
throw new SourceError(`${about} is on: longest level, and this line has no level piece — place the text along it with at:`, route.edge.line);
|
|
58
|
+
}
|
|
59
|
+
return measured(best);
|
|
60
|
+
}
|
|
61
|
+
/** The piece of a polyline that holds the spot `s` along it, never one of no length if another will do. */
|
|
62
|
+
function pieceHolding(line, s) {
|
|
63
|
+
let index = 1;
|
|
64
|
+
while (index < line.points.length - 1 && line.lengths[index] < s)
|
|
65
|
+
index += 1;
|
|
66
|
+
// A spot at a corner belongs to the piece after it, and a piece of no length to its neighbor.
|
|
67
|
+
while (index < line.points.length - 1 && line.lengths[index] - line.lengths[index - 1] < 1e-9)
|
|
68
|
+
index += 1;
|
|
69
|
+
while (index > 1 && line.lengths[index] - line.lengths[index - 1] < 1e-9)
|
|
70
|
+
index -= 1;
|
|
71
|
+
return index;
|
|
72
|
+
}
|
|
73
|
+
function spotAlong(line, s) {
|
|
74
|
+
if (line.points.length === 1)
|
|
75
|
+
return line.points[0];
|
|
76
|
+
const index = pieceHolding(line, s);
|
|
77
|
+
const [a, b] = [line.points[index - 1], line.points[index]];
|
|
78
|
+
const piece = line.lengths[index] - line.lengths[index - 1];
|
|
79
|
+
const t = piece === 0 ? 0 : Math.min(1, Math.max(0, (s - line.lengths[index - 1]) / piece));
|
|
80
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
81
|
+
}
|
|
82
|
+
/** The way the line runs at `s`, from its start toward its end, as a unit step. */
|
|
83
|
+
function headingAlong(line, s) {
|
|
84
|
+
if (line.points.length === 1)
|
|
85
|
+
return { x: 1, y: 0 };
|
|
86
|
+
const index = pieceHolding(line, s);
|
|
87
|
+
const [a, b] = [line.points[index - 1], line.points[index]];
|
|
88
|
+
const length = Math.hypot(b.x - a.x, b.y - a.y);
|
|
89
|
+
return length === 0 ? { x: 1, y: 0 } : { x: (b.x - a.x) / length, y: (b.y - a.y) / length };
|
|
90
|
+
}
|
|
91
|
+
/** How far along the line the point on it nearest `p` is. */
|
|
92
|
+
function distanceTo(line, p) {
|
|
93
|
+
let best = 0;
|
|
94
|
+
let nearest = Infinity;
|
|
95
|
+
for (let index = 1; index < line.points.length; index += 1) {
|
|
96
|
+
const [a, b] = [line.points[index - 1], line.points[index]];
|
|
97
|
+
const dx = b.x - a.x;
|
|
98
|
+
const dy = b.y - a.y;
|
|
99
|
+
const span = dx * dx + dy * dy;
|
|
100
|
+
const t = span === 0 ? 0 : Math.min(1, Math.max(0, ((p.x - a.x) * dx + (p.y - a.y) * dy) / span));
|
|
101
|
+
const off = Math.hypot(a.x + dx * t - p.x, a.y + dy * t - p.y);
|
|
102
|
+
if (off < nearest - 1e-9) {
|
|
103
|
+
nearest = off;
|
|
104
|
+
best = line.lengths[index - 1] + t * Math.sqrt(span);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return best;
|
|
108
|
+
}
|
|
109
|
+
/** Where a ray from `origin` the way `way` points first meets the line, as a distance along the line. */
|
|
110
|
+
function firstMeeting(line, origin, way) {
|
|
111
|
+
let nearest = Infinity;
|
|
112
|
+
let found;
|
|
113
|
+
for (let index = 1; index < line.points.length; index += 1) {
|
|
114
|
+
const [a, b] = [line.points[index - 1], line.points[index]];
|
|
115
|
+
const ex = b.x - a.x;
|
|
116
|
+
const ey = b.y - a.y;
|
|
117
|
+
const denominator = way.x * ey - way.y * ex;
|
|
118
|
+
if (Math.abs(denominator) < 1e-12) {
|
|
119
|
+
// Running along the ray: the nearer end of the piece is where it is met, if it is on the ray at all.
|
|
120
|
+
const across = (a.x - origin.x) * way.y - (a.y - origin.y) * way.x;
|
|
121
|
+
if (Math.abs(across) > 1e-6)
|
|
122
|
+
continue;
|
|
123
|
+
for (const [end, t] of [[a, 0], [b, 1]]) {
|
|
124
|
+
const out = (end.x - origin.x) * way.x + (end.y - origin.y) * way.y;
|
|
125
|
+
if (out >= -1e-9 && out < nearest) {
|
|
126
|
+
nearest = out;
|
|
127
|
+
found = line.lengths[index - 1] + t * (line.lengths[index] - line.lengths[index - 1]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const wx = a.x - origin.x;
|
|
133
|
+
const wy = a.y - origin.y;
|
|
134
|
+
const out = (wx * ey - wy * ex) / denominator;
|
|
135
|
+
const t = (wx * way.y - wy * way.x) / denominator;
|
|
136
|
+
if (out < -1e-9 || t < -1e-9 || t > 1 + 1e-9)
|
|
137
|
+
continue;
|
|
138
|
+
if (out < nearest) {
|
|
139
|
+
nearest = out;
|
|
140
|
+
found = line.lengths[index - 1] + Math.min(1, Math.max(0, t)) * (line.lengths[index] - line.lengths[index - 1]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return found;
|
|
144
|
+
}
|
|
145
|
+
/** Where the line first reaches `value` on `axis`, from its start, as a distance along it. */
|
|
146
|
+
function firstReaching(line, axis, value) {
|
|
147
|
+
for (let index = 1; index < line.points.length; index += 1) {
|
|
148
|
+
const [a, b] = [line.points[index - 1], line.points[index]];
|
|
149
|
+
const low = Math.min(a[axis], b[axis]);
|
|
150
|
+
const high = Math.max(a[axis], b[axis]);
|
|
151
|
+
if (value < low - 1e-9 || value > high + 1e-9)
|
|
152
|
+
continue;
|
|
153
|
+
const span = b[axis] - a[axis];
|
|
154
|
+
const t = Math.abs(span) < 1e-12 ? 0 : (value - a[axis]) / span;
|
|
155
|
+
return line.lengths[index - 1] + Math.min(1, Math.max(0, t)) * (line.lengths[index] - line.lengths[index - 1]);
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The stretch of the line around `s` that stays inside a band across the page,
|
|
161
|
+
* walked both ways from `s` until it leaves: the part a text beside the line
|
|
162
|
+
* landed on (design.org 7.3), all of it within the text's own height (for a
|
|
163
|
+
* side to the left or right) or width (above or below).
|
|
164
|
+
*/
|
|
165
|
+
function partInBand(line, s, axis, low, high) {
|
|
166
|
+
const inside = (p) => p[axis] >= low - 1e-6 && p[axis] <= high + 1e-6;
|
|
167
|
+
const here = spotAlong(line, s);
|
|
168
|
+
const part = [here];
|
|
169
|
+
if (!inside(here))
|
|
170
|
+
return part;
|
|
171
|
+
const edgeOf = (a, b) => {
|
|
172
|
+
const bound = b[axis] > high ? high : low;
|
|
173
|
+
const t = (bound - a[axis]) / (b[axis] - a[axis]);
|
|
174
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
175
|
+
};
|
|
176
|
+
const index = pieceHolding(line, s);
|
|
177
|
+
let from = here;
|
|
178
|
+
for (let at = index; at < line.points.length; at += 1) {
|
|
179
|
+
const next = line.points[at];
|
|
180
|
+
if (!inside(next)) {
|
|
181
|
+
part.push(edgeOf(from, next));
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
part.push(next);
|
|
185
|
+
from = next;
|
|
186
|
+
}
|
|
187
|
+
from = here;
|
|
188
|
+
for (let at = index - 1; at >= 0; at -= 1) {
|
|
189
|
+
const next = line.points[at];
|
|
190
|
+
if (!inside(next)) {
|
|
191
|
+
part.push(edgeOf(from, next));
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
part.push(next);
|
|
195
|
+
from = next;
|
|
196
|
+
}
|
|
197
|
+
return part;
|
|
198
|
+
}
|
|
199
|
+
/** The size of a box `width` by `height` turned by `angle` degrees, as an upright box. */
|
|
200
|
+
function turned(width, height, angle) {
|
|
201
|
+
const radians = (angle * Math.PI) / 180;
|
|
202
|
+
const cos = Math.abs(Math.cos(radians));
|
|
203
|
+
const sin = Math.abs(Math.sin(radians));
|
|
204
|
+
return { width: width * cos + height * sin, height: width * sin + height * cos };
|
|
205
|
+
}
|
|
206
|
+
/** Which way a text reads, from its bracket and the way the line runs where it sits (design.org 7.3). */
|
|
207
|
+
function angleOf(text, heading) {
|
|
208
|
+
const reads = text.place.reads;
|
|
209
|
+
if (reads === undefined)
|
|
210
|
+
return 0;
|
|
211
|
+
if (reads === 'up')
|
|
212
|
+
return -90;
|
|
213
|
+
if (reads === 'down')
|
|
214
|
+
return 90;
|
|
215
|
+
// Along: the line's angle, read whichever way is closer to upright, so never upside down.
|
|
216
|
+
if (Math.abs(heading.x) < 1e-6) {
|
|
217
|
+
// Exactly upright, both ways are readable. Beside the line the text faces
|
|
218
|
+
// it, feet toward it; on the line it follows the line from the first name
|
|
219
|
+
// written toward the second.
|
|
220
|
+
const beside = text.place.beside;
|
|
221
|
+
if (beside === 'right of')
|
|
222
|
+
return 90;
|
|
223
|
+
if (beside === 'left of')
|
|
224
|
+
return -90;
|
|
225
|
+
return heading.y > 0 ? 90 : -90;
|
|
226
|
+
}
|
|
227
|
+
let angle = (Math.atan2(heading.y, heading.x) * 180) / Math.PI;
|
|
228
|
+
if (angle > 90)
|
|
229
|
+
angle -= 180;
|
|
230
|
+
if (angle < -90)
|
|
231
|
+
angle += 180;
|
|
232
|
+
return angle;
|
|
233
|
+
}
|
|
234
|
+
/** One way on the page, as a unit step. */
|
|
235
|
+
const WAY_STEP = {
|
|
236
|
+
up: { x: 0, y: -1 },
|
|
237
|
+
down: { x: 0, y: 1 },
|
|
238
|
+
left: { x: -1, y: 0 },
|
|
239
|
+
right: { x: 1, y: 0 },
|
|
240
|
+
};
|
|
241
|
+
/**
|
|
242
|
+
* Every text on one line, placed. Texts placed along the line, by a box, or
|
|
243
|
+
* near an end cut the line into stretches, and the texts with no place of
|
|
244
|
+
* their own spread evenly within the stretch they were written in: one at
|
|
245
|
+
* the middle, three at a quarter, a half and three-quarters (design.org 7.3).
|
|
246
|
+
*/
|
|
247
|
+
export function placeLineTexts(edge, route) {
|
|
248
|
+
if (edge.texts.length === 0)
|
|
249
|
+
return [];
|
|
250
|
+
const whole = measured(traceRoute(route));
|
|
251
|
+
const subject = `edge ${edge.from.name} ${edge.arrow} ${edge.to.name}`;
|
|
252
|
+
// Where each text lands on the line, as a distance along the whole of it.
|
|
253
|
+
const landing = edge.texts.map((text) => {
|
|
254
|
+
const place = text.place;
|
|
255
|
+
const about = `${subject}: the text "${text.text}"`;
|
|
256
|
+
const on = place.longestLevel
|
|
257
|
+
? longestLevel(route, about)
|
|
258
|
+
: place.on === undefined
|
|
259
|
+
? whole
|
|
260
|
+
: measured(route.legs.get(place.on) ?? []);
|
|
261
|
+
const onLeg = place.longestLevel ? ' (its longest level piece)' : place.on === undefined ? '' : ` (leg ${place.on})`;
|
|
262
|
+
const toWhole = (s) => (on === whole ? s : distanceTo(whole, spotAlong(on, s)));
|
|
263
|
+
if (typeof place.at === 'number')
|
|
264
|
+
return toWhole(place.at * on.total);
|
|
265
|
+
if (place.at === 'start' || place.at === 'end') {
|
|
266
|
+
// A gap along the line from the end, clear of a mark there, however long the line.
|
|
267
|
+
const fromEnd = place.at === 'start';
|
|
268
|
+
const heading = headingAlong(on, fromEnd ? 0 : on.total);
|
|
269
|
+
const size = turned(text.width, text.height, angleOf(text, heading));
|
|
270
|
+
const half = (size.width / 2) * Math.abs(heading.x) + (size.height / 2) * Math.abs(heading.y);
|
|
271
|
+
const mark = on === whole && edge.marks[fromEnd ? 'from' : 'to'] !== 'none' ? arrowLength(edge.look.thickness) : 0;
|
|
272
|
+
const gap = place.gap === undefined ? TEXT_GAP : gapPixels(place.gap, `${about}: gap:`, edge.line);
|
|
273
|
+
const s = Math.min(on.total, mark + gap + half);
|
|
274
|
+
return toWhole(fromEnd ? s : on.total - s);
|
|
275
|
+
}
|
|
276
|
+
if (place.straight) {
|
|
277
|
+
const box = text.target;
|
|
278
|
+
const origin = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
279
|
+
const s = firstMeeting(on, origin, WAY_STEP[place.straight.way]);
|
|
280
|
+
if (s === undefined) {
|
|
281
|
+
throw new SourceError(`${about} is placed ${place.straight.written}, and a way ${place.straight.way} from there never meets this line${onLeg} — ` +
|
|
282
|
+
'name another box, or place the text along the line with at:', edge.line);
|
|
283
|
+
}
|
|
284
|
+
return toWhole(s);
|
|
285
|
+
}
|
|
286
|
+
if (place.by && !place.onLine) {
|
|
287
|
+
// Off the line by the box; it lands on the line where the line comes nearest, for ordering the rest.
|
|
288
|
+
const gap = place.gap === undefined ? TEXT_GAP : gapPixels(place.gap, `${about}: gap:`, edge.line);
|
|
289
|
+
return distanceTo(whole, byBox(place.by.side, text.target, gap, turned(text.width, text.height, angleOf(text, { x: 1, y: 0 }))));
|
|
290
|
+
}
|
|
291
|
+
if (place.by) {
|
|
292
|
+
const box = text.target;
|
|
293
|
+
const gap = place.gap === undefined ? TEXT_GAP : gapPixels(place.gap, `${about}: gap:`, edge.line);
|
|
294
|
+
const [axis, value] = reachFor(place.by.side, box, gap);
|
|
295
|
+
const s = firstReaching(on, axis, value);
|
|
296
|
+
if (s === undefined) {
|
|
297
|
+
throw new SourceError(`${about} is placed ${place.by.written}, and this line${onLeg} never reaches ` +
|
|
298
|
+
`${axis === 'y' ? 'that height' : 'that far across'} — name another box, or place the text along the line with at:`, edge.line);
|
|
299
|
+
}
|
|
300
|
+
return toWhole(s);
|
|
301
|
+
}
|
|
302
|
+
if (on !== whole)
|
|
303
|
+
return toWhole(on.total / 2);
|
|
304
|
+
return undefined;
|
|
305
|
+
});
|
|
306
|
+
// The unplaced ones, spread in the stretch between the placed ones written either side of them.
|
|
307
|
+
for (let index = 0; index < landing.length;) {
|
|
308
|
+
if (landing[index] !== undefined) {
|
|
309
|
+
index += 1;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
let past = index;
|
|
313
|
+
while (past < landing.length && landing[past] === undefined)
|
|
314
|
+
past += 1;
|
|
315
|
+
const low = index > 0 ? landing[index - 1] : 0;
|
|
316
|
+
const high = past < landing.length ? landing[past] : whole.total;
|
|
317
|
+
const count = past - index;
|
|
318
|
+
for (let at = 0; at < count; at += 1)
|
|
319
|
+
landing[index + at] = low + ((high - low) * (at + 1)) / (count + 1);
|
|
320
|
+
index = past;
|
|
321
|
+
}
|
|
322
|
+
return edge.texts.map((text, index) => {
|
|
323
|
+
const s = landing[index];
|
|
324
|
+
const place = text.place;
|
|
325
|
+
const spot = spotAlong(whole, s);
|
|
326
|
+
const angle = angleOf(text, headingAlong(whole, s));
|
|
327
|
+
const size = turned(text.width, text.height, angle);
|
|
328
|
+
let center = spot;
|
|
329
|
+
if (place.by && !place.onLine) {
|
|
330
|
+
const gap = place.gap === undefined ? TEXT_GAP : gapPixels(place.gap, `${subject}: the text "${text.text}" gap:`, edge.line);
|
|
331
|
+
center = byBox(place.by.side, text.target, gap, size);
|
|
332
|
+
}
|
|
333
|
+
else if (place.by) {
|
|
334
|
+
// The text's own edge facing the box is on the crossing, its middle at the line there.
|
|
335
|
+
const out = { above: [0, -1], below: [0, 1], 'left of': [-1, 0], 'right of': [1, 0] }[place.by.side];
|
|
336
|
+
center = { x: spot.x + (out[0] * size.width) / 2, y: spot.y + (out[1] * size.height) / 2 };
|
|
337
|
+
}
|
|
338
|
+
if (place.beside) {
|
|
339
|
+
const gap = place.edgeGap === undefined ? TEXT_GAP : gapPixels(place.edgeGap, `${subject}: the text "${text.text}" edge-gap`, edge.line);
|
|
340
|
+
center = besideLine(whole, s, center, size, place.beside, gap);
|
|
341
|
+
}
|
|
342
|
+
const pad = patchPadding(text, subject, edge.line);
|
|
343
|
+
const knockout = turned(text.width + pad.x * 2, text.height + pad.y * 2, angle);
|
|
344
|
+
for (const even of text.evenAt ?? []) {
|
|
345
|
+
// `left even with x right` sets one axis, measured to the patch's edge, the edge that is seen; the other stays where the line put it.
|
|
346
|
+
const half = (even.axis === 'x' ? knockout.width : knockout.height) / 2;
|
|
347
|
+
const start = even.axis === 'x' ? even.rect.x : even.rect.y;
|
|
348
|
+
const length = even.axis === 'x' ? even.rect.width : even.rect.height;
|
|
349
|
+
const value = even.side === 'start' ? start + half : even.side === 'end' ? start + length - half : start + length / 2;
|
|
350
|
+
center = even.axis === 'x' ? { x: value, y: center.y } : { x: center.x, y: value };
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
text,
|
|
354
|
+
center,
|
|
355
|
+
angle,
|
|
356
|
+
box: {
|
|
357
|
+
minX: center.x - knockout.width / 2,
|
|
358
|
+
minY: center.y - knockout.height / 2,
|
|
359
|
+
maxX: center.x + knockout.width / 2,
|
|
360
|
+
maxY: center.y + knockout.height / 2,
|
|
361
|
+
},
|
|
362
|
+
beside: place.beside !== undefined,
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/** For `2 above s1` and the rest: the text's middle, a gap off the box and centered on it the other way, as a box placed so would be. */
|
|
367
|
+
function byBox(side, box, gap, size) {
|
|
368
|
+
const middle = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
369
|
+
switch (side) {
|
|
370
|
+
case 'above':
|
|
371
|
+
return { x: middle.x, y: box.y - gap - size.height / 2 };
|
|
372
|
+
case 'below':
|
|
373
|
+
return { x: middle.x, y: box.y + box.height + gap + size.height / 2 };
|
|
374
|
+
case 'left of':
|
|
375
|
+
return { x: box.x - gap - size.width / 2, y: middle.y };
|
|
376
|
+
case 'right of':
|
|
377
|
+
return { x: box.x + box.width + gap + size.width / 2, y: middle.y };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** For `above s1, on the line` and the rest: the axis the line must reach, and where along it, a gap off the box. */
|
|
381
|
+
function reachFor(side, box, gap) {
|
|
382
|
+
switch (side) {
|
|
383
|
+
case 'above':
|
|
384
|
+
return ['y', box.y - gap];
|
|
385
|
+
case 'below':
|
|
386
|
+
return ['y', box.y + box.height + gap];
|
|
387
|
+
case 'left of':
|
|
388
|
+
return ['x', box.x - gap];
|
|
389
|
+
case 'right of':
|
|
390
|
+
return ['x', box.x + box.width + gap];
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* A text moved off its line the stated way, on the page, until it is a gap
|
|
395
|
+
* clear of the part of the line it landed on. No angle is judged, so a level
|
|
396
|
+
* stretch with `right of edge` puts the text past its right end, and a
|
|
397
|
+
* near-level curve never flips a text from one side to the other.
|
|
398
|
+
*/
|
|
399
|
+
function besideLine(line, s, center, size, side, gap) {
|
|
400
|
+
if (side === 'left of' || side === 'right of') {
|
|
401
|
+
const part = partInBand(line, s, 'y', center.y - size.height / 2, center.y + size.height / 2);
|
|
402
|
+
const xs = part.map((p) => p.x);
|
|
403
|
+
return side === 'right of'
|
|
404
|
+
? { x: Math.max(...xs) + gap + size.width / 2, y: center.y }
|
|
405
|
+
: { x: Math.min(...xs) - gap - size.width / 2, y: center.y };
|
|
406
|
+
}
|
|
407
|
+
const part = partInBand(line, s, 'x', center.x - size.width / 2, center.x + size.width / 2);
|
|
408
|
+
const ys = part.map((p) => p.y);
|
|
409
|
+
return side === 'below'
|
|
410
|
+
? { x: center.x, y: Math.max(...ys) + gap + size.height / 2 }
|
|
411
|
+
: { x: center.x, y: Math.min(...ys) - gap - size.height / 2 };
|
|
412
|
+
}
|