@yanqirenshi/d3.deployment 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/js/Rectum.js +145 -0
- package/dist/js/painters/Edges.js +32 -9
- package/package.json +1 -1
- package/tests/Rectum.test.js +100 -0
package/dist/js/Rectum.js
CHANGED
|
@@ -5,6 +5,7 @@ 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"));
|
|
@@ -13,6 +14,7 @@ var _Nodes = _interopRequireDefault(require("./painters/Nodes.js"));
|
|
|
13
14
|
var _Edges = _interopRequireDefault(require("./painters/Edges.js"));
|
|
14
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; }
|
|
@@ -400,6 +402,149 @@ 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
|
+
|
|
419
|
+
// d3 コールバック内で `this`(= DOM 要素)に依存しない。
|
|
420
|
+
// Next.js(Webpack/SWC)のトランスパイルで `this` が書き換わり得るため、
|
|
421
|
+
// 掴んだ要素は sourceEvent から closest() で取り、クロージャで保持する。
|
|
422
|
+
var grabbed = null;
|
|
423
|
+
var drag = d3.drag().on('start', function (event) {
|
|
424
|
+
grabbed = event.sourceEvent.target.closest('g.node, g.component');
|
|
425
|
+
if (grabbed) grabbed.style.cursor = 'grabbing';
|
|
426
|
+
}).on('drag', function (event, node) {
|
|
427
|
+
self.moveSubtree(place, data, node, event.dx, event.dy);
|
|
428
|
+
}).on('end', function () {
|
|
429
|
+
if (grabbed) grabbed.style.cursor = 'grab';
|
|
430
|
+
grabbed = null;
|
|
431
|
+
});
|
|
432
|
+
place.selectAll('g.node, g.component').style('cursor', 'grab').call(drag);
|
|
433
|
+
}
|
|
434
|
+
/** node を根とするサブツリーの node._id 集合を返す。 */
|
|
435
|
+
}, {
|
|
436
|
+
key: "collectSubtree",
|
|
437
|
+
value: function collectSubtree(node) {
|
|
438
|
+
var ids = new Set();
|
|
439
|
+
var _walk = function walk(n) {
|
|
440
|
+
ids.add(n._id);
|
|
441
|
+
var _iterator9 = _createForOfIteratorHelper(n.children || []),
|
|
442
|
+
_step9;
|
|
443
|
+
try {
|
|
444
|
+
for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
|
|
445
|
+
var child = _step9.value;
|
|
446
|
+
_walk(child);
|
|
447
|
+
}
|
|
448
|
+
} catch (err) {
|
|
449
|
+
_iterator9.e(err);
|
|
450
|
+
} finally {
|
|
451
|
+
_iterator9.f();
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
_walk(node);
|
|
455
|
+
return ids;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* サブツリーを (dx, dy) だけ移動する「データ変換」。DOM には触れない(テスト可能)。
|
|
459
|
+
* ノードは剛体移動なのでポートも同じ量だけ平行移動し、影響エッジを再フィットする。
|
|
460
|
+
*
|
|
461
|
+
* @returns { moved:Set<id>, edges:Array } moved は移動した node._id、edges は再描画対象のエッジ
|
|
462
|
+
*/
|
|
463
|
+
}, {
|
|
464
|
+
key: "moveSubtreeData",
|
|
465
|
+
value: function moveSubtreeData(data, node, dx, dy) {
|
|
466
|
+
var moved = this.collectSubtree(node);
|
|
467
|
+
var _iterator0 = _createForOfIteratorHelper(data.nodes.list),
|
|
468
|
+
_step0;
|
|
469
|
+
try {
|
|
470
|
+
for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
|
|
471
|
+
var n = _step0.value;
|
|
472
|
+
if (moved.has(n._id)) {
|
|
473
|
+
n._position.x += dx;
|
|
474
|
+
n._position.y += dy;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} catch (err) {
|
|
478
|
+
_iterator0.e(err);
|
|
479
|
+
} finally {
|
|
480
|
+
_iterator0.f();
|
|
481
|
+
}
|
|
482
|
+
var _iterator1 = _createForOfIteratorHelper(data.ports.list),
|
|
483
|
+
_step1;
|
|
484
|
+
try {
|
|
485
|
+
for (_iterator1.s(); !(_step1 = _iterator1.n()).done;) {
|
|
486
|
+
var port = _step1.value;
|
|
487
|
+
if (moved.has(port.node._id)) {
|
|
488
|
+
port.position.x += dx;
|
|
489
|
+
port.position.y += dy;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} catch (err) {
|
|
493
|
+
_iterator1.e(err);
|
|
494
|
+
} finally {
|
|
495
|
+
_iterator1.f();
|
|
496
|
+
}
|
|
497
|
+
var edges = [];
|
|
498
|
+
var _iterator10 = _createForOfIteratorHelper(data.edges.list),
|
|
499
|
+
_step10;
|
|
500
|
+
try {
|
|
501
|
+
for (_iterator10.s(); !(_step10 = _iterator10.n()).done;) {
|
|
502
|
+
var edge = _step10.value;
|
|
503
|
+
if (!moved.has(edge.from.node._id) && !moved.has(edge.to.node._id)) continue;
|
|
504
|
+
this.fittingEdge(edge);
|
|
505
|
+
edges.push(edge);
|
|
506
|
+
}
|
|
507
|
+
} catch (err) {
|
|
508
|
+
_iterator10.e(err);
|
|
509
|
+
} finally {
|
|
510
|
+
_iterator10.f();
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
moved: moved,
|
|
514
|
+
edges: edges
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/** moveSubtreeData でデータを更新し、対応する SVG(ノード g・ポート円・エッジ path)を反映する。 */
|
|
518
|
+
}, {
|
|
519
|
+
key: "moveSubtree",
|
|
520
|
+
value: function moveSubtree(place, data, node, dx, dy) {
|
|
521
|
+
var _this$moveSubtreeData = this.moveSubtreeData(data, node, dx, dy),
|
|
522
|
+
moved = _this$moveSubtreeData.moved,
|
|
523
|
+
edges = _this$moveSubtreeData.edges;
|
|
524
|
+
place.selectAll('g.node, g.component').filter(function (d) {
|
|
525
|
+
return moved.has(d._id);
|
|
526
|
+
}).attr('transform', function (d) {
|
|
527
|
+
return 'translate(' + d._position.x + ',' + d._position.y + ')';
|
|
528
|
+
});
|
|
529
|
+
place.selectAll('circle.port').filter(function (d) {
|
|
530
|
+
return moved.has(d.node._id);
|
|
531
|
+
}).attr('cx', function (d) {
|
|
532
|
+
return d.position.x;
|
|
533
|
+
}).attr('cy', function (d) {
|
|
534
|
+
return d.position.y;
|
|
535
|
+
});
|
|
536
|
+
var _iterator11 = _createForOfIteratorHelper(edges),
|
|
537
|
+
_step11;
|
|
538
|
+
try {
|
|
539
|
+
for (_iterator11.s(); !(_step11 = _iterator11.n()).done;) {
|
|
540
|
+
var edge = _step11.value;
|
|
541
|
+
this._painter.EDGE.redraw(place, edge);
|
|
542
|
+
}
|
|
543
|
+
} catch (err) {
|
|
544
|
+
_iterator11.e(err);
|
|
545
|
+
} finally {
|
|
546
|
+
_iterator11.f();
|
|
547
|
+
}
|
|
403
548
|
}
|
|
404
549
|
}]);
|
|
405
550
|
}(_assh0le.Colon);
|
|
@@ -14,6 +14,7 @@ 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
19
|
var Edges = exports["default"] = /*#__PURE__*/function () {
|
|
19
20
|
function Edges() {
|
|
@@ -22,33 +23,55 @@ var Edges = exports["default"] = /*#__PURE__*/function () {
|
|
|
22
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
|
+
|
|
35
|
+
// d3 コールバック内では `this`(= DOM 要素)に依存しない。
|
|
36
|
+
// Next.js(Webpack/SWC)などのトランスパイル環境で `this` が
|
|
37
|
+
// 書き換わり得るため、引数 (d, i, nodes) の nodes[i] で要素へアクセスする。
|
|
38
|
+
var path = place.selectAll('path.edge').filter(function (d, i, nodes) {
|
|
39
|
+
return nodes[i].getAttribute('data-edge-id') === id;
|
|
40
|
+
});
|
|
41
|
+
if (!path.empty()) this.applyGeometry(path, data);
|
|
42
|
+
}
|
|
43
|
+
/** from/to の位置からエッジ path の d・マーカー・破線を適用する。 */
|
|
44
|
+
}, {
|
|
45
|
+
key: "applyGeometry",
|
|
46
|
+
value: function applyGeometry(path, data) {
|
|
25
47
|
var lineData = [{
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
x: data.from.position.x,
|
|
49
|
+
y: data.from.position.y,
|
|
28
50
|
stroke: data.stroke
|
|
29
51
|
}, {
|
|
30
|
-
|
|
31
|
-
|
|
52
|
+
x: data.to.position.x,
|
|
53
|
+
y: data.to.position.y
|
|
32
54
|
}];
|
|
33
55
|
var lineFunction = d3.line().x(function (d) {
|
|
34
56
|
return d.x;
|
|
35
57
|
}).y(function (d) {
|
|
36
58
|
return d.y;
|
|
37
59
|
});
|
|
38
|
-
|
|
39
|
-
if (!d[0].stroke.marker || d[0].stroke.marker.end) return
|
|
60
|
+
path.datum(lineData).attr('d', lineFunction).attr('marker-end', function (d) {
|
|
61
|
+
if (!d[0].stroke.marker || d[0].stroke.marker.end) return 'url(#edge-arrow)';
|
|
40
62
|
return null;
|
|
41
|
-
}).style('
|
|
63
|
+
}).style('fill', function (d) {
|
|
42
64
|
return d[0].stroke.color;
|
|
43
|
-
}).style(
|
|
65
|
+
}).style('stroke', function (d) {
|
|
44
66
|
return d[0].stroke.color;
|
|
45
|
-
}).style(
|
|
67
|
+
}).style('stroke-width', function (d) {
|
|
46
68
|
return d[0].stroke.width;
|
|
47
69
|
});
|
|
48
70
|
var len = path.node().getTotalLength();
|
|
49
71
|
var margin = 12;
|
|
50
72
|
var t = len - margin * 2;
|
|
51
73
|
path.attr('stroke-dasharray', "0 ".concat(margin, " ").concat(t, " ").concat(margin)).attr('stroke-dashoffset', 0);
|
|
74
|
+
return path;
|
|
52
75
|
}
|
|
53
76
|
}]);
|
|
54
77
|
}();
|
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
|
+
});
|