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/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. On a right-angled grid many ways tie on length — a staircase
15
- * is exactly as long as one bend — and the turn charge breaks those ties toward
16
- * the fewest bends, which is the line a person would draw. What still ties
17
- * after that goes over the top rather than under, and round the right rather
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. On a right-angled grid many ways tie on length — a staircase
15
- * is exactly as long as one bend — and the turn charge breaks those ties toward
16
- * the fewest bends, which is the line a person would draw. What still ties
17
- * after that goes over the top rather than under, and round the right rather
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 xs = tracks(walls, 'x', clear, [out.x, back.x], options.taken, options.lane);
49
- const ys = tracks(walls, 'y', clear, [out.y, back.y], options.taken, options.lane);
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
- const size = width * height * 4;
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 h = current % 4;
89
- const cell = (current - h) / 4;
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
- if (blocked(a, b, walls) || alongTaken(a, b, options.taken, options.lane))
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
- const length = Math.abs(b.x - a.x) + Math.abs(b.y - a.y) + crowding(a, b, walls, clear);
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 cell = (at - (at % 4)) / 4;
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, lane) {
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
- for (const line of taken) {
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
- found.add(level - lane);
180
- found.add(level + lane);
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
- /** Whether a stretch runs along a placed line, closer than a lane, for any distance. */
254
- function alongTaken(a, b, taken, lane) {
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) >= lane - 0.5)
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.11.1",
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",