@yuneta/gobj-ui 7.23.156 → 7.23.158
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 +38 -71
- package/dist/gobj-ui.cjs.js +299 -959
- package/dist/gobj-ui.es.js +300 -960
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +227 -241
- package/src/c_yui_treedb_graph.js +91 -13
- package/src/treedb_elbow.js +13 -344
- package/src/treedb_elbow.test.js +1 -236
- package/src/treedb_layout.js +39 -620
- package/src/treedb_layout.test.js +3 -269
|
@@ -80,7 +80,7 @@ import {
|
|
|
80
80
|
} from "@yuneta/gobj-js";
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
import {attach_clear} from "./yui_inputs.js";
|
|
83
|
+
import {attach_clear, refresh_clear} from "./yui_inputs.js";
|
|
84
84
|
import {register_c_g6_nodes_tree} from "./c_g6_nodes_tree.js";
|
|
85
85
|
import {yui_toolbar, yui_toolbar_icon, YUI_TOOLBAR_ICON_SIZE} from "./yui_toolbar.js";
|
|
86
86
|
import {
|
|
@@ -555,8 +555,6 @@ function option_label(name)
|
|
|
555
555
|
return t("manual");
|
|
556
556
|
case "treedb-tree":
|
|
557
557
|
return t("treedb-tree");
|
|
558
|
-
case "compact-tree":
|
|
559
|
-
return t("compact-tree");
|
|
560
558
|
case "radial":
|
|
561
559
|
return t("radial");
|
|
562
560
|
case "dagre":
|
|
@@ -984,11 +982,13 @@ function make_toolbar(gobj)
|
|
|
984
982
|
* Right: find a node.
|
|
985
983
|
*
|
|
986
984
|
* A graph of a few hundred records has no other way in: the only way
|
|
987
|
-
* to locate one was to read every card. The box
|
|
988
|
-
*
|
|
989
|
-
*
|
|
990
|
-
*
|
|
991
|
-
*
|
|
985
|
+
* to locate one was to read every card. The box lights every match
|
|
986
|
+
* ON SCREEN with the same amber the topic focus uses and changes
|
|
987
|
+
* nothing else (no unfold, no layout, no camera); Enter centres the
|
|
988
|
+
* next lit card, Shift+Enter the previous. It SAYS how many it found,
|
|
989
|
+
* and how many more are folded away or in hidden topics — a graph
|
|
990
|
+
* with no amber looks the same whether nothing matched or the matches
|
|
991
|
+
* are not on screen.
|
|
992
992
|
*/
|
|
993
993
|
/* Materialised, not a spec: attach_clear() hangs the NORM clear (✕)
|
|
994
994
|
* on a real element. Clearing dispatches a synthetic `input`, which
|
|
@@ -1002,8 +1002,12 @@ function make_toolbar(gobj)
|
|
|
1002
1002
|
* cannot reach it: it needs its own key. */
|
|
1003
1003
|
placeholder: t('search'),
|
|
1004
1004
|
'data-i18n-placeholder': 'search',
|
|
1005
|
-
|
|
1006
|
-
|
|
1005
|
+
/* The title is where Enter is told: nothing else on the
|
|
1006
|
+
* box says it can step through the matches. */
|
|
1007
|
+
title: t('find on screen'),
|
|
1008
|
+
'data-i18n-title': 'find on screen',
|
|
1009
|
+
'aria-label': t('find on screen'),
|
|
1010
|
+
'data-i18n-aria-label': 'find on screen'
|
|
1007
1011
|
}, [], {
|
|
1008
1012
|
/* Rate-limited, not delayed for effect: a match repaints the
|
|
1009
1013
|
* cards it lands on, and on a large treedb the first letter
|
|
@@ -1020,6 +1024,24 @@ function make_toolbar(gobj)
|
|
|
1020
1024
|
priv.find_timer = null;
|
|
1021
1025
|
gobj_send_event(gobj, "EV_FIND_NODES", {text: text}, gobj);
|
|
1022
1026
|
}, 250);
|
|
1027
|
+
},
|
|
1028
|
+
/* Enter steps through the lit cards. A term still waiting
|
|
1029
|
+
* on the rate limit is sent first, or Enter would step
|
|
1030
|
+
* through the matches of the term before it. */
|
|
1031
|
+
keydown: (evt) => {
|
|
1032
|
+
if(evt.key !== 'Enter' || evt.isComposing) {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
evt.preventDefault();
|
|
1036
|
+
evt.stopPropagation();
|
|
1037
|
+
if(priv.find_timer) {
|
|
1038
|
+
clearTimeout(priv.find_timer);
|
|
1039
|
+
priv.find_timer = null;
|
|
1040
|
+
gobj_send_event(
|
|
1041
|
+
gobj, "EV_FIND_NODES", {text: evt.target.value.trim()}, gobj
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
gobj_send_event(gobj, "EV_FIND_NEXT", {back: evt.shiftKey}, gobj);
|
|
1023
1045
|
}
|
|
1024
1046
|
}]);
|
|
1025
1047
|
|
|
@@ -3190,6 +3212,23 @@ function ac_find_nodes(gobj, event, kw, src)
|
|
|
3190
3212
|
return 0;
|
|
3191
3213
|
}
|
|
3192
3214
|
|
|
3215
|
+
/************************************************************
|
|
3216
|
+
* Enter in the find box: forward the step down to the graph child.
|
|
3217
|
+
************************************************************/
|
|
3218
|
+
function ac_find_next(gobj, event, kw, src)
|
|
3219
|
+
{
|
|
3220
|
+
let priv = gobj.priv;
|
|
3221
|
+
if(priv.gobj_nodes_tree) {
|
|
3222
|
+
gobj_send_event(
|
|
3223
|
+
priv.gobj_nodes_tree,
|
|
3224
|
+
"EV_FIND_NEXT",
|
|
3225
|
+
{back: !!(kw && kw.back)},
|
|
3226
|
+
gobj
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
3229
|
+
return 0;
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3193
3232
|
/************************************************************
|
|
3194
3233
|
* One press of the fold stepper (EV_EXPAND_LEVEL /
|
|
3195
3234
|
* EV_COLLAPSE_LEVEL): the level moves by one, is SAVED (the
|
|
@@ -3309,17 +3348,47 @@ function refresh_find_result(gobj)
|
|
|
3309
3348
|
}
|
|
3310
3349
|
|
|
3311
3350
|
let text = t("matches", {count: (kw && kw.matches) || 0});
|
|
3312
|
-
/*
|
|
3313
|
-
|
|
3351
|
+
/* Where Enter stands among the lit cards: "3/12 matches". */
|
|
3352
|
+
let current = (kw && kw.current) || 0;
|
|
3353
|
+
if(current > 0) {
|
|
3354
|
+
text = `${current}/${text}`;
|
|
3355
|
+
}
|
|
3356
|
+
/* Matches NOT lit, counted apart: "0" alone reads as "does not
|
|
3357
|
+
* exist" when it means "is folded away" or "is hidden". */
|
|
3358
|
+
let extra = [];
|
|
3359
|
+
let folded = (kw && kw.folded_matches) || 0;
|
|
3360
|
+
if(folded > 0) {
|
|
3361
|
+
extra.push(`+${folded} ${t("not shown")}`);
|
|
3362
|
+
}
|
|
3314
3363
|
let hidden = (kw && kw.hidden_matches) || 0;
|
|
3315
3364
|
if(hidden > 0) {
|
|
3316
|
-
|
|
3365
|
+
extra.push(`+${hidden} ${t("hidden topics")}`);
|
|
3366
|
+
}
|
|
3367
|
+
if(extra.length) {
|
|
3368
|
+
text += ` (${extra.join(", ")})`;
|
|
3317
3369
|
}
|
|
3318
3370
|
$count.textContent = text;
|
|
3319
3371
|
$result.classList.remove("is-hidden");
|
|
3320
3372
|
return 0;
|
|
3321
3373
|
}
|
|
3322
3374
|
|
|
3375
|
+
function clear_find_box(gobj)
|
|
3376
|
+
{
|
|
3377
|
+
let priv = gobj.priv;
|
|
3378
|
+
if(priv.find_timer) {
|
|
3379
|
+
clearTimeout(priv.find_timer);
|
|
3380
|
+
priv.find_timer = null;
|
|
3381
|
+
}
|
|
3382
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
3383
|
+
let $input = $container? $container.querySelector(".GRAPH_FIND_INPUT") : null;
|
|
3384
|
+
if($input) {
|
|
3385
|
+
$input.value = "";
|
|
3386
|
+
refresh_clear($input);
|
|
3387
|
+
}
|
|
3388
|
+
priv._find_result = null;
|
|
3389
|
+
refresh_find_result(gobj);
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3323
3392
|
function ac_set_focus_topic(gobj, event, kw, src)
|
|
3324
3393
|
{
|
|
3325
3394
|
let priv = gobj.priv;
|
|
@@ -3336,6 +3405,13 @@ function ac_set_focus_topic(gobj, event, kw, src)
|
|
|
3336
3405
|
}
|
|
3337
3406
|
}
|
|
3338
3407
|
|
|
3408
|
+
/* A topic focus takes the highlight from the find (the engine
|
|
3409
|
+
* drops its term), so the box must not go on showing a term
|
|
3410
|
+
* nothing is lit for. */
|
|
3411
|
+
if(priv.focus_topic) {
|
|
3412
|
+
clear_find_box(gobj);
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3339
3415
|
if(priv.gobj_nodes_tree) {
|
|
3340
3416
|
gobj_send_event(
|
|
3341
3417
|
priv.gobj_nodes_tree,
|
|
@@ -3437,6 +3513,7 @@ function create_gclass(gclass_name)
|
|
|
3437
3513
|
["EV_SET_OPERATION_MODE", ac_set_operation_mode, null],
|
|
3438
3514
|
["EV_SET_FOCUS_TOPIC", ac_set_focus_topic, null],
|
|
3439
3515
|
["EV_FIND_NODES", ac_find_nodes, null],
|
|
3516
|
+
["EV_FIND_NEXT", ac_find_next, null],
|
|
3440
3517
|
["EV_EXPAND_LEVEL", ac_fold_level_step, null],
|
|
3441
3518
|
["EV_COLLAPSE_LEVEL", ac_fold_level_step, null],
|
|
3442
3519
|
["EV_SET_NODE_MODE", ac_set_node_mode, null],
|
|
@@ -3484,6 +3561,7 @@ function create_gclass(gclass_name)
|
|
|
3484
3561
|
["EV_SET_OPERATION_MODE", 0],
|
|
3485
3562
|
["EV_SET_FOCUS_TOPIC", 0],
|
|
3486
3563
|
["EV_FIND_NODES", 0],
|
|
3564
|
+
["EV_FIND_NEXT", 0],
|
|
3487
3565
|
["EV_EXPAND_LEVEL", 0],
|
|
3488
3566
|
["EV_COLLAPSE_LEVEL", 0],
|
|
3489
3567
|
["EV_SET_NODE_MODE", 0],
|
package/src/treedb_elbow.js
CHANGED
|
@@ -25,25 +25,6 @@
|
|
|
25
25
|
* the order the edges were made, so a new edge never moves the
|
|
26
26
|
* ones already drawn.
|
|
27
27
|
*
|
|
28
|
-
* - Several forward edges sharing one END: the children of one
|
|
29
|
-
* card, or the parents of one card (all its fkeys enter by one
|
|
30
|
-
* point). They are STAGGERED: each turns at its own height in
|
|
31
|
-
* the channel, so their runs lie side by side instead of on top
|
|
32
|
-
* of each other. Leaving a card, the one that runs farthest
|
|
33
|
-
* turns highest; reaching a card, the one that comes from
|
|
34
|
-
* farthest turns lowest -- the two orders in which the lines of
|
|
35
|
-
* one end nest without crossing.
|
|
36
|
-
*
|
|
37
|
-
* - A card in a lower row of a STACK (treedb_layout.js lays a
|
|
38
|
-
* run of leaf children out as two columns with a corridor
|
|
39
|
-
* between them): the card above it stands in the way of the
|
|
40
|
-
* channel, so its edge runs down the corridor in a lane of
|
|
41
|
-
* its own -- a COMB -- and turns into the gap above its card.
|
|
42
|
-
*
|
|
43
|
-
* And for CURVED edges (curve_hits, rounded_path): a curve
|
|
44
|
-
* that would cross another card follows the same way an elbow
|
|
45
|
-
* would, with round corners, so it reads as a curve.
|
|
46
|
-
*
|
|
47
28
|
* Written for the top-down reading; left to right is the same
|
|
48
29
|
* geometry with the axes swapped.
|
|
49
30
|
*
|
|
@@ -51,12 +32,9 @@
|
|
|
51
32
|
* All Rights Reserved.
|
|
52
33
|
***********************************************************************/
|
|
53
34
|
|
|
54
|
-
import {STACK_GAP, STACK_LANE_PAD, STACK_LANE_STEP, STACK_TRUNK_MIN} from "./treedb_layout.js";
|
|
55
|
-
|
|
56
35
|
export const ELBOW_STEP = 10; /* between two lanes */
|
|
57
36
|
export const ELBOW_JETTY = 16; /* straight run out of a port before a detour turns */
|
|
58
37
|
export const ELBOW_CLEAR = 24; /* how far outside the two cards a detour runs */
|
|
59
|
-
export const ELBOW_STAGGER_PAD = 6; /* staggered turns keep this far from the rows */
|
|
60
38
|
|
|
61
39
|
/************************************************************
|
|
62
40
|
* The lane of `id` among `ids`, the edges joining the same two
|
|
@@ -76,24 +54,6 @@ export function elbow_lane(ids, id)
|
|
|
76
54
|
return side * Math.ceil(k / 2);
|
|
77
55
|
}
|
|
78
56
|
|
|
79
|
-
/************************************************************
|
|
80
|
-
* The rank of `id` among `items` ([{id, span}]): the forward
|
|
81
|
-
* edges that share one end with it, each with how far it runs
|
|
82
|
-
* ACROSS. Farthest first; a tie goes to the edge made first.
|
|
83
|
-
* Returns {rank, count}.
|
|
84
|
-
************************************************************/
|
|
85
|
-
export function elbow_rank(items, id)
|
|
86
|
-
{
|
|
87
|
-
let sorted = items.slice().sort((a, b) => {
|
|
88
|
-
if(b.span !== a.span) {
|
|
89
|
-
return b.span - a.span;
|
|
90
|
-
}
|
|
91
|
-
return String(a.id).localeCompare(String(b.id), undefined, {numeric: true});
|
|
92
|
-
});
|
|
93
|
-
let rank = sorted.findIndex((it) => it.id === id);
|
|
94
|
-
return {rank: (rank < 0)? 0 : rank, count: sorted.length};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
57
|
function swap(p)
|
|
98
58
|
{
|
|
99
59
|
return [p[1], p[0]];
|
|
@@ -104,154 +64,6 @@ function swap_box(b)
|
|
|
104
64
|
return {x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2};
|
|
105
65
|
}
|
|
106
66
|
|
|
107
|
-
/* Where a forward edge turns between `lo` and `hi`: the middle, or
|
|
108
|
-
* its staggered place among the edges sharing one of its ends --
|
|
109
|
-
* leaving, the farthest highest; arriving, the farthest lowest. */
|
|
110
|
-
function staggered_turn(lo, hi, st)
|
|
111
|
-
{
|
|
112
|
-
let mid = (lo + hi) / 2;
|
|
113
|
-
let group = null;
|
|
114
|
-
let k = 0;
|
|
115
|
-
if(st.dep && st.dep.count > 1) {
|
|
116
|
-
group = st.dep;
|
|
117
|
-
k = st.dep.rank;
|
|
118
|
-
} else if(st.arr && st.arr.count > 1) {
|
|
119
|
-
group = st.arr;
|
|
120
|
-
k = st.arr.count - 1 - st.arr.rank;
|
|
121
|
-
}
|
|
122
|
-
if(group) {
|
|
123
|
-
let room = Math.max(0, hi - lo - 2 * ELBOW_STAGGER_PAD);
|
|
124
|
-
let step = Math.min(ELBOW_STEP, room / (group.count - 1));
|
|
125
|
-
mid = mid - (group.count - 1) * step / 2 + k * step;
|
|
126
|
-
}
|
|
127
|
-
return mid;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/************************************************************
|
|
131
|
-
* The combs of the stacks under one card.
|
|
132
|
-
*
|
|
133
|
-
* treedb_layout.js lays a run of leaf children out as two columns
|
|
134
|
-
* with a corridor between them. A card in the first row is reached
|
|
135
|
-
* from the channel like any other; one in a lower row cannot be --
|
|
136
|
-
* the card above it stands in the way -- so its edge runs down
|
|
137
|
-
* the corridor in a LANE of its own and turns into the gap above
|
|
138
|
-
* its card, which it shares only with the other card of its row,
|
|
139
|
-
* reached from the other side.
|
|
140
|
-
*
|
|
141
|
-
* targets [{id, box}]: the forward edges leaving one card,
|
|
142
|
-
* each with the box of the card it reaches
|
|
143
|
-
* vertical as in elbow_points; false swaps the boxes
|
|
144
|
-
*
|
|
145
|
-
* Returns Map id -> {lane, z, top}: the lane, the height of the
|
|
146
|
-
* gap it turns into, and the top of the stack's first row (where
|
|
147
|
-
* the channel over the stack ends). Only for a card that stands
|
|
148
|
-
* under another of the group, within a stack gap and lined up
|
|
149
|
-
* with it on the corridor side -- what a stack looks like.
|
|
150
|
-
*
|
|
151
|
-
* Lanes go outermost first, row by row: a line into a higher row
|
|
152
|
-
* keeps outside the lines going deeper, and none crosses another.
|
|
153
|
-
************************************************************/
|
|
154
|
-
export function elbow_combs(targets, vertical)
|
|
155
|
-
{
|
|
156
|
-
let list = vertical? targets : targets.map((it) => ({id: it.id, box: swap_box(it.box)}));
|
|
157
|
-
let cy = (b) => (b.y1 + b.y2) / 2;
|
|
158
|
-
let near = (a, b) => Math.abs(a - b) < 1.5;
|
|
159
|
-
|
|
160
|
-
let under = [];
|
|
161
|
-
for(let it of list) {
|
|
162
|
-
let b = it.box;
|
|
163
|
-
let above = null;
|
|
164
|
-
for(let o of list) {
|
|
165
|
-
let a = o.box;
|
|
166
|
-
if(o === it || a.y2 > b.y1 || b.y1 - a.y2 > STACK_GAP) {
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if(!(near(a.x2, b.x2) || near(a.x1, b.x1))) {
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
if(!above || a.y2 > above.y2) {
|
|
173
|
-
above = a;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if(!above) {
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
/* Its column: lined up on the corridor side, which is the
|
|
180
|
-
* right edge of the first column and the left edge of the
|
|
181
|
-
* second. Two cards of one width line up on both; then the
|
|
182
|
-
* card beside it in its row says. */
|
|
183
|
-
let side;
|
|
184
|
-
if(near(above.x2, b.x2) && !near(above.x1, b.x1)) {
|
|
185
|
-
side = 0;
|
|
186
|
-
} else if(near(above.x1, b.x1) && !near(above.x2, b.x2)) {
|
|
187
|
-
side = 1;
|
|
188
|
-
} else {
|
|
189
|
-
let left_of_it = list.some((o) => o !== it && near(cy(o.box), cy(b)) &&
|
|
190
|
-
o.box.x2 <= b.x1);
|
|
191
|
-
side = left_of_it? 1 : 0;
|
|
192
|
-
}
|
|
193
|
-
under.push({id: it.id, box: b, above: above, side: side});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/* The corridor of a card: from the first column's edge to the
|
|
197
|
-
* second's, read off any row of the stack that has both. */
|
|
198
|
-
let corridor = (u) => {
|
|
199
|
-
if(u.side === 0) {
|
|
200
|
-
let l = u.box.x2;
|
|
201
|
-
let col = list.filter((o) => near(o.box.x2, l));
|
|
202
|
-
let r = Infinity;
|
|
203
|
-
for(let c of col) {
|
|
204
|
-
for(let o of list) {
|
|
205
|
-
if(o !== c && near(cy(o.box), cy(c.box)) && o.box.x1 >= l - 0.5) {
|
|
206
|
-
r = Math.min(r, o.box.x1);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return {l: l, r: isFinite(r)? r : l + STACK_TRUNK_MIN, col: col};
|
|
211
|
-
}
|
|
212
|
-
let r = u.box.x1;
|
|
213
|
-
let col = list.filter((o) => near(o.box.x1, r));
|
|
214
|
-
let l = -Infinity;
|
|
215
|
-
for(let c of col) {
|
|
216
|
-
for(let o of list) {
|
|
217
|
-
if(o !== c && near(cy(o.box), cy(c.box)) && o.box.x2 <= r + 0.5) {
|
|
218
|
-
l = Math.max(l, o.box.x2);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return {l: isFinite(l)? l : r - STACK_TRUNK_MIN, r: r, col: col};
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
let groups = new Map();
|
|
226
|
-
for(let u of under) {
|
|
227
|
-
let c = corridor(u);
|
|
228
|
-
u.top = Math.min(...c.col.map((o) => o.box.y1));
|
|
229
|
-
let key = `${Math.round(c.l)}|${Math.round(c.r)}`;
|
|
230
|
-
if(!groups.has(key)) {
|
|
231
|
-
groups.set(key, {l: c.l, r: c.r, left: [], right: []});
|
|
232
|
-
}
|
|
233
|
-
groups.get(key)[(u.side === 0)? 'left' : 'right'].push(u);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
let out = new Map();
|
|
237
|
-
for(let g of groups.values()) {
|
|
238
|
-
g.left.sort((a, b) => a.box.y1 - b.box.y1);
|
|
239
|
-
g.right.sort((a, b) => a.box.y1 - b.box.y1);
|
|
240
|
-
let n = g.left.length + g.right.length;
|
|
241
|
-
let room = g.r - g.l - 2 * STACK_LANE_PAD;
|
|
242
|
-
let step = (n > 1)? Math.max(0, Math.min(STACK_LANE_STEP, room / (n - 1))) : 0;
|
|
243
|
-
g.left.forEach((u, i) => {
|
|
244
|
-
out.set(u.id, {lane: g.l + STACK_LANE_PAD + i * step,
|
|
245
|
-
z: (u.above.y2 + u.box.y1) / 2, top: u.top});
|
|
246
|
-
});
|
|
247
|
-
g.right.forEach((u, j) => {
|
|
248
|
-
out.set(u.id, {lane: g.r - STACK_LANE_PAD - j * step,
|
|
249
|
-
z: (u.above.y2 + u.box.y1) / 2, top: u.top});
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
return out;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
67
|
/************************************************************
|
|
256
68
|
* The control points of an elbow from the port `s` to the port
|
|
257
69
|
* `t` ([x, y] each), knowing the boxes of the two cards
|
|
@@ -259,39 +71,21 @@ export function elbow_combs(targets, vertical)
|
|
|
259
71
|
*
|
|
260
72
|
* vertical the rows are stacked (top to bottom); false
|
|
261
73
|
* when they are side by side (left to right)
|
|
262
|
-
* stagger {dep, arr, comb}: the edge's rank among the
|
|
263
|
-
* forward edges leaving its source card and among
|
|
264
|
-
* those reaching its target card (elbow_rank), and
|
|
265
|
-
* its comb when the target is in a lower row of a
|
|
266
|
-
* stack (elbow_combs). Absent, the channel is the
|
|
267
|
-
* middle.
|
|
268
74
|
************************************************************/
|
|
269
|
-
export function elbow_points(s, t, box_s, box_t, lane, vertical
|
|
75
|
+
export function elbow_points(s, t, box_s, box_t, lane, vertical)
|
|
270
76
|
{
|
|
271
77
|
if(!vertical) {
|
|
272
|
-
return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true
|
|
273
|
-
|
|
78
|
+
return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true)
|
|
79
|
+
.map(swap);
|
|
274
80
|
}
|
|
275
81
|
|
|
276
82
|
let shift = lane * ELBOW_STEP;
|
|
277
83
|
|
|
278
|
-
/* Forward: the channel half way
|
|
279
|
-
*
|
|
280
|
-
* lane, and kept between the two rows. Into a lower row of a
|
|
281
|
-
* stack: along the channel to its corridor lane, down the lane,
|
|
282
|
-
* and into the gap above its card. */
|
|
84
|
+
/* Forward: the channel half way down, moved by the lane and
|
|
85
|
+
* kept strictly between the two ports. */
|
|
283
86
|
if(t[1] - s[1] > 2 * ELBOW_STEP) {
|
|
284
|
-
let
|
|
285
|
-
|
|
286
|
-
if(st.comb) {
|
|
287
|
-
let c = st.comb;
|
|
288
|
-
let turn = staggered_turn(lo, c.top, {dep: st.dep}) + shift;
|
|
289
|
-
turn = Math.min(Math.max(turn, lo + 4), c.top - 4);
|
|
290
|
-
return [[s[0], turn], [c.lane, turn], [c.lane, c.z], [t[0], c.z]];
|
|
291
|
-
}
|
|
292
|
-
let hi = Math.min(t[1], box_t.y1);
|
|
293
|
-
let mid = staggered_turn(lo, hi, st) + shift;
|
|
294
|
-
mid = Math.min(Math.max(mid, lo + 4), hi - 4);
|
|
87
|
+
let mid = (s[1] + t[1]) / 2 + shift;
|
|
88
|
+
mid = Math.min(Math.max(mid, s[1] + ELBOW_STEP), t[1] - ELBOW_STEP);
|
|
295
89
|
return [[s[0], mid], [t[0], mid]];
|
|
296
90
|
}
|
|
297
91
|
|
|
@@ -421,12 +215,8 @@ function heap_pop(h)
|
|
|
421
215
|
/************************************************************
|
|
422
216
|
* The shortest orthogonal line from `a` to `z` that enters none
|
|
423
217
|
* of `boxes` (already expanded by the clearance), a turn costing
|
|
424
|
-
* ROUTE_BEND. The line leaves `a`
|
|
425
|
-
*
|
|
426
|
-
* over it belong to the forward edges that share that port, which
|
|
427
|
-
* run along them to their staggered turns -- a route going down
|
|
428
|
-
* that column lay on top of them. So a routed line shares only
|
|
429
|
-
* the stub between the port and `a` (or `z`).
|
|
218
|
+
* ROUTE_BEND. The line leaves `a` going down and ends at `z`
|
|
219
|
+
* going down, since after `z` it drops into a port.
|
|
430
220
|
*
|
|
431
221
|
* The grid is SPARSE: only the lines of the boxes' borders and
|
|
432
222
|
* of the two ends, which is all an orthogonal shortest path ever
|
|
@@ -512,10 +302,7 @@ function orth_search(a, z, boxes)
|
|
|
512
302
|
let k = state >> 2;
|
|
513
303
|
let dir = state & 3;
|
|
514
304
|
if(k === goal) {
|
|
515
|
-
|
|
516
|
-
continue; /* down the port's own column: not allowed */
|
|
517
|
-
}
|
|
518
|
-
let total = cost + ROUTE_BEND; /* the turn into the port */
|
|
305
|
+
let total = cost + ((dir === 2)? 0 : ROUTE_BEND);
|
|
519
306
|
if(total < found_cost) {
|
|
520
307
|
found_cost = total;
|
|
521
308
|
found = state;
|
|
@@ -523,9 +310,6 @@ function orth_search(a, z, boxes)
|
|
|
523
310
|
continue;
|
|
524
311
|
}
|
|
525
312
|
for(let [nk, nd, len] of moves(k)) {
|
|
526
|
-
if(k === start && nd >= 2) {
|
|
527
|
-
continue; /* out of the port sideways, not down its column */
|
|
528
|
-
}
|
|
529
313
|
let c = cost + len + ((nd === dir)? 0 : ROUTE_BEND);
|
|
530
314
|
let ns = nk * 4 + nd;
|
|
531
315
|
if(c < best[ns]) {
|
|
@@ -576,21 +360,15 @@ function orth_search(a, z, boxes)
|
|
|
576
360
|
* coincide. Nearby cards first; all of them if that finds no way;
|
|
577
361
|
* the simple elbow if nothing does.
|
|
578
362
|
************************************************************/
|
|
579
|
-
export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes
|
|
363
|
+
export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes)
|
|
580
364
|
{
|
|
581
365
|
let others = boxes || [];
|
|
582
366
|
if(!vertical) {
|
|
583
367
|
return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true,
|
|
584
|
-
others.map(swap_box)
|
|
368
|
+
others.map(swap_box)).map(swap);
|
|
585
369
|
}
|
|
586
370
|
|
|
587
|
-
|
|
588
|
-
* and its gap are empty by construction. */
|
|
589
|
-
if(stagger && stagger.comb) {
|
|
590
|
-
return elbow_points(s, t, box_s, box_t, lane, true, stagger);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
let simple = elbow_points(s, t, box_s, box_t, lane, true, stagger);
|
|
371
|
+
let simple = elbow_points(s, t, box_s, box_t, lane, true);
|
|
594
372
|
if(!elbow_path_hits([s, ...simple, t], others, box_s, box_t)) {
|
|
595
373
|
return simple;
|
|
596
374
|
}
|
|
@@ -613,112 +391,3 @@ export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes, stagger)
|
|
|
613
391
|
}
|
|
614
392
|
return route || simple;
|
|
615
393
|
}
|
|
616
|
-
|
|
617
|
-
/*----------------------------------------------------------------*
|
|
618
|
-
* CURVED edges that would cross a card
|
|
619
|
-
*----------------------------------------------------------------*/
|
|
620
|
-
export const CURVE_RADIUS = 24; /* the corners of a curve that goes round */
|
|
621
|
-
export const CURVE_STEPS = 16; /* samples of each curved stretch */
|
|
622
|
-
|
|
623
|
-
/* Points along a G6 path: the ends of its runs and CURVE_STEPS
|
|
624
|
-
* samples of each quadratic or cubic stretch. */
|
|
625
|
-
function path_points(path)
|
|
626
|
-
{
|
|
627
|
-
let pts = [];
|
|
628
|
-
let cur = null;
|
|
629
|
-
for(let c of path) {
|
|
630
|
-
if(c[0] === 'M' || c[0] === 'L') {
|
|
631
|
-
cur = [c[1], c[2]];
|
|
632
|
-
pts.push(cur);
|
|
633
|
-
} else if(c[0] === 'C' && cur) {
|
|
634
|
-
let [x0, y0] = cur;
|
|
635
|
-
for(let k = 1; k <= CURVE_STEPS; k++) {
|
|
636
|
-
let u = k / CURVE_STEPS;
|
|
637
|
-
let v = 1 - u;
|
|
638
|
-
pts.push([
|
|
639
|
-
v * v * v * x0 + 3 * v * v * u * c[1] + 3 * v * u * u * c[3] + u * u * u * c[5],
|
|
640
|
-
v * v * v * y0 + 3 * v * v * u * c[2] + 3 * v * u * u * c[4] + u * u * u * c[6],
|
|
641
|
-
]);
|
|
642
|
-
}
|
|
643
|
-
cur = [c[5], c[6]];
|
|
644
|
-
} else if(c[0] === 'Q' && cur) {
|
|
645
|
-
let [x0, y0] = cur;
|
|
646
|
-
for(let k = 1; k <= CURVE_STEPS; k++) {
|
|
647
|
-
let u = k / CURVE_STEPS;
|
|
648
|
-
let v = 1 - u;
|
|
649
|
-
pts.push([
|
|
650
|
-
v * v * x0 + 2 * v * u * c[1] + u * u * c[3],
|
|
651
|
-
v * v * y0 + 2 * v * u * c[2] + u * u * c[4],
|
|
652
|
-
]);
|
|
653
|
-
}
|
|
654
|
-
cur = [c[3], c[4]];
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return pts;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/************************************************************
|
|
661
|
-
* Does the G6 path `path` of a curved edge cross a card other than
|
|
662
|
-
* its own two? Measured on the curve itself -- the path G6 would
|
|
663
|
-
* draw, sampled -- and not on the rectangle round it, which took
|
|
664
|
-
* every card the curve merely passed near: the ports of a row
|
|
665
|
-
* stick well into the space above it. Its own two cards are left
|
|
666
|
-
* out: the curve starts and ends on their ports.
|
|
667
|
-
************************************************************/
|
|
668
|
-
export function curve_hits(path, boxes, box_s, box_t)
|
|
669
|
-
{
|
|
670
|
-
let pts = path_points(path);
|
|
671
|
-
if(pts.length < 2) {
|
|
672
|
-
return false;
|
|
673
|
-
}
|
|
674
|
-
let bb = {x1: Infinity, y1: Infinity, x2: -Infinity, y2: -Infinity};
|
|
675
|
-
for(let p of pts) {
|
|
676
|
-
bb.x1 = Math.min(bb.x1, p[0]);
|
|
677
|
-
bb.y1 = Math.min(bb.y1, p[1]);
|
|
678
|
-
bb.x2 = Math.max(bb.x2, p[0]);
|
|
679
|
-
bb.y2 = Math.max(bb.y2, p[1]);
|
|
680
|
-
}
|
|
681
|
-
for(let b of (boxes || [])) {
|
|
682
|
-
if(same_box(b, box_s) || same_box(b, box_t) || !box_overlaps(b, box_expand(bb, 1))) {
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
let inner = box_expand(b, -1);
|
|
686
|
-
for(let k = 0; k + 1 < pts.length; k++) {
|
|
687
|
-
if(segment_hits(pts[k], pts[k + 1], inner)) {
|
|
688
|
-
return true;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
return false;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/************************************************************
|
|
696
|
-
* A G6 path (['M'|'L'|'Q', ...]) along `points` with every corner
|
|
697
|
-
* rounded -- by `radius`, or by half the shorter of its two
|
|
698
|
-
* segments, so a short run is never overshot. The rounding is the
|
|
699
|
-
* quadratic G6's own polyline uses.
|
|
700
|
-
************************************************************/
|
|
701
|
-
export function rounded_path(points, radius)
|
|
702
|
-
{
|
|
703
|
-
let path = [['M', points[0][0], points[0][1]]];
|
|
704
|
-
for(let i = 1; i + 1 < points.length; i++) {
|
|
705
|
-
let a = points[i - 1];
|
|
706
|
-
let m = points[i];
|
|
707
|
-
let b = points[i + 1];
|
|
708
|
-
let len_in = Math.hypot(m[0] - a[0], m[1] - a[1]);
|
|
709
|
-
let len_out = Math.hypot(b[0] - m[0], b[1] - m[1]);
|
|
710
|
-
let r = Math.min(radius, len_in / 2, len_out / 2);
|
|
711
|
-
let collinear = Math.abs((m[0] - a[0]) * (b[1] - m[1]) - (m[1] - a[1]) * (b[0] - m[0])) < 1e-9;
|
|
712
|
-
if(r <= 0 || collinear) {
|
|
713
|
-
path.push(['L', m[0], m[1]]);
|
|
714
|
-
continue;
|
|
715
|
-
}
|
|
716
|
-
let p1 = [m[0] - (m[0] - a[0]) / len_in * r, m[1] - (m[1] - a[1]) / len_in * r];
|
|
717
|
-
let p2 = [m[0] + (b[0] - m[0]) / len_out * r, m[1] + (b[1] - m[1]) / len_out * r];
|
|
718
|
-
path.push(['L', p1[0], p1[1]]);
|
|
719
|
-
path.push(['Q', m[0], m[1], p2[0], p2[1]]);
|
|
720
|
-
}
|
|
721
|
-
let last = points[points.length - 1];
|
|
722
|
-
path.push(['L', last[0], last[1]]);
|
|
723
|
-
return path;
|
|
724
|
-
}
|