reladraw 0.21.0 → 0.22.1
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/.claude/skills/reladraw/SKILL.md +209 -0
- package/.claude/skills/reladraw/reference/syntax.md +1608 -0
- package/README.md +7 -5
- package/SYNTAX.md +18 -3
- package/dist/cli.js +44 -3
- package/dist/format.js +67 -20
- package/dist/page.js +69 -145
- package/dist/viewer.d.ts +16 -0
- package/dist/viewer.js +185 -0
- package/package.json +2 -1
- package/dist/constrain.d.ts +0 -56
- package/dist/constrain.js +0 -95
- package/dist/search.d.ts +0 -104
- package/dist/search.js +0 -490
package/dist/viewer.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pan and zoom shared by the page `render --page` writes and the
|
|
3
|
+
* playground, so the two look at a diagram the same way: − and +, Fit, 100%,
|
|
4
|
+
* the zoom always shown and saying "Fit" while fitted, the wheel, a pinch, a
|
|
5
|
+
* drag, a double-click, and the keys + - 0 1.
|
|
6
|
+
*
|
|
7
|
+
* It is browser code kept as a string, since it runs in a page, not here: the
|
|
8
|
+
* page writes it into its script, and the playground's build writes it into
|
|
9
|
+
* its own. Calling `panZoom(view, stage, label, buttons)` returns the controls
|
|
10
|
+
* the page and the playground need beyond it.
|
|
11
|
+
*
|
|
12
|
+
* `view` clips and takes the pointer; `stage` holds the SVG and is moved
|
|
13
|
+
* inside it; `label` shows the zoom; `buttons` holds the elements for out, in,
|
|
14
|
+
* fit and 100%, any of which may be missing.
|
|
15
|
+
*/
|
|
16
|
+
export const PAN_ZOOM = String.raw `
|
|
17
|
+
function panZoom(view, stage, label, buttons) {
|
|
18
|
+
var MIN = 0.05;
|
|
19
|
+
var MAX = 20;
|
|
20
|
+
var svg = null;
|
|
21
|
+
var natural = { w: 1, h: 1 };
|
|
22
|
+
// The drawing is resized rather than scaled with a transform, so the
|
|
23
|
+
// browser draws it afresh at every size and it stays sharp.
|
|
24
|
+
var at = { x: 0, y: 0, s: 1 };
|
|
25
|
+
// Until the reader pans or zooms, a change of size redraws the view the way
|
|
26
|
+
// it was asked for: as it opened, or fitted.
|
|
27
|
+
var fitted = 'open';
|
|
28
|
+
|
|
29
|
+
function apply() {
|
|
30
|
+
if (!svg) {
|
|
31
|
+
label.textContent = '';
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
svg.setAttribute('width', natural.w * at.s);
|
|
35
|
+
svg.setAttribute('height', natural.h * at.s);
|
|
36
|
+
stage.style.transform = 'translate(' + at.x + 'px,' + at.y + 'px)';
|
|
37
|
+
// The zoom always shows. Fitted, it says so too, since 62% alone doesn't
|
|
38
|
+
// say whether that is the whole diagram or a zoom that happens to be 62%.
|
|
39
|
+
// A large diagram opens fitted; a small one opens at 100%, not fitted.
|
|
40
|
+
var isFit = fitted === 'fit' || (fitted === 'open' && at.s < 1);
|
|
41
|
+
label.innerHTML = (isFit ? '<span class="fitted">Fit</span> · ' : '') + Math.round(at.s * 100) + '%';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function size() {
|
|
45
|
+
return { w: view.clientWidth, h: view.clientHeight };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function center(s) {
|
|
49
|
+
var room = size();
|
|
50
|
+
at = { s: s, x: (room.w - natural.w * s) / 2, y: (room.h - natural.h * s) / 2 };
|
|
51
|
+
apply();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fitScale() {
|
|
55
|
+
var room = size();
|
|
56
|
+
return Math.max(MIN, Math.min((room.w - 32) / natural.w, (room.h - 32) / natural.h));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fit() {
|
|
60
|
+
fitted = 'fit';
|
|
61
|
+
center(fitScale());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function actual() {
|
|
65
|
+
fitted = false;
|
|
66
|
+
center(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// A diagram smaller than the view opens at its own size; a larger one
|
|
70
|
+
// opens fitted.
|
|
71
|
+
function open() {
|
|
72
|
+
fitted = 'open';
|
|
73
|
+
center(Math.min(1, fitScale()));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function refit() {
|
|
77
|
+
if (!svg) return;
|
|
78
|
+
if (fitted === 'fit') fit();
|
|
79
|
+
else if (fitted === 'open') open();
|
|
80
|
+
else apply();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Zoom about a point of the view, which stays where it is on screen.
|
|
84
|
+
function zoomAt(factor, px, py) {
|
|
85
|
+
if (!svg) return;
|
|
86
|
+
var s = Math.min(MAX, Math.max(MIN, at.s * factor));
|
|
87
|
+
var k = s / at.s;
|
|
88
|
+
at = { s: s, x: px - (px - at.x) * k, y: py - (py - at.y) * k };
|
|
89
|
+
fitted = false;
|
|
90
|
+
apply();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function zoomMiddle(factor) {
|
|
94
|
+
var room = size();
|
|
95
|
+
zoomAt(factor, room.w / 2, room.h / 2);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function pan(dx, dy) {
|
|
99
|
+
at.x += dx;
|
|
100
|
+
at.y += dy;
|
|
101
|
+
fitted = false;
|
|
102
|
+
apply();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A new drawing in the stage. The view stays as the reader left it: fitted
|
|
106
|
+
// stays fitted to the new drawing, and a zoom keeps its scale and place.
|
|
107
|
+
function show() {
|
|
108
|
+
svg = stage.querySelector('svg');
|
|
109
|
+
if (!svg) return apply();
|
|
110
|
+
var box = svg.viewBox.baseVal;
|
|
111
|
+
natural = box && box.width ? { w: box.width, h: box.height } : { w: parseFloat(svg.getAttribute('width')) || 1, h: parseFloat(svg.getAttribute('height')) || 1 };
|
|
112
|
+
refit();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (buttons.out) buttons.out.onclick = function () { zoomMiddle(0.8); };
|
|
116
|
+
if (buttons.in) buttons.in.onclick = function () { zoomMiddle(1.25); };
|
|
117
|
+
if (buttons.fit) buttons.fit.onclick = fit;
|
|
118
|
+
if (buttons.actual) buttons.actual.onclick = actual;
|
|
119
|
+
|
|
120
|
+
view.addEventListener('wheel', function (event) {
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
var rect = view.getBoundingClientRect();
|
|
123
|
+
// A pinch on a trackpad arrives as a wheel with ctrl held, in small steps.
|
|
124
|
+
var step = event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
|
|
125
|
+
zoomAt(Math.exp(-step * (event.ctrlKey ? 0.01 : 0.0015)), event.clientX - rect.left, event.clientY - rect.top);
|
|
126
|
+
}, { passive: false });
|
|
127
|
+
|
|
128
|
+
// Dragging pans; two fingers pinch.
|
|
129
|
+
var pointers = new Map();
|
|
130
|
+
var pinch = null;
|
|
131
|
+
view.addEventListener('pointerdown', function (event) {
|
|
132
|
+
if (event.button !== 0) return;
|
|
133
|
+
if (event.target.closest('a')) return;
|
|
134
|
+
view.setPointerCapture(event.pointerId);
|
|
135
|
+
pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
136
|
+
if (pointers.size === 2) {
|
|
137
|
+
var two = Array.from(pointers.values());
|
|
138
|
+
pinch = Math.hypot(two[0].x - two[1].x, two[0].y - two[1].y);
|
|
139
|
+
}
|
|
140
|
+
view.classList.add('dragging');
|
|
141
|
+
});
|
|
142
|
+
view.addEventListener('pointermove', function (event) {
|
|
143
|
+
var last = pointers.get(event.pointerId);
|
|
144
|
+
if (!last) return;
|
|
145
|
+
var now = { x: event.clientX, y: event.clientY };
|
|
146
|
+
pointers.set(event.pointerId, now);
|
|
147
|
+
if (pointers.size === 2 && pinch) {
|
|
148
|
+
var two = Array.from(pointers.values());
|
|
149
|
+
var apart = Math.hypot(two[0].x - two[1].x, two[0].y - two[1].y);
|
|
150
|
+
var rect = view.getBoundingClientRect();
|
|
151
|
+
zoomAt(apart / pinch, (two[0].x + two[1].x) / 2 - rect.left, (two[0].y + two[1].y) / 2 - rect.top);
|
|
152
|
+
pinch = apart;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
pan(now.x - last.x, now.y - last.y);
|
|
156
|
+
});
|
|
157
|
+
function release(event) {
|
|
158
|
+
pointers.delete(event.pointerId);
|
|
159
|
+
if (pointers.size < 2) pinch = null;
|
|
160
|
+
if (pointers.size === 0) view.classList.remove('dragging');
|
|
161
|
+
}
|
|
162
|
+
view.addEventListener('pointerup', release);
|
|
163
|
+
view.addEventListener('pointercancel', release);
|
|
164
|
+
view.addEventListener('dblclick', function (event) {
|
|
165
|
+
var rect = view.getBoundingClientRect();
|
|
166
|
+
zoomAt(event.shiftKey ? 0.5 : 2, event.clientX - rect.left, event.clientY - rect.top);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// The view's own size, not the window's: a dragged divider changes it too.
|
|
170
|
+
new ResizeObserver(refit).observe(view);
|
|
171
|
+
|
|
172
|
+
document.addEventListener('keydown', function (event) {
|
|
173
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
174
|
+
if (event.target.closest && event.target.closest('input, textarea, select, [contenteditable]')) return;
|
|
175
|
+
if (event.key === '+' || event.key === '=') zoomMiddle(1.25);
|
|
176
|
+
else if (event.key === '-' || event.key === '_') zoomMiddle(0.8);
|
|
177
|
+
else if (event.key === '0') fit();
|
|
178
|
+
else if (event.key === '1') actual();
|
|
179
|
+
else return;
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return { show: show, open: open, fit: fit, pan: pan };
|
|
184
|
+
}
|
|
185
|
+
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reladraw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "A diagram language where you say where things go.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"main": "./dist/index.js",
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"files": [
|
|
12
|
+
".claude/skills",
|
|
12
13
|
"dist",
|
|
13
14
|
"NOTICE",
|
|
14
15
|
"SYNTAX.md"
|
package/dist/constrain.d.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type { Placement } from './ast.js';
|
|
2
|
-
/**
|
|
3
|
-
* The tightest arrangement satisfying a set of relative distances.
|
|
4
|
-
*
|
|
5
|
-
* Every placement in the language turns into the same shape of fact: one thing sits
|
|
6
|
-
* at least so far along an axis from another. Written as `pos[to] >= pos[from]
|
|
7
|
-
* + weight`, a whole diagram is a system of those, and the arrangement the
|
|
8
|
-
* author meant is the one where nothing is further apart than it has to be.
|
|
9
|
-
*
|
|
10
|
-
* That arrangement is unique and is found by longest paths, so no search
|
|
11
|
-
* happens and no alternative is ever weighed. Distances come out of the
|
|
12
|
-
* arithmetic; which side of what a thing sits on came from the author.
|
|
13
|
-
*/
|
|
14
|
-
export interface Constraint {
|
|
15
|
-
from: number;
|
|
16
|
-
to: number;
|
|
17
|
-
weight: number;
|
|
18
|
-
/** The placement this came from, so a failure can be reported in the author's words. */
|
|
19
|
-
placement?: Placement;
|
|
20
|
-
}
|
|
21
|
-
/** An exact distance, which is two inequalities pointing opposite ways. */
|
|
22
|
-
export declare function fix(from: number, to: number, distance: number, placement?: Placement): Constraint[];
|
|
23
|
-
export interface Contradiction {
|
|
24
|
-
/** The placements that cannot all hold. Empty if the loop is entirely implicit. */
|
|
25
|
-
placements: Placement[];
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Solve, or report the placements that fight.
|
|
29
|
-
*
|
|
30
|
-
* Longest path from a virtual source joined to everything at zero, which is
|
|
31
|
-
* Bellman-Ford with the comparison flipped. A distance that keeps growing after
|
|
32
|
-
* one pass per node means the constraints run in a circle that demands ever
|
|
33
|
-
* more room, and that circle is exactly the set of placements to quote back.
|
|
34
|
-
*/
|
|
35
|
-
export declare function tightest(count: number, constraints: Constraint[]): {
|
|
36
|
-
positions: number[];
|
|
37
|
-
} | {
|
|
38
|
-
contradiction: Contradiction;
|
|
39
|
-
};
|
|
40
|
-
/**
|
|
41
|
-
* Which members can be driven apart from which, along one axis.
|
|
42
|
-
*
|
|
43
|
-
* A constraint says `pos[to] >= pos[from] + weight`, so it bounds `to` from
|
|
44
|
-
* below and leaves it free to move further away. Follow those edges and the
|
|
45
|
-
* question "may this one end up beyond that one?" becomes plain reachability:
|
|
46
|
-
* if `j` is reachable from `i` and `i` is not reachable from `j`, then the file
|
|
47
|
-
* lets the distance from `i` to `j` grow without limit and never lets it be
|
|
48
|
-
* closed from the other side. Pushing `j` past `i` is then the one separation
|
|
49
|
-
* the file allows, and no choice was made.
|
|
50
|
-
*
|
|
51
|
-
* Reachable both ways means the two are pinned at a fixed distance, so they
|
|
52
|
-
* cannot be separated along this axis at all. Reachable neither way means the
|
|
53
|
-
* file said nothing that orders them, which is the case the caller must refuse
|
|
54
|
-
* rather than guess at.
|
|
55
|
-
*/
|
|
56
|
-
export declare function reachability(count: number, constraints: Constraint[]): boolean[][];
|
package/dist/constrain.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/** An exact distance, which is two inequalities pointing opposite ways. */
|
|
2
|
-
export function fix(from, to, distance, placement) {
|
|
3
|
-
return [
|
|
4
|
-
{ from, to, weight: distance, ...(placement ? { placement } : {}) },
|
|
5
|
-
{ from: to, to: from, weight: -distance, ...(placement ? { placement } : {}) },
|
|
6
|
-
];
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Solve, or report the placements that fight.
|
|
10
|
-
*
|
|
11
|
-
* Longest path from a virtual source joined to everything at zero, which is
|
|
12
|
-
* Bellman-Ford with the comparison flipped. A distance that keeps growing after
|
|
13
|
-
* one pass per node means the constraints run in a circle that demands ever
|
|
14
|
-
* more room, and that circle is exactly the set of placements to quote back.
|
|
15
|
-
*/
|
|
16
|
-
export function tightest(count, constraints) {
|
|
17
|
-
const positions = new Array(count).fill(0);
|
|
18
|
-
const cameFrom = new Array(count).fill(undefined);
|
|
19
|
-
for (let pass = 0; pass < count; pass += 1) {
|
|
20
|
-
let moved = false;
|
|
21
|
-
for (const constraint of constraints) {
|
|
22
|
-
const candidate = positions[constraint.from] + constraint.weight;
|
|
23
|
-
if (candidate > positions[constraint.to] + 1e-9) {
|
|
24
|
-
positions[constraint.to] = candidate;
|
|
25
|
-
cameFrom[constraint.to] = constraint;
|
|
26
|
-
moved = true;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (!moved)
|
|
30
|
-
return { positions };
|
|
31
|
-
}
|
|
32
|
-
// Still moving after a full pass per node, so some loop demands more room
|
|
33
|
-
// every time round it. Walk backwards to find it.
|
|
34
|
-
for (const constraint of constraints) {
|
|
35
|
-
if (positions[constraint.from] + constraint.weight > positions[constraint.to] + 1e-9) {
|
|
36
|
-
return { contradiction: { placements: loopFrom(constraint.to, cameFrom, count) } };
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return { contradiction: { placements: [] } };
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Which members can be driven apart from which, along one axis.
|
|
43
|
-
*
|
|
44
|
-
* A constraint says `pos[to] >= pos[from] + weight`, so it bounds `to` from
|
|
45
|
-
* below and leaves it free to move further away. Follow those edges and the
|
|
46
|
-
* question "may this one end up beyond that one?" becomes plain reachability:
|
|
47
|
-
* if `j` is reachable from `i` and `i` is not reachable from `j`, then the file
|
|
48
|
-
* lets the distance from `i` to `j` grow without limit and never lets it be
|
|
49
|
-
* closed from the other side. Pushing `j` past `i` is then the one separation
|
|
50
|
-
* the file allows, and no choice was made.
|
|
51
|
-
*
|
|
52
|
-
* Reachable both ways means the two are pinned at a fixed distance, so they
|
|
53
|
-
* cannot be separated along this axis at all. Reachable neither way means the
|
|
54
|
-
* file said nothing that orders them, which is the case the caller must refuse
|
|
55
|
-
* rather than guess at.
|
|
56
|
-
*/
|
|
57
|
-
export function reachability(count, constraints) {
|
|
58
|
-
const reach = Array.from({ length: count }, () => new Array(count).fill(false));
|
|
59
|
-
for (const constraint of constraints)
|
|
60
|
-
reach[constraint.from][constraint.to] = true;
|
|
61
|
-
for (let via = 0; via < count; via += 1) {
|
|
62
|
-
for (let from = 0; from < count; from += 1) {
|
|
63
|
-
if (!reach[from][via])
|
|
64
|
-
continue;
|
|
65
|
-
for (let to = 0; to < count; to += 1) {
|
|
66
|
-
if (reach[via][to])
|
|
67
|
-
reach[from][to] = true;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return reach;
|
|
72
|
-
}
|
|
73
|
-
/** The placements on the loop reached by following each position back to what set it. */
|
|
74
|
-
function loopFrom(start, cameFrom, count) {
|
|
75
|
-
let at = start;
|
|
76
|
-
for (let step = 0; step < count; step += 1) {
|
|
77
|
-
const previous = cameFrom[at];
|
|
78
|
-
if (previous === undefined)
|
|
79
|
-
break;
|
|
80
|
-
at = previous.from;
|
|
81
|
-
}
|
|
82
|
-
const placements = [];
|
|
83
|
-
const seen = new Set();
|
|
84
|
-
let cursor = at;
|
|
85
|
-
while (!seen.has(cursor)) {
|
|
86
|
-
seen.add(cursor);
|
|
87
|
-
const previous = cameFrom[cursor];
|
|
88
|
-
if (previous === undefined)
|
|
89
|
-
break;
|
|
90
|
-
if (previous.placement)
|
|
91
|
-
placements.push(previous.placement);
|
|
92
|
-
cursor = previous.from;
|
|
93
|
-
}
|
|
94
|
-
return placements.reverse();
|
|
95
|
-
}
|
package/dist/search.d.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Finding a line's way through the open space between boxes.
|
|
3
|
-
*
|
|
4
|
-
* Every box is a wall. A line may turn only where two tracks cross, and the
|
|
5
|
-
* tracks are few: a line a little way out from each side of each wall, one
|
|
6
|
-
* down the middle of each gap between walls, and the lines through the two
|
|
7
|
-
* ends. That is the whole of the open space a right-angled line ever needs,
|
|
8
|
-
* reduced to a grid of a few hundred points for a diagram of ordinary size.
|
|
9
|
-
*
|
|
10
|
-
* The line leaves its start heading straight out and arrives at its end heading
|
|
11
|
-
* straight in, with a straight stretch at each end at least `stub` long, so an
|
|
12
|
-
* arrowhead always lands on a straight piece. Between those two stretches it
|
|
13
|
-
* takes the cheapest way through the grid, where cost is length plus a charge
|
|
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
|
-
*
|
|
20
|
-
* A way through a box is not a losing option here. It is not a way at all.
|
|
21
|
-
*/
|
|
22
|
-
export interface SearchPoint {
|
|
23
|
-
x: number;
|
|
24
|
-
y: number;
|
|
25
|
-
}
|
|
26
|
-
/** A box the line may not enter, as its extent. Touching its border is allowed. */
|
|
27
|
-
export interface SearchWall {
|
|
28
|
-
minX: number;
|
|
29
|
-
minY: number;
|
|
30
|
-
maxX: number;
|
|
31
|
-
maxY: number;
|
|
32
|
-
}
|
|
33
|
-
/** One end of the line: a point on a box's side, and the unit direction out of it. */
|
|
34
|
-
export interface SearchEnd {
|
|
35
|
-
x: number;
|
|
36
|
-
y: number;
|
|
37
|
-
dx: number;
|
|
38
|
-
dy: number;
|
|
39
|
-
}
|
|
40
|
-
export interface SearchOptions {
|
|
41
|
-
/** How far the line runs straight out of each end before it may turn. */
|
|
42
|
-
stub: number;
|
|
43
|
-
/** How far outside a wall a track runs beside it, where there is room. */
|
|
44
|
-
clear: number;
|
|
45
|
-
/** What a turn costs, in the same units as length. */
|
|
46
|
-
turn: number;
|
|
47
|
-
/**
|
|
48
|
-
* Walls the stretches at the two ends may pass through: the line's own boxes,
|
|
49
|
-
* which it has to cross the margin of to leave, and any box holding an end.
|
|
50
|
-
*/
|
|
51
|
-
ownStart: SearchWall[];
|
|
52
|
-
ownEnd: SearchWall[];
|
|
53
|
-
/**
|
|
54
|
-
* Lines already placed. A line may cross one but not run along it within
|
|
55
|
-
* `lane` of it; tracks are added a lane either side of each, so a later line
|
|
56
|
-
* sharing a stretch takes the next lane out.
|
|
57
|
-
*/
|
|
58
|
-
taken: SearchPoint[][];
|
|
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 cost the line is no use at unless it comes in under: the cheapest way
|
|
84
|
-
* already found between another pair of sides. The search gives up as soon
|
|
85
|
-
* as it cannot.
|
|
86
|
-
*/
|
|
87
|
-
limit?: number;
|
|
88
|
-
}
|
|
89
|
-
/** A gap a line must pass through. See `SearchOptions.gate`. */
|
|
90
|
-
export interface SearchGate {
|
|
91
|
-
run: 'x' | 'y';
|
|
92
|
-
at: number;
|
|
93
|
-
lo: number;
|
|
94
|
-
hi: number;
|
|
95
|
-
}
|
|
96
|
-
export interface SearchResult {
|
|
97
|
-
points: SearchPoint[];
|
|
98
|
-
cost: number;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* The cheapest right-angled line from `start` to `end` that enters no wall, or
|
|
102
|
-
* undefined when there is none.
|
|
103
|
-
*/
|
|
104
|
-
export declare function searchRoute(start: SearchEnd, end: SearchEnd, walls: SearchWall[], options: SearchOptions): SearchResult | undefined;
|