@yanqirenshi/d3.deployment 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/js/Rectum.js +158 -20
- package/dist/js/datamodels/Edge.js +1 -1
- package/dist/js/datamodels/Node.js +1 -1
- package/dist/js/datamodels/Port.js +1 -1
- package/dist/js/painters/{Edge.js → Edges.js} +32 -13
- package/dist/js/painters/{Node.js → Nodes.js} +4 -4
- package/dist/js/painters/{Port.js → Ports.js} +4 -4
- package/package.json +1 -1
- package/tests/Rectum.test.js +100 -0
package/dist/js/Rectum.js
CHANGED
|
@@ -5,14 +5,16 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
7
|
exports["default"] = void 0;
|
|
8
|
+
var d3 = _interopRequireWildcard(require("d3"));
|
|
8
9
|
var _assh0le = require("@yanqirenshi/assh0le");
|
|
9
10
|
var _Node = _interopRequireDefault(require("./datamodels/Node.js"));
|
|
10
11
|
var _Edge = _interopRequireDefault(require("./datamodels/Edge.js"));
|
|
11
12
|
var _Port = _interopRequireDefault(require("./datamodels/Port.js"));
|
|
12
|
-
var
|
|
13
|
-
var
|
|
14
|
-
var
|
|
13
|
+
var _Nodes = _interopRequireDefault(require("./painters/Nodes.js"));
|
|
14
|
+
var _Edges = _interopRequireDefault(require("./painters/Edges.js"));
|
|
15
|
+
var _Ports = _interopRequireDefault(require("./painters/Ports.js"));
|
|
15
16
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
|
|
17
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
|
|
16
18
|
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
|
|
17
19
|
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
18
20
|
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
@@ -46,9 +48,9 @@ var Rectum = exports["default"] = /*#__PURE__*/function (_Colon) {
|
|
|
46
48
|
|
|
47
49
|
// 描画クラス(ペインタ)
|
|
48
50
|
_this._painter = {
|
|
49
|
-
NODE: new
|
|
50
|
-
EDGE: new
|
|
51
|
-
PORT: new
|
|
51
|
+
NODE: new _Nodes["default"](),
|
|
52
|
+
EDGE: new _Edges["default"](),
|
|
53
|
+
PORT: new _Ports["default"]()
|
|
52
54
|
};
|
|
53
55
|
return _this;
|
|
54
56
|
}
|
|
@@ -185,15 +187,15 @@ var Rectum = exports["default"] = /*#__PURE__*/function (_Colon) {
|
|
|
185
187
|
}
|
|
186
188
|
return ports;
|
|
187
189
|
}
|
|
188
|
-
/**
|
|
189
|
-
* Port の位置を計算するため、Port と Nodeの中心の直線を算出する。
|
|
190
|
-
*
|
|
191
|
-
* 直線の構築は共有版(assh0le の Geometry.getPortLine)に委譲する。
|
|
192
|
-
* 共有版は box ローカル座標で返すため、node の絶対位置をオフセットして返す
|
|
193
|
-
* (旧インライン実装と数学的に等価)。
|
|
194
|
-
*
|
|
195
|
-
* @param {number} degree Port の位置(角度)。
|
|
196
|
-
* @param {object} node port の Node。 算出した Line の位置を補正するための Node
|
|
190
|
+
/**
|
|
191
|
+
* Port の位置を計算するため、Port と Nodeの中心の直線を算出する。
|
|
192
|
+
*
|
|
193
|
+
* 直線の構築は共有版(assh0le の Geometry.getPortLine)に委譲する。
|
|
194
|
+
* 共有版は box ローカル座標で返すため、node の絶対位置をオフセットして返す
|
|
195
|
+
* (旧インライン実装と数学的に等価)。
|
|
196
|
+
*
|
|
197
|
+
* @param {number} degree Port の位置(角度)。
|
|
198
|
+
* @param {object} node port の Node。 算出した Line の位置を補正するための Node
|
|
197
199
|
*/
|
|
198
200
|
}, {
|
|
199
201
|
key: "makePortLine",
|
|
@@ -249,11 +251,11 @@ var Rectum = exports["default"] = /*#__PURE__*/function (_Colon) {
|
|
|
249
251
|
y: edge.to.port.position.y
|
|
250
252
|
};
|
|
251
253
|
}
|
|
252
|
-
/**
|
|
253
|
-
* data を元に描画用のデータに変換する。
|
|
254
|
-
* 変換したデータを保管する。
|
|
255
|
-
* data.edge を元に port のデータも作成する。
|
|
256
|
-
* @param {object} data { node: [], edges: [] }
|
|
254
|
+
/**
|
|
255
|
+
* data を元に描画用のデータに変換する。
|
|
256
|
+
* 変換したデータを保管する。
|
|
257
|
+
* data.edge を元に port のデータも作成する。
|
|
258
|
+
* @param {object} data { node: [], edges: [] }
|
|
257
259
|
*/
|
|
258
260
|
}, {
|
|
259
261
|
key: "data",
|
|
@@ -400,6 +402,142 @@ var Rectum = exports["default"] = /*#__PURE__*/function (_Colon) {
|
|
|
400
402
|
} finally {
|
|
401
403
|
_iterator8.f();
|
|
402
404
|
}
|
|
405
|
+
this.attachNodeDrag(place, data);
|
|
406
|
+
}
|
|
407
|
+
///// ////////////////////////////////////////////////////////////////
|
|
408
|
+
///// Drag (grab + drag でノードを移動)
|
|
409
|
+
///// ////////////////////////////////////////////////////////////////
|
|
410
|
+
/**
|
|
411
|
+
* ノード(node / component)にグラブ&ドラッグを付与する。
|
|
412
|
+
* 親を掴んで動かすとサブツリー(子孫ノード + ポート + 接続エッジ)がまとまって動く。
|
|
413
|
+
*/
|
|
414
|
+
}, {
|
|
415
|
+
key: "attachNodeDrag",
|
|
416
|
+
value: function attachNodeDrag(place, data) {
|
|
417
|
+
var self = this;
|
|
418
|
+
var drag = d3.drag().on('start', function () {
|
|
419
|
+
d3.select(this).style('cursor', 'grabbing');
|
|
420
|
+
}).on('drag', function (event, node) {
|
|
421
|
+
self.moveSubtree(place, data, node, event.dx, event.dy);
|
|
422
|
+
}).on('end', function () {
|
|
423
|
+
d3.select(this).style('cursor', 'grab');
|
|
424
|
+
});
|
|
425
|
+
place.selectAll('g.node, g.component').style('cursor', 'grab').call(drag);
|
|
426
|
+
}
|
|
427
|
+
/** node を根とするサブツリーの node._id 集合を返す。 */
|
|
428
|
+
}, {
|
|
429
|
+
key: "collectSubtree",
|
|
430
|
+
value: function collectSubtree(node) {
|
|
431
|
+
var ids = new Set();
|
|
432
|
+
var _walk = function walk(n) {
|
|
433
|
+
ids.add(n._id);
|
|
434
|
+
var _iterator9 = _createForOfIteratorHelper(n.children || []),
|
|
435
|
+
_step9;
|
|
436
|
+
try {
|
|
437
|
+
for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
|
|
438
|
+
var child = _step9.value;
|
|
439
|
+
_walk(child);
|
|
440
|
+
}
|
|
441
|
+
} catch (err) {
|
|
442
|
+
_iterator9.e(err);
|
|
443
|
+
} finally {
|
|
444
|
+
_iterator9.f();
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
_walk(node);
|
|
448
|
+
return ids;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* サブツリーを (dx, dy) だけ移動する「データ変換」。DOM には触れない(テスト可能)。
|
|
452
|
+
* ノードは剛体移動なのでポートも同じ量だけ平行移動し、影響エッジを再フィットする。
|
|
453
|
+
*
|
|
454
|
+
* @returns { moved:Set<id>, edges:Array } moved は移動した node._id、edges は再描画対象のエッジ
|
|
455
|
+
*/
|
|
456
|
+
}, {
|
|
457
|
+
key: "moveSubtreeData",
|
|
458
|
+
value: function moveSubtreeData(data, node, dx, dy) {
|
|
459
|
+
var moved = this.collectSubtree(node);
|
|
460
|
+
var _iterator0 = _createForOfIteratorHelper(data.nodes.list),
|
|
461
|
+
_step0;
|
|
462
|
+
try {
|
|
463
|
+
for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
|
|
464
|
+
var n = _step0.value;
|
|
465
|
+
if (moved.has(n._id)) {
|
|
466
|
+
n._position.x += dx;
|
|
467
|
+
n._position.y += dy;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch (err) {
|
|
471
|
+
_iterator0.e(err);
|
|
472
|
+
} finally {
|
|
473
|
+
_iterator0.f();
|
|
474
|
+
}
|
|
475
|
+
var _iterator1 = _createForOfIteratorHelper(data.ports.list),
|
|
476
|
+
_step1;
|
|
477
|
+
try {
|
|
478
|
+
for (_iterator1.s(); !(_step1 = _iterator1.n()).done;) {
|
|
479
|
+
var port = _step1.value;
|
|
480
|
+
if (moved.has(port.node._id)) {
|
|
481
|
+
port.position.x += dx;
|
|
482
|
+
port.position.y += dy;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
} catch (err) {
|
|
486
|
+
_iterator1.e(err);
|
|
487
|
+
} finally {
|
|
488
|
+
_iterator1.f();
|
|
489
|
+
}
|
|
490
|
+
var edges = [];
|
|
491
|
+
var _iterator10 = _createForOfIteratorHelper(data.edges.list),
|
|
492
|
+
_step10;
|
|
493
|
+
try {
|
|
494
|
+
for (_iterator10.s(); !(_step10 = _iterator10.n()).done;) {
|
|
495
|
+
var edge = _step10.value;
|
|
496
|
+
if (!moved.has(edge.from.node._id) && !moved.has(edge.to.node._id)) continue;
|
|
497
|
+
this.fittingEdge(edge);
|
|
498
|
+
edges.push(edge);
|
|
499
|
+
}
|
|
500
|
+
} catch (err) {
|
|
501
|
+
_iterator10.e(err);
|
|
502
|
+
} finally {
|
|
503
|
+
_iterator10.f();
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
moved: moved,
|
|
507
|
+
edges: edges
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/** moveSubtreeData でデータを更新し、対応する SVG(ノード g・ポート円・エッジ path)を反映する。 */
|
|
511
|
+
}, {
|
|
512
|
+
key: "moveSubtree",
|
|
513
|
+
value: function moveSubtree(place, data, node, dx, dy) {
|
|
514
|
+
var _this$moveSubtreeData = this.moveSubtreeData(data, node, dx, dy),
|
|
515
|
+
moved = _this$moveSubtreeData.moved,
|
|
516
|
+
edges = _this$moveSubtreeData.edges;
|
|
517
|
+
place.selectAll('g.node, g.component').filter(function (d) {
|
|
518
|
+
return moved.has(d._id);
|
|
519
|
+
}).attr('transform', function (d) {
|
|
520
|
+
return 'translate(' + d._position.x + ',' + d._position.y + ')';
|
|
521
|
+
});
|
|
522
|
+
place.selectAll('circle.port').filter(function (d) {
|
|
523
|
+
return moved.has(d.node._id);
|
|
524
|
+
}).attr('cx', function (d) {
|
|
525
|
+
return d.position.x;
|
|
526
|
+
}).attr('cy', function (d) {
|
|
527
|
+
return d.position.y;
|
|
528
|
+
});
|
|
529
|
+
var _iterator11 = _createForOfIteratorHelper(edges),
|
|
530
|
+
_step11;
|
|
531
|
+
try {
|
|
532
|
+
for (_iterator11.s(); !(_step11 = _iterator11.n()).done;) {
|
|
533
|
+
var edge = _step11.value;
|
|
534
|
+
this._painter.EDGE.redraw(place, edge);
|
|
535
|
+
}
|
|
536
|
+
} catch (err) {
|
|
537
|
+
_iterator11.e(err);
|
|
538
|
+
} finally {
|
|
539
|
+
_iterator11.f();
|
|
540
|
+
}
|
|
403
541
|
}
|
|
404
542
|
}]);
|
|
405
543
|
}(_assh0le.Colon);
|
|
@@ -12,7 +12,7 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
|
|
|
12
12
|
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
13
13
|
/**
|
|
14
14
|
* Edge のデータクラス。入力データの正規化(既定値補完)を行う。
|
|
15
|
-
* 描画は painters/
|
|
15
|
+
* 描画は painters/Edges.js が担当する。
|
|
16
16
|
*/
|
|
17
17
|
var Edge = exports["default"] = /*#__PURE__*/function () {
|
|
18
18
|
function Edge() {
|
|
@@ -13,7 +13,7 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
|
|
|
13
13
|
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
14
14
|
/**
|
|
15
15
|
* Node のデータクラス。入力データの正規化(既定値補完)と幾何情報の提供を行う。
|
|
16
|
-
* 描画は painters/
|
|
16
|
+
* 描画は painters/Nodes.js が担当する。
|
|
17
17
|
*/
|
|
18
18
|
var Node = exports["default"] = /*#__PURE__*/function () {
|
|
19
19
|
function Node() {
|
|
@@ -12,7 +12,7 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
|
|
|
12
12
|
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
13
13
|
/**
|
|
14
14
|
* Port のデータクラス。入力データの正規化を行う。
|
|
15
|
-
* 描画は painters/
|
|
15
|
+
* 描画は painters/Ports.js が担当する。
|
|
16
16
|
*/
|
|
17
17
|
var Port = exports["default"] = /*#__PURE__*/function () {
|
|
18
18
|
function Port() {
|
|
@@ -14,41 +14,60 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
|
|
|
14
14
|
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
15
15
|
/**
|
|
16
16
|
* Edge の描画クラス。正規化済みデータ(datamodels/Edge.js の出力)を SVG path で描画する。
|
|
17
|
+
* path には class="edge" と data-edge-id を付与し、ドラッグ後の再描画(redraw)を可能にする。
|
|
17
18
|
*/
|
|
18
|
-
var
|
|
19
|
-
function
|
|
20
|
-
_classCallCheck(this,
|
|
19
|
+
var Edges = exports["default"] = /*#__PURE__*/function () {
|
|
20
|
+
function Edges() {
|
|
21
|
+
_classCallCheck(this, Edges);
|
|
21
22
|
}
|
|
22
|
-
return _createClass(
|
|
23
|
+
return _createClass(Edges, [{
|
|
23
24
|
key: "draw",
|
|
24
25
|
value: function draw(place, data) {
|
|
26
|
+
var path = place.append('path').attr('class', 'edge').attr('data-edge-id', data._id).attr('fill', 'none').style('stroke-linecap', 'round');
|
|
27
|
+
this.applyGeometry(path, data);
|
|
28
|
+
}
|
|
29
|
+
/** ドラッグ後などに、既存のエッジ path を from/to の最新位置で引き直す。 */
|
|
30
|
+
}, {
|
|
31
|
+
key: "redraw",
|
|
32
|
+
value: function redraw(place, data) {
|
|
33
|
+
var id = String(data._id);
|
|
34
|
+
var path = place.selectAll('path.edge').filter(function () {
|
|
35
|
+
return this.getAttribute('data-edge-id') === id;
|
|
36
|
+
});
|
|
37
|
+
if (!path.empty()) this.applyGeometry(path, data);
|
|
38
|
+
}
|
|
39
|
+
/** from/to の位置からエッジ path の d・マーカー・破線を適用する。 */
|
|
40
|
+
}, {
|
|
41
|
+
key: "applyGeometry",
|
|
42
|
+
value: function applyGeometry(path, data) {
|
|
25
43
|
var lineData = [{
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
x: data.from.position.x,
|
|
45
|
+
y: data.from.position.y,
|
|
28
46
|
stroke: data.stroke
|
|
29
47
|
}, {
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
x: data.to.position.x,
|
|
49
|
+
y: data.to.position.y
|
|
32
50
|
}];
|
|
33
51
|
var lineFunction = d3.line().x(function (d) {
|
|
34
52
|
return d.x;
|
|
35
53
|
}).y(function (d) {
|
|
36
54
|
return d.y;
|
|
37
55
|
});
|
|
38
|
-
|
|
39
|
-
if (!d[0].stroke.marker || d[0].stroke.marker.end) return
|
|
56
|
+
path.datum(lineData).attr('d', lineFunction).attr('marker-end', function (d) {
|
|
57
|
+
if (!d[0].stroke.marker || d[0].stroke.marker.end) return 'url(#edge-arrow)';
|
|
40
58
|
return null;
|
|
41
|
-
}).style('
|
|
59
|
+
}).style('fill', function (d) {
|
|
42
60
|
return d[0].stroke.color;
|
|
43
|
-
}).style(
|
|
61
|
+
}).style('stroke', function (d) {
|
|
44
62
|
return d[0].stroke.color;
|
|
45
|
-
}).style(
|
|
63
|
+
}).style('stroke-width', function (d) {
|
|
46
64
|
return d[0].stroke.width;
|
|
47
65
|
});
|
|
48
66
|
var len = path.node().getTotalLength();
|
|
49
67
|
var margin = 12;
|
|
50
68
|
var t = len - margin * 2;
|
|
51
69
|
path.attr('stroke-dasharray', "0 ".concat(margin, " ").concat(t, " ").concat(margin)).attr('stroke-dashoffset', 0);
|
|
70
|
+
return path;
|
|
52
71
|
}
|
|
53
72
|
}]);
|
|
54
73
|
}();
|
|
@@ -13,11 +13,11 @@ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e
|
|
|
13
13
|
/**
|
|
14
14
|
* Node の描画クラス。正規化済みデータ(datamodels/Node.js の出力)を SVG に描画する。
|
|
15
15
|
*/
|
|
16
|
-
var
|
|
17
|
-
function
|
|
18
|
-
_classCallCheck(this,
|
|
16
|
+
var Nodes = exports["default"] = /*#__PURE__*/function () {
|
|
17
|
+
function Nodes() {
|
|
18
|
+
_classCallCheck(this, Nodes);
|
|
19
19
|
}
|
|
20
|
-
return _createClass(
|
|
20
|
+
return _createClass(Nodes, [{
|
|
21
21
|
key: "addFilterShadow",
|
|
22
22
|
value:
|
|
23
23
|
///// ////////////////////////////////////////////////////////////////
|
|
@@ -13,11 +13,11 @@ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e
|
|
|
13
13
|
/**
|
|
14
14
|
* Port の描画クラス。エッジ端点の円マーカーを描画する。
|
|
15
15
|
*/
|
|
16
|
-
var
|
|
17
|
-
function
|
|
18
|
-
_classCallCheck(this,
|
|
16
|
+
var Ports = exports["default"] = /*#__PURE__*/function () {
|
|
17
|
+
function Ports() {
|
|
18
|
+
_classCallCheck(this, Ports);
|
|
19
19
|
}
|
|
20
|
-
return _createClass(
|
|
20
|
+
return _createClass(Ports, [{
|
|
21
21
|
key: "draw",
|
|
22
22
|
value: function draw(place, data) {
|
|
23
23
|
place.selectAll('circle.port').data([data], function (d) {
|
package/package.json
CHANGED
package/tests/Rectum.test.js
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import Rectum from '../src/js/Rectum.js';
|
|
2
2
|
|
|
3
|
+
// 親(id:1)の中に子(id:3)が入れ子。edge 100: 1→2、edge 101: 3→2。
|
|
4
|
+
const NESTED_SAMPLE = () => ({
|
|
5
|
+
nodes: [
|
|
6
|
+
{
|
|
7
|
+
type: 'NODE', id: 1,
|
|
8
|
+
label: { text: 'P', position: { x: 20, y: 20 } },
|
|
9
|
+
size: { w: 300, h: 300 }, position: { x: 0, y: 0 },
|
|
10
|
+
children: [
|
|
11
|
+
{
|
|
12
|
+
type: 'NODE', id: 3,
|
|
13
|
+
label: { text: 'C', position: { x: 20, y: 20 } },
|
|
14
|
+
size: { w: 120, h: 100 }, position: { x: 90, y: 120 },
|
|
15
|
+
children: [],
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: 'NODE', id: 2,
|
|
21
|
+
label: { text: 'Q', position: { x: 20, y: 20 } },
|
|
22
|
+
size: { w: 300, h: 300 }, position: { x: 450, y: 0 },
|
|
23
|
+
children: [],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
edges: [
|
|
27
|
+
{ id: 100, from: { id: 1, position: 0 }, to: { id: 2, position: 180 }, port: 45 },
|
|
28
|
+
{ id: 101, from: { id: 3, position: 270 }, to: { id: 2, position: 90 }, port: 45 },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
3
32
|
// makePortLine は共有版 Geometry.getPortLine + node 位置オフセットで構築される。
|
|
4
33
|
// 200×100 の node / degree=90: 中心 {100,50}、回転後の終点 {-223,0} → {-123,50}。
|
|
5
34
|
// 位置オフセット {10,20} を足して from {110,70} / to {-113,70}。
|
|
@@ -16,3 +45,74 @@ test('makePortLine builds the center-to-port line offset by the node position',
|
|
|
16
45
|
to: { x: -113, y: 70 },
|
|
17
46
|
});
|
|
18
47
|
});
|
|
48
|
+
|
|
49
|
+
// selector を設定していないので data() は描画せず pool を返すだけ(DOM 不要)。
|
|
50
|
+
const buildData = () => {
|
|
51
|
+
const rectum = new Rectum({});
|
|
52
|
+
rectum.data(NESTED_SAMPLE());
|
|
53
|
+
return { rectum, data: rectum.data() };
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
test('collectSubtree gathers a node and all descendants', () => {
|
|
57
|
+
const { rectum, data } = buildData();
|
|
58
|
+
|
|
59
|
+
// 親(1)を根にすると子(3)も含む。子(3)単体は 3 のみ。
|
|
60
|
+
expect([...rectum.collectSubtree(data.nodes.ht[1])].sort()).toEqual([1, 3]);
|
|
61
|
+
expect([...rectum.collectSubtree(data.nodes.ht[3])]).toEqual([3]);
|
|
62
|
+
expect([...rectum.collectSubtree(data.nodes.ht[2])]).toEqual([2]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('moving a parent shifts its subtree (nodes + ports) rigidly and re-fits touched edges', () => {
|
|
66
|
+
const { rectum, data } = buildData();
|
|
67
|
+
|
|
68
|
+
const parent = data.nodes.ht[1];
|
|
69
|
+
const child = data.nodes.ht[3];
|
|
70
|
+
const other = data.nodes.ht[2];
|
|
71
|
+
|
|
72
|
+
const p0 = { ...parent._position };
|
|
73
|
+
const c0 = { ...child._position };
|
|
74
|
+
const o0 = { ...other._position };
|
|
75
|
+
|
|
76
|
+
// 親のポート(edge100 FROM)と子のポート(edge101 FROM)の初期位置
|
|
77
|
+
const edge100 = data.edges.list.find((e) => e._id === 100);
|
|
78
|
+
const edge101 = data.edges.list.find((e) => e._id === 101);
|
|
79
|
+
const parentPort0 = { ...edge100.from.port.position };
|
|
80
|
+
const childPort0 = { ...edge101.from.port.position };
|
|
81
|
+
const staticEnd0 = { ...edge100.to.position }; // node 2 側(動かさない)
|
|
82
|
+
|
|
83
|
+
const result = rectum.moveSubtreeData(data, parent, 50, 30);
|
|
84
|
+
|
|
85
|
+
// 親サブツリー(1,3)が移動対象、node 2 は非対象
|
|
86
|
+
expect([...result.moved].sort()).toEqual([1, 3]);
|
|
87
|
+
expect(result.edges.map((e) => e._id).sort()).toEqual([100, 101]);
|
|
88
|
+
|
|
89
|
+
// 親・子ノードは +50 / +30 だけ剛体移動(z は不変)、node 2 は不変
|
|
90
|
+
expect(parent._position).toEqual({ ...p0, x: p0.x + 50, y: p0.y + 30 });
|
|
91
|
+
expect(child._position).toEqual({ ...c0, x: c0.x + 50, y: c0.y + 30 });
|
|
92
|
+
expect(other._position).toEqual(o0);
|
|
93
|
+
|
|
94
|
+
// 親・子のポートも同じ量だけ移動
|
|
95
|
+
expect(edge100.from.port.position).toEqual({ x: parentPort0.x + 50, y: parentPort0.y + 30 });
|
|
96
|
+
expect(edge101.from.port.position).toEqual({ x: childPort0.x + 50, y: childPort0.y + 30 });
|
|
97
|
+
|
|
98
|
+
// エッジ端点: 動いた側(from)は追従、node 2 側(to)は不変
|
|
99
|
+
expect(edge100.from.position).toEqual({ x: parentPort0.x + 50, y: parentPort0.y + 30 });
|
|
100
|
+
expect(edge100.to.position).toEqual(staticEnd0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('moving a child leaf shifts only that child, and edges from it follow', () => {
|
|
104
|
+
const { rectum, data } = buildData();
|
|
105
|
+
|
|
106
|
+
const parent = data.nodes.ht[1];
|
|
107
|
+
const child = data.nodes.ht[3];
|
|
108
|
+
const p0 = { ...parent._position };
|
|
109
|
+
const c0 = { ...child._position };
|
|
110
|
+
|
|
111
|
+
const result = rectum.moveSubtreeData(data, child, -20, 40);
|
|
112
|
+
|
|
113
|
+
expect([...result.moved]).toEqual([3]);
|
|
114
|
+
expect(result.edges.map((e) => e._id)).toEqual([101]); // 3→2 のみ
|
|
115
|
+
|
|
116
|
+
expect(child._position).toEqual({ ...c0, x: c0.x - 20, y: c0.y + 40 });
|
|
117
|
+
expect(parent._position).toEqual(p0); // 親は動かない
|
|
118
|
+
});
|