@yuneta/gobj-ui 7.23.149 → 7.23.150
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 +11 -0
- package/dist/gobj-ui.cjs.js +85 -5
- package/dist/gobj-ui.es.js +85 -5
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +43 -5
- package/src/treedb_elbow.js +102 -0
- package/src/treedb_elbow.test.js +105 -0
package/README.md
CHANGED
|
@@ -1438,6 +1438,17 @@ direction, and a layout with no rows (`rowless_layouts`: `radial`,
|
|
|
1438
1438
|
`edge_shape` is `SDF_PERSIST` on `C_YUI_TREEDB_GRAPH`, a preference per
|
|
1439
1439
|
treedb like `node_labels`. Consumer key: `elbow edges`.
|
|
1440
1440
|
|
|
1441
|
+
Two cases are not a channel (`7.23.150`, `treedb_elbow.js`, pure and
|
|
1442
|
+
tested). An elbow that does not run **forward** — the target above its
|
|
1443
|
+
source or in the same row, which one edge of every reciprocal pair is —
|
|
1444
|
+
goes **round** the two cards instead of through them: a short run out of its
|
|
1445
|
+
port, along the outside of both, and in from the other port's own side. And
|
|
1446
|
+
the edges joining the **same two cards**, either way round, take **lanes**
|
|
1447
|
+
10px apart in the order they were made (0, +1, −1…), so a new one never
|
|
1448
|
+
moves the ones already drawn; a detour on a negative lane goes round the
|
|
1449
|
+
left. The detour clears the two cards it joins, not the others: a card of a
|
|
1450
|
+
third record standing in its way is crossed, as a curve would cross it.
|
|
1451
|
+
|
|
1441
1452
|
All three take the **same spanning tree**, chosen deterministically: roots are the
|
|
1442
1453
|
nodes with no incoming edge, in node order; a node belongs to the **first
|
|
1443
1454
|
parent that reaches it** in a breadth-first walk (the place, not the
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -29630,6 +29630,54 @@ function layout_radial(nodes, edges, opts) {
|
|
|
29630
29630
|
}
|
|
29631
29631
|
return pos;
|
|
29632
29632
|
}
|
|
29633
|
+
/************************************************************
|
|
29634
|
+
* The lane of `id` among `ids`, the edges joining the same two
|
|
29635
|
+
* cards: 0, then +1, -1, +2, -2... in the order they were made.
|
|
29636
|
+
* Edge ids end in a counter, so a numeric sort is that order.
|
|
29637
|
+
************************************************************/
|
|
29638
|
+
function elbow_lane(ids, id) {
|
|
29639
|
+
let k = ids.slice().sort((a, b) => String(a).localeCompare(String(b), void 0, { numeric: true })).indexOf(id);
|
|
29640
|
+
if (k <= 0) return 0;
|
|
29641
|
+
return (k % 2 ? 1 : -1) * Math.ceil(k / 2);
|
|
29642
|
+
}
|
|
29643
|
+
function swap(p) {
|
|
29644
|
+
return [p[1], p[0]];
|
|
29645
|
+
}
|
|
29646
|
+
function swap_box(b) {
|
|
29647
|
+
return {
|
|
29648
|
+
x1: b.y1,
|
|
29649
|
+
y1: b.x1,
|
|
29650
|
+
x2: b.y2,
|
|
29651
|
+
y2: b.x2
|
|
29652
|
+
};
|
|
29653
|
+
}
|
|
29654
|
+
/************************************************************
|
|
29655
|
+
* The control points of an elbow from the port `s` to the port
|
|
29656
|
+
* `t` ([x, y] each), knowing the boxes of the two cards
|
|
29657
|
+
* ({x1, y1, x2, y2}) and the edge's lane.
|
|
29658
|
+
*
|
|
29659
|
+
* vertical the rows are stacked (top to bottom); false
|
|
29660
|
+
* when they are side by side (left to right)
|
|
29661
|
+
************************************************************/
|
|
29662
|
+
function elbow_points(s, t, box_s, box_t, lane, vertical) {
|
|
29663
|
+
if (!vertical) return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true).map(swap);
|
|
29664
|
+
let shift = lane * 10;
|
|
29665
|
+
if (t[1] - s[1] > 20) {
|
|
29666
|
+
let mid = (s[1] + t[1]) / 2 + shift;
|
|
29667
|
+
mid = Math.min(Math.max(mid, s[1] + 10), t[1] - 10);
|
|
29668
|
+
return [[s[0], mid], [t[0], mid]];
|
|
29669
|
+
}
|
|
29670
|
+
let out = 16 + Math.abs(shift);
|
|
29671
|
+
let side = lane < 0 ? Math.min(box_s.x1, box_t.x1) - 24 - Math.abs(shift) : Math.max(box_s.x2, box_t.x2) + 24 + Math.abs(shift);
|
|
29672
|
+
let y1 = s[1] + out;
|
|
29673
|
+
let y2 = t[1] - out;
|
|
29674
|
+
return [
|
|
29675
|
+
[s[0], y1],
|
|
29676
|
+
[side, y1],
|
|
29677
|
+
[side, y2],
|
|
29678
|
+
[t[0], y2]
|
|
29679
|
+
];
|
|
29680
|
+
}
|
|
29633
29681
|
//#endregion
|
|
29634
29682
|
//#region node_modules/@babel/runtime/helpers/esm/typeof.js
|
|
29635
29683
|
function _typeof(o) {
|
|
@@ -59009,14 +59057,47 @@ var TreedbTreeLayout = class extends _antv_g6.BaseLayout {
|
|
|
59009
59057
|
*
|
|
59010
59058
|
* Computed on every draw from where the two ends ARE, so a
|
|
59011
59059
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
59012
|
-
* between rows (top to bottom), `-h` between columns.
|
|
59060
|
+
* between rows (top to bottom), `-h` between columns. An edge that
|
|
59061
|
+
* does not run forward goes round the two cards, and the edges
|
|
59062
|
+
* joining the same two cards take lanes (treedb_elbow.js).
|
|
59013
59063
|
************************************************************/
|
|
59014
59064
|
var ELBOW_RADIUS = 6;
|
|
59065
|
+
function elbow_box(node) {
|
|
59066
|
+
try {
|
|
59067
|
+
let b = node.getShape("key").getBounds();
|
|
59068
|
+
return {
|
|
59069
|
+
x1: b.min[0],
|
|
59070
|
+
y1: b.min[1],
|
|
59071
|
+
x2: b.max[0],
|
|
59072
|
+
y2: b.max[1]
|
|
59073
|
+
};
|
|
59074
|
+
} catch (e) {
|
|
59075
|
+
(0, _yuneta_gobj_js.log_error)(`elbow edge: no bounds for node ${node && node.id}: ${e}`);
|
|
59076
|
+
let c = node.getCenter();
|
|
59077
|
+
return {
|
|
59078
|
+
x1: c[0],
|
|
59079
|
+
y1: c[1],
|
|
59080
|
+
x2: c[0],
|
|
59081
|
+
y2: c[1]
|
|
59082
|
+
};
|
|
59083
|
+
}
|
|
59084
|
+
}
|
|
59085
|
+
function elbow_lane_of(edge) {
|
|
59086
|
+
let graph = edge.context && edge.context.graph;
|
|
59087
|
+
let s = edge.sourceNode && edge.sourceNode.id;
|
|
59088
|
+
let t = edge.targetNode && edge.targetNode.id;
|
|
59089
|
+
if (!graph || s === void 0 || t === void 0 || s === t) return 0;
|
|
59090
|
+
try {
|
|
59091
|
+
return elbow_lane(graph.getRelatedEdgesData(s).filter((d) => d.source === s && d.target === t || d.source === t && d.target === s).map((d) => d.id), edge.id);
|
|
59092
|
+
} catch (e) {
|
|
59093
|
+
(0, _yuneta_gobj_js.log_error)(`elbow edge ${edge.id}: cannot read the edges of ${s}: ${e}`);
|
|
59094
|
+
return 0;
|
|
59095
|
+
}
|
|
59096
|
+
}
|
|
59015
59097
|
var TreedbElbowV = class extends _antv_g6.Polyline {
|
|
59016
59098
|
getControlPoints(attributes) {
|
|
59017
59099
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59018
|
-
|
|
59019
|
-
return [[s[0], mid], [t[0], mid]];
|
|
59100
|
+
return elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true);
|
|
59020
59101
|
}
|
|
59021
59102
|
getKeyPath(attributes) {
|
|
59022
59103
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
|
@@ -59025,8 +59106,7 @@ var TreedbElbowV = class extends _antv_g6.Polyline {
|
|
|
59025
59106
|
var TreedbElbowH = class extends _antv_g6.Polyline {
|
|
59026
59107
|
getControlPoints(attributes) {
|
|
59027
59108
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59028
|
-
|
|
59029
|
-
return [[mid, s[1]], [mid, t[1]]];
|
|
59109
|
+
return elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false);
|
|
59030
59110
|
}
|
|
59031
59111
|
getKeyPath(attributes) {
|
|
59032
59112
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -29625,6 +29625,54 @@ function layout_radial(nodes, edges, opts) {
|
|
|
29625
29625
|
}
|
|
29626
29626
|
return pos;
|
|
29627
29627
|
}
|
|
29628
|
+
/************************************************************
|
|
29629
|
+
* The lane of `id` among `ids`, the edges joining the same two
|
|
29630
|
+
* cards: 0, then +1, -1, +2, -2... in the order they were made.
|
|
29631
|
+
* Edge ids end in a counter, so a numeric sort is that order.
|
|
29632
|
+
************************************************************/
|
|
29633
|
+
function elbow_lane(ids, id) {
|
|
29634
|
+
let k = ids.slice().sort((a, b) => String(a).localeCompare(String(b), void 0, { numeric: true })).indexOf(id);
|
|
29635
|
+
if (k <= 0) return 0;
|
|
29636
|
+
return (k % 2 ? 1 : -1) * Math.ceil(k / 2);
|
|
29637
|
+
}
|
|
29638
|
+
function swap(p) {
|
|
29639
|
+
return [p[1], p[0]];
|
|
29640
|
+
}
|
|
29641
|
+
function swap_box(b) {
|
|
29642
|
+
return {
|
|
29643
|
+
x1: b.y1,
|
|
29644
|
+
y1: b.x1,
|
|
29645
|
+
x2: b.y2,
|
|
29646
|
+
y2: b.x2
|
|
29647
|
+
};
|
|
29648
|
+
}
|
|
29649
|
+
/************************************************************
|
|
29650
|
+
* The control points of an elbow from the port `s` to the port
|
|
29651
|
+
* `t` ([x, y] each), knowing the boxes of the two cards
|
|
29652
|
+
* ({x1, y1, x2, y2}) and the edge's lane.
|
|
29653
|
+
*
|
|
29654
|
+
* vertical the rows are stacked (top to bottom); false
|
|
29655
|
+
* when they are side by side (left to right)
|
|
29656
|
+
************************************************************/
|
|
29657
|
+
function elbow_points(s, t, box_s, box_t, lane, vertical) {
|
|
29658
|
+
if (!vertical) return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true).map(swap);
|
|
29659
|
+
let shift = lane * 10;
|
|
29660
|
+
if (t[1] - s[1] > 20) {
|
|
29661
|
+
let mid = (s[1] + t[1]) / 2 + shift;
|
|
29662
|
+
mid = Math.min(Math.max(mid, s[1] + 10), t[1] - 10);
|
|
29663
|
+
return [[s[0], mid], [t[0], mid]];
|
|
29664
|
+
}
|
|
29665
|
+
let out = 16 + Math.abs(shift);
|
|
29666
|
+
let side = lane < 0 ? Math.min(box_s.x1, box_t.x1) - 24 - Math.abs(shift) : Math.max(box_s.x2, box_t.x2) + 24 + Math.abs(shift);
|
|
29667
|
+
let y1 = s[1] + out;
|
|
29668
|
+
let y2 = t[1] - out;
|
|
29669
|
+
return [
|
|
29670
|
+
[s[0], y1],
|
|
29671
|
+
[side, y1],
|
|
29672
|
+
[side, y2],
|
|
29673
|
+
[t[0], y2]
|
|
29674
|
+
];
|
|
29675
|
+
}
|
|
29628
29676
|
//#endregion
|
|
29629
29677
|
//#region node_modules/@babel/runtime/helpers/esm/typeof.js
|
|
29630
29678
|
function _typeof(o) {
|
|
@@ -59004,14 +59052,47 @@ var TreedbTreeLayout = class extends BaseLayout {
|
|
|
59004
59052
|
*
|
|
59005
59053
|
* Computed on every draw from where the two ends ARE, so a
|
|
59006
59054
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
59007
|
-
* between rows (top to bottom), `-h` between columns.
|
|
59055
|
+
* between rows (top to bottom), `-h` between columns. An edge that
|
|
59056
|
+
* does not run forward goes round the two cards, and the edges
|
|
59057
|
+
* joining the same two cards take lanes (treedb_elbow.js).
|
|
59008
59058
|
************************************************************/
|
|
59009
59059
|
var ELBOW_RADIUS = 6;
|
|
59060
|
+
function elbow_box(node) {
|
|
59061
|
+
try {
|
|
59062
|
+
let b = node.getShape("key").getBounds();
|
|
59063
|
+
return {
|
|
59064
|
+
x1: b.min[0],
|
|
59065
|
+
y1: b.min[1],
|
|
59066
|
+
x2: b.max[0],
|
|
59067
|
+
y2: b.max[1]
|
|
59068
|
+
};
|
|
59069
|
+
} catch (e) {
|
|
59070
|
+
log_error(`elbow edge: no bounds for node ${node && node.id}: ${e}`);
|
|
59071
|
+
let c = node.getCenter();
|
|
59072
|
+
return {
|
|
59073
|
+
x1: c[0],
|
|
59074
|
+
y1: c[1],
|
|
59075
|
+
x2: c[0],
|
|
59076
|
+
y2: c[1]
|
|
59077
|
+
};
|
|
59078
|
+
}
|
|
59079
|
+
}
|
|
59080
|
+
function elbow_lane_of(edge) {
|
|
59081
|
+
let graph = edge.context && edge.context.graph;
|
|
59082
|
+
let s = edge.sourceNode && edge.sourceNode.id;
|
|
59083
|
+
let t = edge.targetNode && edge.targetNode.id;
|
|
59084
|
+
if (!graph || s === void 0 || t === void 0 || s === t) return 0;
|
|
59085
|
+
try {
|
|
59086
|
+
return elbow_lane(graph.getRelatedEdgesData(s).filter((d) => d.source === s && d.target === t || d.source === t && d.target === s).map((d) => d.id), edge.id);
|
|
59087
|
+
} catch (e) {
|
|
59088
|
+
log_error(`elbow edge ${edge.id}: cannot read the edges of ${s}: ${e}`);
|
|
59089
|
+
return 0;
|
|
59090
|
+
}
|
|
59091
|
+
}
|
|
59010
59092
|
var TreedbElbowV = class extends Polyline {
|
|
59011
59093
|
getControlPoints(attributes) {
|
|
59012
59094
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59013
|
-
|
|
59014
|
-
return [[s[0], mid], [t[0], mid]];
|
|
59095
|
+
return elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true);
|
|
59015
59096
|
}
|
|
59016
59097
|
getKeyPath(attributes) {
|
|
59017
59098
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
|
@@ -59020,8 +59101,7 @@ var TreedbElbowV = class extends Polyline {
|
|
|
59020
59101
|
var TreedbElbowH = class extends Polyline {
|
|
59021
59102
|
getControlPoints(attributes) {
|
|
59022
59103
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59023
|
-
|
|
59024
|
-
return [[mid, s[1]], [mid, t[1]]];
|
|
59104
|
+
return elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false);
|
|
59025
59105
|
}
|
|
59026
59106
|
getKeyPath(attributes) {
|
|
59027
59107
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
package/package.json
CHANGED
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -133,6 +133,7 @@ import {
|
|
|
133
133
|
fold_root_group_key,
|
|
134
134
|
} from "./treedb_fold_model.js";
|
|
135
135
|
import {layout_tree, layout_radial, layout_compact} from "./treedb_layout.js";
|
|
136
|
+
import {elbow_lane, elbow_points} from "./treedb_elbow.js";
|
|
136
137
|
|
|
137
138
|
import {
|
|
138
139
|
BaseLayout,
|
|
@@ -4221,16 +4222,53 @@ class TreedbTreeLayout extends BaseLayout
|
|
|
4221
4222
|
*
|
|
4222
4223
|
* Computed on every draw from where the two ends ARE, so a
|
|
4223
4224
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
4224
|
-
* between rows (top to bottom), `-h` between columns.
|
|
4225
|
+
* between rows (top to bottom), `-h` between columns. An edge that
|
|
4226
|
+
* does not run forward goes round the two cards, and the edges
|
|
4227
|
+
* joining the same two cards take lanes (treedb_elbow.js).
|
|
4225
4228
|
************************************************************/
|
|
4226
4229
|
const ELBOW_RADIUS = 6;
|
|
4227
4230
|
|
|
4231
|
+
/* The box of a card on the canvas, for a detour to go round it. */
|
|
4232
|
+
function elbow_box(node)
|
|
4233
|
+
{
|
|
4234
|
+
try {
|
|
4235
|
+
let b = node.getShape('key').getBounds();
|
|
4236
|
+
return {x1: b.min[0], y1: b.min[1], x2: b.max[0], y2: b.max[1]};
|
|
4237
|
+
} catch(e) {
|
|
4238
|
+
log_error(`elbow edge: no bounds for node ${node && node.id}: ${e}`);
|
|
4239
|
+
let c = node.getCenter();
|
|
4240
|
+
return {x1: c[0], y1: c[1], x2: c[0], y2: c[1]};
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
|
|
4244
|
+
/* The lane of an edge among the edges joining the same two cards,
|
|
4245
|
+
* either way round (treedb_elbow.js). */
|
|
4246
|
+
function elbow_lane_of(edge)
|
|
4247
|
+
{
|
|
4248
|
+
let graph = edge.context && edge.context.graph;
|
|
4249
|
+
let s = edge.sourceNode && edge.sourceNode.id;
|
|
4250
|
+
let t = edge.targetNode && edge.targetNode.id;
|
|
4251
|
+
if(!graph || s === undefined || t === undefined || s === t) {
|
|
4252
|
+
return 0;
|
|
4253
|
+
}
|
|
4254
|
+
try {
|
|
4255
|
+
let ids = graph.getRelatedEdgesData(s)
|
|
4256
|
+
.filter((d) => (d.source === s && d.target === t) ||
|
|
4257
|
+
(d.source === t && d.target === s))
|
|
4258
|
+
.map((d) => d.id);
|
|
4259
|
+
return elbow_lane(ids, edge.id);
|
|
4260
|
+
} catch(e) {
|
|
4261
|
+
log_error(`elbow edge ${edge.id}: cannot read the edges of ${s}: ${e}`);
|
|
4262
|
+
return 0;
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
|
|
4228
4266
|
class TreedbElbowV extends Polyline
|
|
4229
4267
|
{
|
|
4230
4268
|
getControlPoints(attributes) {
|
|
4231
4269
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
4232
|
-
|
|
4233
|
-
|
|
4270
|
+
return elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
|
|
4271
|
+
elbow_lane_of(this), true);
|
|
4234
4272
|
}
|
|
4235
4273
|
|
|
4236
4274
|
getKeyPath(attributes) {
|
|
@@ -4243,8 +4281,8 @@ class TreedbElbowH extends Polyline
|
|
|
4243
4281
|
{
|
|
4244
4282
|
getControlPoints(attributes) {
|
|
4245
4283
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
4246
|
-
|
|
4247
|
-
|
|
4284
|
+
return elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
|
|
4285
|
+
elbow_lane_of(this), false);
|
|
4248
4286
|
}
|
|
4249
4287
|
|
|
4250
4288
|
getKeyPath(attributes) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* treedb_elbow.js
|
|
3
|
+
*
|
|
4
|
+
* Where an elbow edge of the treedb graph turns, as plain
|
|
5
|
+
* geometry: the two elbow edge types of c_g6_nodes_tree.js ask
|
|
6
|
+
* this module for their control points on every draw, and it
|
|
7
|
+
* can be tested without G6.
|
|
8
|
+
*
|
|
9
|
+
* An elbow is mxGraph's tree routing: straight out of the
|
|
10
|
+
* parent's port to the channel half way to the child, along it,
|
|
11
|
+
* and straight into the child's port. Two cases are not that:
|
|
12
|
+
*
|
|
13
|
+
* - An edge that does not run FORWARD: the child above its
|
|
14
|
+
* parent, or in the same row. One of the two edges of a
|
|
15
|
+
* reciprocal pair (A -> B and B -> A) is always one. A channel
|
|
16
|
+
* half way between would cross both cards and lie on top of
|
|
17
|
+
* its partner, so it goes ROUND: a short run out of its port,
|
|
18
|
+
* along the outside of both cards, and into the other port
|
|
19
|
+
* from that port's own side.
|
|
20
|
+
*
|
|
21
|
+
* - Several edges joining the SAME two cards: a reciprocal pair,
|
|
22
|
+
* or two hooks of one parent reaching one child. Each gets a
|
|
23
|
+
* LANE: the first stays where it is, the next moves one step to
|
|
24
|
+
* one side, the next one step to the other. The lane follows
|
|
25
|
+
* the order the edges were made, so a new edge never moves the
|
|
26
|
+
* ones already drawn.
|
|
27
|
+
*
|
|
28
|
+
* Written for the top-down reading; left to right is the same
|
|
29
|
+
* geometry with the axes swapped.
|
|
30
|
+
*
|
|
31
|
+
* Copyright (c) 2026, ArtGins.
|
|
32
|
+
* All Rights Reserved.
|
|
33
|
+
***********************************************************************/
|
|
34
|
+
|
|
35
|
+
export const ELBOW_STEP = 10; /* between two lanes */
|
|
36
|
+
export const ELBOW_JETTY = 16; /* straight run out of a port before a detour turns */
|
|
37
|
+
export const ELBOW_CLEAR = 24; /* how far outside the two cards a detour runs */
|
|
38
|
+
|
|
39
|
+
/************************************************************
|
|
40
|
+
* The lane of `id` among `ids`, the edges joining the same two
|
|
41
|
+
* cards: 0, then +1, -1, +2, -2... in the order they were made.
|
|
42
|
+
* Edge ids end in a counter, so a numeric sort is that order.
|
|
43
|
+
************************************************************/
|
|
44
|
+
export function elbow_lane(ids, id)
|
|
45
|
+
{
|
|
46
|
+
let sorted = ids.slice().sort(
|
|
47
|
+
(a, b) => String(a).localeCompare(String(b), undefined, {numeric: true})
|
|
48
|
+
);
|
|
49
|
+
let k = sorted.indexOf(id);
|
|
50
|
+
if(k <= 0) {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
let side = (k % 2)? 1 : -1;
|
|
54
|
+
return side * Math.ceil(k / 2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function swap(p)
|
|
58
|
+
{
|
|
59
|
+
return [p[1], p[0]];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function swap_box(b)
|
|
63
|
+
{
|
|
64
|
+
return {x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/************************************************************
|
|
68
|
+
* The control points of an elbow from the port `s` to the port
|
|
69
|
+
* `t` ([x, y] each), knowing the boxes of the two cards
|
|
70
|
+
* ({x1, y1, x2, y2}) and the edge's lane.
|
|
71
|
+
*
|
|
72
|
+
* vertical the rows are stacked (top to bottom); false
|
|
73
|
+
* when they are side by side (left to right)
|
|
74
|
+
************************************************************/
|
|
75
|
+
export function elbow_points(s, t, box_s, box_t, lane, vertical)
|
|
76
|
+
{
|
|
77
|
+
if(!vertical) {
|
|
78
|
+
return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true)
|
|
79
|
+
.map(swap);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let shift = lane * ELBOW_STEP;
|
|
83
|
+
|
|
84
|
+
/* Forward: the channel half way down, moved by the lane and
|
|
85
|
+
* kept strictly between the two ports. */
|
|
86
|
+
if(t[1] - s[1] > 2 * ELBOW_STEP) {
|
|
87
|
+
let mid = (s[1] + t[1]) / 2 + shift;
|
|
88
|
+
mid = Math.min(Math.max(mid, s[1] + ELBOW_STEP), t[1] - ELBOW_STEP);
|
|
89
|
+
return [[s[0], mid], [t[0], mid]];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Backwards, or across one row: round the outside. A negative
|
|
93
|
+
* lane goes round the left, any other the right, so the two
|
|
94
|
+
* detours of one pair never share a side. */
|
|
95
|
+
let out = ELBOW_JETTY + Math.abs(shift);
|
|
96
|
+
let side = (lane < 0)
|
|
97
|
+
? Math.min(box_s.x1, box_t.x1) - ELBOW_CLEAR - Math.abs(shift)
|
|
98
|
+
: Math.max(box_s.x2, box_t.x2) + ELBOW_CLEAR + Math.abs(shift);
|
|
99
|
+
let y1 = s[1] + out; /* a hook port is on the bottom edge */
|
|
100
|
+
let y2 = t[1] - out; /* an fkey port is on the top edge */
|
|
101
|
+
return [[s[0], y1], [side, y1], [side, y2], [t[0], y2]];
|
|
102
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* treedb_elbow.test.js
|
|
3
|
+
*
|
|
4
|
+
* The elbow edges of the treedb graph, pinned: the lane of an
|
|
5
|
+
* edge among those joining the same two cards, the channel of a
|
|
6
|
+
* forward edge, the detour of one that is not.
|
|
7
|
+
***********************************************************************/
|
|
8
|
+
import { describe, test, expect } from "vitest";
|
|
9
|
+
import { elbow_lane, elbow_points, ELBOW_STEP, ELBOW_CLEAR } from "./treedb_elbow.js";
|
|
10
|
+
|
|
11
|
+
/* Two cards 100x40: A on top, B one row below it. */
|
|
12
|
+
const A = {x1: 0, y1: 0, x2: 100, y2: 40};
|
|
13
|
+
const B = {x1: 0, y1: 130, x2: 100, y2: 170};
|
|
14
|
+
const A_HOOK = [50, 40]; /* A's bottom edge */
|
|
15
|
+
const A_FKEY = [50, 0]; /* A's top edge */
|
|
16
|
+
const B_HOOK = [60, 170];
|
|
17
|
+
const B_FKEY = [50, 130];
|
|
18
|
+
|
|
19
|
+
describe("the lane", () => {
|
|
20
|
+
test("one edge alone keeps the middle", () => {
|
|
21
|
+
expect(elbow_lane(["edge-t-7"], "edge-t-7")).toBe(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("lanes follow the order the edges were made, not the string order", () => {
|
|
25
|
+
let ids = ["edge-t-10", "edge-t-9", "edge-t-11"];
|
|
26
|
+
expect(elbow_lane(ids, "edge-t-9")).toBe(0);
|
|
27
|
+
expect(elbow_lane(ids, "edge-t-10")).toBe(1);
|
|
28
|
+
expect(elbow_lane(ids, "edge-t-11")).toBe(-1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("a new edge does not move the ones already drawn", () => {
|
|
32
|
+
let before = ["edge-t-1", "edge-t-2"];
|
|
33
|
+
let after = before.concat(["edge-t-3"]);
|
|
34
|
+
for(let id of before) {
|
|
35
|
+
expect(elbow_lane(after, id)).toBe(elbow_lane(before, id));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("a forward edge", () => {
|
|
41
|
+
test("turns in the channel half way between the ports", () => {
|
|
42
|
+
let pts = elbow_points(A_HOOK, B_FKEY, A, B, 0, true);
|
|
43
|
+
expect(pts).toEqual([[50, 85], [50, 85]]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("its lane moves the channel, and keeps it between the ports", () => {
|
|
47
|
+
let pts = elbow_points(A_HOOK, B_FKEY, A, B, 1, true);
|
|
48
|
+
expect(pts[0][1]).toBe(85 + ELBOW_STEP);
|
|
49
|
+
let far = elbow_points(A_HOOK, B_FKEY, A, B, 20, true);
|
|
50
|
+
expect(far[0][1]).toBeLessThan(B_FKEY[1]);
|
|
51
|
+
expect(far[0][1]).toBeGreaterThan(A_HOOK[1]);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("an edge that does not run forward", () => {
|
|
56
|
+
test("goes round the outside of both cards", () => {
|
|
57
|
+
/* B -> A: out of B's bottom, back up into A's top. */
|
|
58
|
+
let pts = elbow_points(B_HOOK, A_FKEY, B, A, 0, true);
|
|
59
|
+
expect(pts.length).toBe(4);
|
|
60
|
+
let side = pts[1][0];
|
|
61
|
+
expect(side).toBeGreaterThanOrEqual(100 + ELBOW_CLEAR);
|
|
62
|
+
expect(pts[2][0]).toBe(side);
|
|
63
|
+
expect(pts[0][1]).toBeGreaterThan(B.y2); /* below B */
|
|
64
|
+
expect(pts[3][1]).toBeLessThan(A.y1); /* above A */
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a reciprocal pair shares no segment", () => {
|
|
68
|
+
let forward = elbow_points(A_HOOK, B_FKEY, A, B, 0, true);
|
|
69
|
+
let back = elbow_points(B_HOOK, A_FKEY, B, A, 1, true);
|
|
70
|
+
let channel = forward[0][1];
|
|
71
|
+
expect(back.every((p) => p[1] !== channel)).toBe(true);
|
|
72
|
+
expect(back[1][0]).toBeGreaterThan(A.x2);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a negative lane goes round the other side", () => {
|
|
76
|
+
let right = elbow_points(B_HOOK, A_FKEY, B, A, 1, true);
|
|
77
|
+
let left = elbow_points(B_HOOK, A_FKEY, B, A, -1, true);
|
|
78
|
+
expect(right[1][0]).toBeGreaterThan(A.x2);
|
|
79
|
+
expect(left[1][0]).toBeLessThan(A.x1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("two cards in one row are not joined through them", () => {
|
|
83
|
+
/* C beside A, same row: A's hook (bottom) to C's fkey (top). */
|
|
84
|
+
let C = {x1: 200, y1: 0, x2: 300, y2: 40};
|
|
85
|
+
let pts = elbow_points(A_HOOK, [250, 0], A, C, 0, true);
|
|
86
|
+
expect(pts.length).toBe(4);
|
|
87
|
+
expect(pts[1][0]).toBeGreaterThan(C.x2);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("read left to right", () => {
|
|
92
|
+
test("is the same geometry with the axes swapped", () => {
|
|
93
|
+
let sw = (p) => [p[1], p[0]];
|
|
94
|
+
let swb = (b) => ({x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2});
|
|
95
|
+
for(let [s, t, bs, bt, lane] of [
|
|
96
|
+
[A_HOOK, B_FKEY, A, B, 0],
|
|
97
|
+
[A_HOOK, B_FKEY, A, B, 1],
|
|
98
|
+
[B_HOOK, A_FKEY, B, A, -1],
|
|
99
|
+
]) {
|
|
100
|
+
let down = elbow_points(s, t, bs, bt, lane, true);
|
|
101
|
+
let right = elbow_points(sw(s), sw(t), swb(bs), swb(bt), lane, false);
|
|
102
|
+
expect(right).toEqual(down.map(sw));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|