reladraw 0.11.1 → 0.13.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 +17 -1
- package/SYNTAX.md +28 -16
- package/dist/element.d.ts +15 -0
- package/dist/element.js +87 -0
- package/dist/render.js +692 -642
- package/dist/search.d.ts +34 -5
- package/dist/search.js +90 -30
- package/package.json +2 -2
package/dist/search.d.ts
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* straight in, with a straight stretch at each end at least `stub` long, so an
|
|
12
12
|
* arrowhead always lands on a straight piece. Between those two stretches it
|
|
13
13
|
* takes the cheapest way through the grid, where cost is length plus a charge
|
|
14
|
-
* for every turn
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* than the left.
|
|
14
|
+
* for every turn and for every line already placed that it crosses. On a
|
|
15
|
+
* right-angled grid many ways tie on length — a staircase is exactly as long as
|
|
16
|
+
* one bend — and the turn charge breaks those ties toward the fewest bends,
|
|
17
|
+
* which is the line a person would draw. What still ties after that goes over
|
|
18
|
+
* the top rather than under, and round the right rather than the left.
|
|
19
19
|
*
|
|
20
20
|
* A way through a box is not a losing option here. It is not a way at all.
|
|
21
21
|
*/
|
|
@@ -57,6 +57,35 @@ export interface SearchOptions {
|
|
|
57
57
|
*/
|
|
58
58
|
taken: SearchPoint[][];
|
|
59
59
|
lane: number;
|
|
60
|
+
/**
|
|
61
|
+
* For each line in `taken`, for each of its stretches, half the depth across
|
|
62
|
+
* it of a text riding there, and 0 where none does. A line running beside a
|
|
63
|
+
* stretch carrying a text keeps clear of the text, not just of the line.
|
|
64
|
+
*/
|
|
65
|
+
takenHalves?: number[][];
|
|
66
|
+
/**
|
|
67
|
+
* A gap the line must pass through: somewhere it crosses the line at `at`
|
|
68
|
+
* on the `run` axis, travelling along `run`, at a level between `lo` and `hi`.
|
|
69
|
+
*/
|
|
70
|
+
gate?: SearchGate;
|
|
71
|
+
/**
|
|
72
|
+
* Half the depth of this line's own text, which rides in the gate: inside it,
|
|
73
|
+
* the line keeps that much further from the lines beside it.
|
|
74
|
+
*/
|
|
75
|
+
ownHalf?: number;
|
|
76
|
+
/**
|
|
77
|
+
* How far the line may go at all: the diagram with room round it. A wall
|
|
78
|
+
* reaching past this — a clause's fence, which runs to the edge of the page —
|
|
79
|
+
* cannot be gone round at its far end.
|
|
80
|
+
*/
|
|
81
|
+
bounds?: SearchWall;
|
|
82
|
+
}
|
|
83
|
+
/** A gap a line must pass through. See `SearchOptions.gate`. */
|
|
84
|
+
export interface SearchGate {
|
|
85
|
+
run: 'x' | 'y';
|
|
86
|
+
at: number;
|
|
87
|
+
lo: number;
|
|
88
|
+
hi: number;
|
|
60
89
|
}
|
|
61
90
|
export interface SearchResult {
|
|
62
91
|
points: SearchPoint[];
|
package/dist/search.js
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* straight in, with a straight stretch at each end at least `stub` long, so an
|
|
12
12
|
* arrowhead always lands on a straight piece. Between those two stretches it
|
|
13
13
|
* takes the cheapest way through the grid, where cost is length plus a charge
|
|
14
|
-
* for every turn
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* than the left.
|
|
14
|
+
* for every turn and for every line already placed that it crosses. On a
|
|
15
|
+
* right-angled grid many ways tie on length — a staircase is exactly as long as
|
|
16
|
+
* one bend — and the turn charge breaks those ties toward the fewest bends,
|
|
17
|
+
* which is the line a person would draw. What still ties after that goes over
|
|
18
|
+
* the top rather than under, and round the right rather than the left.
|
|
19
19
|
*
|
|
20
20
|
* A way through a box is not a losing option here. It is not a way at all.
|
|
21
21
|
*/
|
|
@@ -45,8 +45,25 @@ export function searchRoute(start, end, walls, options) {
|
|
|
45
45
|
return undefined;
|
|
46
46
|
if (inside(out, walls) || inside(back, walls))
|
|
47
47
|
return undefined;
|
|
48
|
-
const
|
|
49
|
-
const
|
|
48
|
+
const bounds = options.bounds;
|
|
49
|
+
const gate = options.gate;
|
|
50
|
+
// How far a stretch keeps from a placed one beside it: a lane, or clear of
|
|
51
|
+
// the text either of them carries there.
|
|
52
|
+
const halves = options.takenHalves ?? [];
|
|
53
|
+
const ownHalf = options.ownHalf ?? 0;
|
|
54
|
+
const inGate = (a, b) => {
|
|
55
|
+
if (!gate)
|
|
56
|
+
return false;
|
|
57
|
+
const along = gate.run === 'x' ? Math.abs(a.y - b.y) < 0.5 : Math.abs(a.x - b.x) < 0.5;
|
|
58
|
+
const level = gate.run === 'x' ? a.y : a.x;
|
|
59
|
+
return along && level > gate.lo && level < gate.hi;
|
|
60
|
+
};
|
|
61
|
+
const spacing = (line, stretch, own) => Math.max(options.lane, (halves[line]?.[stretch] ?? 0) + own);
|
|
62
|
+
const gateTracks = (axis) => !gate ? [] : axis === gate.run ? [gate.at] : [(gate.lo + gate.hi) / 2];
|
|
63
|
+
const xs = tracks(walls, 'x', clear, [out.x, back.x, ...gateTracks('x')], options.taken, spacing, ownHalf)
|
|
64
|
+
.filter((x) => !bounds || (x >= bounds.minX && x <= bounds.maxX) || x === out.x || x === back.x);
|
|
65
|
+
const ys = tracks(walls, 'y', clear, [out.y, back.y, ...gateTracks('y')], options.taken, spacing, ownHalf)
|
|
66
|
+
.filter((y) => !bounds || (y >= bounds.minY && y <= bounds.maxY) || y === out.y || y === back.y);
|
|
50
67
|
const ix = new Map(xs.map((x, index) => [x, index]));
|
|
51
68
|
const iy = new Map(ys.map((y, index) => [y, index]));
|
|
52
69
|
const width = xs.length;
|
|
@@ -69,14 +86,17 @@ export function searchRoute(start, end, walls, options) {
|
|
|
69
86
|
const tj = iy.get(back.y);
|
|
70
87
|
// Cost is compared first on length and turns, then on a tie-break that
|
|
71
88
|
// prefers a stretch higher up the page, or further right, so that of two
|
|
72
|
-
// ways round that are exactly as long the line goes over the top.
|
|
73
|
-
|
|
89
|
+
// ways round that are exactly as long the line goes over the top. A state
|
|
90
|
+
// also records whether the line has passed its gate yet; it may only finish
|
|
91
|
+
// once it has.
|
|
92
|
+
const size = width * height * 8;
|
|
74
93
|
const cost = new Float64Array(size).fill(Infinity);
|
|
75
94
|
const tie = new Float64Array(size).fill(Infinity);
|
|
76
95
|
const from = new Int32Array(size).fill(-1);
|
|
77
|
-
const state = (i, j, h) => (j * width + i) * 4 + h;
|
|
96
|
+
const state = (i, j, h, p) => ((j * width + i) * 4 + h) * 2 + p;
|
|
97
|
+
const done = gate ? 1 : 0;
|
|
78
98
|
const heap = new MinHeap();
|
|
79
|
-
const begin = state(si, sj, first);
|
|
99
|
+
const begin = state(si, sj, first, 0);
|
|
80
100
|
cost[begin] = 0;
|
|
81
101
|
tie[begin] = 0;
|
|
82
102
|
heap.push(begin, 0, 0);
|
|
@@ -85,11 +105,13 @@ export function searchRoute(start, end, walls, options) {
|
|
|
85
105
|
const [current, c, t] = heap.pop();
|
|
86
106
|
if (c > cost[current] || (c === cost[current] && t > tie[current]))
|
|
87
107
|
continue;
|
|
88
|
-
const
|
|
89
|
-
const
|
|
108
|
+
const p = current % 2;
|
|
109
|
+
const rest = (current - p) / 2;
|
|
110
|
+
const h = rest % 4;
|
|
111
|
+
const cell = (rest - h) / 4;
|
|
90
112
|
const i = cell % width;
|
|
91
113
|
const j = (cell - i) / width;
|
|
92
|
-
if (i === ti && j === tj && h === last) {
|
|
114
|
+
if (i === ti && j === tj && h === last && p === done) {
|
|
93
115
|
goal = current;
|
|
94
116
|
break;
|
|
95
117
|
}
|
|
@@ -99,7 +121,7 @@ export function searchRoute(start, end, walls, options) {
|
|
|
99
121
|
continue;
|
|
100
122
|
if (next !== h) {
|
|
101
123
|
// Turning on the spot costs a turn and moves nowhere.
|
|
102
|
-
relax(current, state(i, j, next), c + turn, t);
|
|
124
|
+
relax(current, state(i, j, next, p), c + turn, t);
|
|
103
125
|
continue;
|
|
104
126
|
}
|
|
105
127
|
const ni = i + STEP[next][0];
|
|
@@ -108,16 +130,25 @@ export function searchRoute(start, end, walls, options) {
|
|
|
108
130
|
continue;
|
|
109
131
|
const a = { x: xs[i], y: ys[j] };
|
|
110
132
|
const b = { x: xs[ni], y: ys[nj] };
|
|
111
|
-
|
|
133
|
+
const own = inGate(a, b) ? ownHalf : 0;
|
|
134
|
+
if (blocked(a, b, walls) || alongTaken(a, b, options.taken, spacing, own))
|
|
112
135
|
continue;
|
|
136
|
+
// Passing the gate: a stretch through it, reaching the line at `at`.
|
|
137
|
+
const through = inGate(a, b) &&
|
|
138
|
+
Math.min(gate.run === 'x' ? a.x : a.y, gate.run === 'x' ? b.x : b.y) <= gate.at + 0.01 &&
|
|
139
|
+
Math.max(gate.run === 'x' ? a.x : a.y, gate.run === 'x' ? b.x : b.y) >= gate.at - 0.01;
|
|
140
|
+
const np = p === 1 || through ? 1 : 0;
|
|
113
141
|
// Running closer to a wall than `clear` counts double: the line may pass
|
|
114
|
-
// a narrow gap, but where there is room it keeps its distance.
|
|
115
|
-
|
|
142
|
+
// a narrow gap, but where there is room it keeps its distance. Crossing a
|
|
143
|
+
// line already placed costs what a turn does, so of two ways much the
|
|
144
|
+
// same length the one that crosses nothing wins.
|
|
145
|
+
const length = Math.abs(b.x - a.x) + Math.abs(b.y - a.y) + crowding(a, b, walls, clear) +
|
|
146
|
+
turn * crossings(a, b, options.taken);
|
|
116
147
|
// Higher up for a stretch across, further right for one down: a smaller
|
|
117
148
|
// tie-break is preferred, so the across level counts as it is and the
|
|
118
149
|
// down level counts negated.
|
|
119
150
|
const lean = next % 2 === 0 ? a.y * length : -a.x * length;
|
|
120
|
-
relax(current, state(ni, nj, next), c + length, t + lean);
|
|
151
|
+
relax(current, state(ni, nj, next, np), c + length, t + lean);
|
|
121
152
|
}
|
|
122
153
|
}
|
|
123
154
|
if (goal < 0)
|
|
@@ -132,7 +163,8 @@ export function searchRoute(start, end, walls, options) {
|
|
|
132
163
|
}
|
|
133
164
|
const path = [];
|
|
134
165
|
for (let at = goal; at >= 0; at = from[at]) {
|
|
135
|
-
const
|
|
166
|
+
const rest = (at - (at % 2)) / 2;
|
|
167
|
+
const cell = (rest - (rest % 4)) / 4;
|
|
136
168
|
const i = cell % width;
|
|
137
169
|
const j = (cell - i) / width;
|
|
138
170
|
const point = { x: xs[i], y: ys[j] };
|
|
@@ -146,9 +178,10 @@ export function searchRoute(start, end, walls, options) {
|
|
|
146
178
|
/**
|
|
147
179
|
* The tracks on one axis: beside each wall at `clear` where that is open,
|
|
148
180
|
* down the middle of every gap between walls, through the ends, and a lane
|
|
149
|
-
* either side of each line already placed
|
|
181
|
+
* either side of each line already placed — a wider one where a text rides,
|
|
182
|
+
* and wider again by `own` for this line's own text.
|
|
150
183
|
*/
|
|
151
|
-
function tracks(walls, axis, clear, ends, taken,
|
|
184
|
+
function tracks(walls, axis, clear, ends, taken, spacing, own) {
|
|
152
185
|
const lo = (wall) => (axis === 'x' ? wall.minX : wall.minY);
|
|
153
186
|
const hi = (wall) => (axis === 'x' ? wall.maxX : wall.maxY);
|
|
154
187
|
// The other axis, to tell a real gap — two walls facing each other across
|
|
@@ -166,7 +199,7 @@ function tracks(walls, axis, clear, ends, taken, lane) {
|
|
|
166
199
|
found.add((hi(a) + lo(b)) / 2);
|
|
167
200
|
}
|
|
168
201
|
}
|
|
169
|
-
|
|
202
|
+
taken.forEach((line, which) => {
|
|
170
203
|
for (let index = 0; index + 1 < line.length; index += 1) {
|
|
171
204
|
const [a, b] = [line[index], line[index + 1]];
|
|
172
205
|
// A stretch across sits at a level on y; one down, at a level on x.
|
|
@@ -176,10 +209,12 @@ function tracks(walls, axis, clear, ends, taken, lane) {
|
|
|
176
209
|
// A lane either side of a placed line. One that runs close beside a box
|
|
177
210
|
// is paid for as crowding, like any other track.
|
|
178
211
|
const level = axis === 'y' ? a.y : a.x;
|
|
179
|
-
|
|
180
|
-
|
|
212
|
+
for (const by of new Set([spacing(which, index, 0), spacing(which, index, own)])) {
|
|
213
|
+
found.add(level - by);
|
|
214
|
+
found.add(level + by);
|
|
215
|
+
}
|
|
181
216
|
}
|
|
182
|
-
}
|
|
217
|
+
});
|
|
183
218
|
return [...new Set([...found].map(snap))].sort((p, q) => p - q);
|
|
184
219
|
}
|
|
185
220
|
/** Coordinates to a hundredth, so a track and the end it runs through compare equal. */
|
|
@@ -250,20 +285,23 @@ function crowding(a, b, walls, clear) {
|
|
|
250
285
|
}
|
|
251
286
|
return worst;
|
|
252
287
|
}
|
|
253
|
-
/**
|
|
254
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Whether a stretch runs along a placed line, for any distance, closer than
|
|
290
|
+
* the spacing between the two there: a lane, or clear of a text either carries.
|
|
291
|
+
*/
|
|
292
|
+
function alongTaken(a, b, taken, spacing, own) {
|
|
255
293
|
const across = Math.abs(a.y - b.y) < 0.5;
|
|
256
294
|
const level = across ? a.y : a.x;
|
|
257
295
|
const lo = across ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
|
|
258
296
|
const hi = across ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
259
|
-
for (const line of taken) {
|
|
297
|
+
for (const [which, line] of taken.entries()) {
|
|
260
298
|
for (let index = 0; index + 1 < line.length; index += 1) {
|
|
261
299
|
const [p, q] = [line[index], line[index + 1]];
|
|
262
300
|
const pAcross = Math.abs(p.y - q.y) < 0.5;
|
|
263
301
|
if (pAcross !== across)
|
|
264
302
|
continue;
|
|
265
303
|
const other = across ? p.y : p.x;
|
|
266
|
-
if (Math.abs(other - level) >=
|
|
304
|
+
if (Math.abs(other - level) >= spacing(which, index, own) - 0.5)
|
|
267
305
|
continue;
|
|
268
306
|
const pLo = across ? Math.min(p.x, q.x) : Math.min(p.y, q.y);
|
|
269
307
|
const pHi = across ? Math.max(p.x, q.x) : Math.max(p.y, q.y);
|
|
@@ -273,6 +311,28 @@ function alongTaken(a, b, taken, lane) {
|
|
|
273
311
|
}
|
|
274
312
|
return false;
|
|
275
313
|
}
|
|
314
|
+
/** How many stretches of placed lines a stretch crosses square on, touching neither end of either. */
|
|
315
|
+
function crossings(a, b, taken) {
|
|
316
|
+
const across = Math.abs(a.y - b.y) < 0.5;
|
|
317
|
+
const level = across ? a.y : a.x;
|
|
318
|
+
const lo = across ? Math.min(a.x, b.x) : Math.min(a.y, b.y);
|
|
319
|
+
const hi = across ? Math.max(a.x, b.x) : Math.max(a.y, b.y);
|
|
320
|
+
let found = 0;
|
|
321
|
+
for (const line of taken) {
|
|
322
|
+
for (let index = 0; index + 1 < line.length; index += 1) {
|
|
323
|
+
const [p, q] = [line[index], line[index + 1]];
|
|
324
|
+
const pAcross = Math.abs(p.y - q.y) < 0.5;
|
|
325
|
+
if (pAcross === across)
|
|
326
|
+
continue;
|
|
327
|
+
const at = across ? p.x : p.y;
|
|
328
|
+
const pLo = across ? Math.min(p.y, q.y) : Math.min(p.x, q.x);
|
|
329
|
+
const pHi = across ? Math.max(p.y, q.y) : Math.max(p.x, q.x);
|
|
330
|
+
if (at > lo + 0.5 && at < hi - 0.5 && level > pLo + 0.5 && level < pHi - 0.5)
|
|
331
|
+
found += 1;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return found;
|
|
335
|
+
}
|
|
276
336
|
/** The points with every one dropped that sits on a straight run between its neighbors. */
|
|
277
337
|
function straightRuns(points) {
|
|
278
338
|
const kept = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reladraw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "A diagram language where you say where things go.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"url": "https://github.com/reladraw/reladraw/issues"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
|
-
"build": "tsc",
|
|
35
|
+
"build": "tsc && tsc -p tsconfig.element.json",
|
|
36
36
|
"prepublishOnly": "npm run build",
|
|
37
37
|
"preversion": "./dev.sh check-version \"$npm_new_version\"",
|
|
38
38
|
"dev": "tsc --watch",
|