@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.
@@ -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 highlights every
988
- * match with the same amber the topic focus uses and centres the
989
- * viewport on them, and it SAYS how many it found — a graph that did
990
- * not move looks the same whether nothing matched or the match was
991
- * already on screen.
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
- 'aria-label': t('search'),
1006
- 'data-i18n-aria-label': 'search'
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
- /* Matches in HIDDEN topics, counted apart: "0" alone reads as
3313
- * "does not exist" when it means "is hidden". */
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
- text += ` (+${hidden} ${t("hidden topics")})`;
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],
@@ -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, stagger)
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
- stagger).map(swap);
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 between the two cards, each
279
- * edge of a shared end at its own height round it, moved by the
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 st = stagger || {};
285
- let lo = Math.max(s[1], box_s.y2);
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` SIDEWAYS and reaches `z`
425
- * SIDEWAYS: the column straight under a port and the one straight
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
- if(dir >= 2) {
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, stagger)
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), stagger).map(swap);
368
+ others.map(swap_box)).map(swap);
585
369
  }
586
370
 
587
- /* A comb runs where the layout left room for it: its corridor
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
- }