@takagaki/cortex-decisions-viewer 0.4.8 → 0.4.9
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/build.js +192 -0
- package/dist/render.js +20 -10
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -589,6 +589,197 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
589
589
|
});
|
|
590
590
|
return { nodes: [...nodes.values()], edges: deduped };
|
|
591
591
|
}
|
|
592
|
+
/** 反発(O(n²))。点が少ないうちはこれが最速。 */
|
|
593
|
+
function repulseNaiveLayout(ps) {
|
|
594
|
+
for (let i = 0; i < ps.length; i++) {
|
|
595
|
+
for (let j = i + 1; j < ps.length; j++) {
|
|
596
|
+
const a = ps[i], b = ps[j];
|
|
597
|
+
const dx = b.x - a.x, dy = b.y - a.y;
|
|
598
|
+
const d2 = dx * dx + dy * dy + 0.01;
|
|
599
|
+
const f = 1800 / d2;
|
|
600
|
+
const d = Math.sqrt(d2);
|
|
601
|
+
const fx = (dx / d) * f, fy = (dy / d) * f;
|
|
602
|
+
a.vx -= fx;
|
|
603
|
+
a.vy -= fy;
|
|
604
|
+
b.vx += fx;
|
|
605
|
+
b.vy += fy;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/** 反発(Barnes-Hut・O(n log n))。四分木で遠いノード群を重心1点に近似する。render.ts と同一アルゴリズム。 */
|
|
610
|
+
function repulseBHLayout(ps) {
|
|
611
|
+
const nn = ps.length;
|
|
612
|
+
if (nn < 2)
|
|
613
|
+
return;
|
|
614
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity, i;
|
|
615
|
+
for (i = 0; i < nn; i++) {
|
|
616
|
+
const p = ps[i];
|
|
617
|
+
if (p.x < minX)
|
|
618
|
+
minX = p.x;
|
|
619
|
+
if (p.y < minY)
|
|
620
|
+
minY = p.y;
|
|
621
|
+
if (p.x > maxX)
|
|
622
|
+
maxX = p.x;
|
|
623
|
+
if (p.y > maxY)
|
|
624
|
+
maxY = p.y;
|
|
625
|
+
}
|
|
626
|
+
const size = Math.max(maxX - minX, maxY - minY, 1) + 1;
|
|
627
|
+
const QX = [minX], QY = [minY], QS = [size], MASS = [0], CX = [0], CY = [0];
|
|
628
|
+
const C0 = [-1], C1 = [-1], C2 = [-1], C3 = [-1], BODY = [-1];
|
|
629
|
+
const nc = (x, y, s) => { QX.push(x); QY.push(y); QS.push(s); MASS.push(0); CX.push(0); CY.push(0); C0.push(-1); C1.push(-1); C2.push(-1); C3.push(-1); BODY.push(-1); return QX.length - 1; };
|
|
630
|
+
const childOf = (c, q) => q === 0 ? C0[c] : q === 1 ? C1[c] : q === 2 ? C2[c] : C3[c];
|
|
631
|
+
const setChild = (c, q, v) => { if (q === 0)
|
|
632
|
+
C0[c] = v;
|
|
633
|
+
else if (q === 1)
|
|
634
|
+
C1[c] = v;
|
|
635
|
+
else if (q === 2)
|
|
636
|
+
C2[c] = v;
|
|
637
|
+
else
|
|
638
|
+
C3[c] = v; };
|
|
639
|
+
const pushDown = (c, bi, depth) => {
|
|
640
|
+
const hx = QX[c] + QS[c] / 2, hy = QY[c] + QS[c] / 2;
|
|
641
|
+
const q = (ps[bi].x >= hx ? 1 : 0) + (ps[bi].y >= hy ? 2 : 0);
|
|
642
|
+
let ch = childOf(c, q);
|
|
643
|
+
if (ch === -1) {
|
|
644
|
+
const s = QS[c] / 2;
|
|
645
|
+
ch = nc(QX[c] + ((q & 1) ? s : 0), QY[c] + ((q & 2) ? s : 0), s);
|
|
646
|
+
setChild(c, q, ch);
|
|
647
|
+
}
|
|
648
|
+
insert(ch, bi, depth + 1);
|
|
649
|
+
};
|
|
650
|
+
const insert = (c, bi, depth) => {
|
|
651
|
+
const x = ps[bi].x, y = ps[bi].y;
|
|
652
|
+
if (MASS[c] === 0) {
|
|
653
|
+
CX[c] = x;
|
|
654
|
+
CY[c] = y;
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
CX[c] = (CX[c] * MASS[c] + x) / (MASS[c] + 1);
|
|
658
|
+
CY[c] = (CY[c] * MASS[c] + y) / (MASS[c] + 1);
|
|
659
|
+
}
|
|
660
|
+
if (MASS[c] === 0 && BODY[c] === -1) {
|
|
661
|
+
BODY[c] = bi;
|
|
662
|
+
MASS[c] = 1;
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
MASS[c]++;
|
|
666
|
+
if (depth >= 24)
|
|
667
|
+
return;
|
|
668
|
+
if (BODY[c] !== -2) {
|
|
669
|
+
const old = BODY[c];
|
|
670
|
+
BODY[c] = -2;
|
|
671
|
+
pushDown(c, old, depth);
|
|
672
|
+
}
|
|
673
|
+
pushDown(c, bi, depth);
|
|
674
|
+
};
|
|
675
|
+
for (i = 0; i < nn; i++)
|
|
676
|
+
insert(0, i, 0);
|
|
677
|
+
const theta2 = 1.0;
|
|
678
|
+
for (i = 0; i < nn; i++) {
|
|
679
|
+
const p = ps[i];
|
|
680
|
+
const stack = [0];
|
|
681
|
+
while (stack.length) {
|
|
682
|
+
const c = stack.pop();
|
|
683
|
+
if (MASS[c] === 0)
|
|
684
|
+
continue;
|
|
685
|
+
if (BODY[c] === i && MASS[c] === 1)
|
|
686
|
+
continue;
|
|
687
|
+
const dx = CX[c] - p.x, dy = CY[c] - p.y;
|
|
688
|
+
const d2 = dx * dx + dy * dy + 0.01;
|
|
689
|
+
if (BODY[c] !== -2 || QS[c] * QS[c] < theta2 * d2) {
|
|
690
|
+
const f = 1800 * MASS[c] / d2;
|
|
691
|
+
const d = Math.sqrt(d2);
|
|
692
|
+
p.vx -= dx / d * f;
|
|
693
|
+
p.vy -= dy / d * f;
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
if (C0[c] !== -1)
|
|
697
|
+
stack.push(C0[c]);
|
|
698
|
+
if (C1[c] !== -1)
|
|
699
|
+
stack.push(C1[c]);
|
|
700
|
+
if (C2[c] !== -1)
|
|
701
|
+
stack.push(C2[c]);
|
|
702
|
+
if (C3[c] !== -1)
|
|
703
|
+
stack.push(C3[c]);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* グラフのレイアウト座標をビルド時に1回だけ計算し、各ノードに x/y を書き込む。
|
|
710
|
+
* クライアント(render.ts startGraph)と同一の力学を回すので、ブラウザはこれを初期位置に使い
|
|
711
|
+
* ウォームアップを省略できる=開いた瞬間ゼロ待ち。描画対象(recordId or 辺に接続)のみ計算する。
|
|
712
|
+
*/
|
|
713
|
+
function computeLayout(graph) {
|
|
714
|
+
const used = new Set();
|
|
715
|
+
for (const e of graph.edges) {
|
|
716
|
+
used.add(e.from);
|
|
717
|
+
used.add(e.to);
|
|
718
|
+
}
|
|
719
|
+
const ns = graph.nodes.filter((n) => n.recordId || used.has(n.id));
|
|
720
|
+
const N = ns.length;
|
|
721
|
+
if (N === 0)
|
|
722
|
+
return;
|
|
723
|
+
const ps = ns.map(() => ({ x: 0, y: 0, vx: 0, vy: 0 }));
|
|
724
|
+
const isRec = ns.map((n) => !!n.recordId);
|
|
725
|
+
const byId = new Map();
|
|
726
|
+
ns.forEach((n, i) => byId.set(n.id, i));
|
|
727
|
+
const RING = Math.max(200, 26 * Math.sqrt(N));
|
|
728
|
+
for (let i = 0; i < N; i++) {
|
|
729
|
+
const a = (i / N) * Math.PI * 2;
|
|
730
|
+
const r = isRec[i] ? 60 : RING;
|
|
731
|
+
ps[i].x = Math.cos(a) * r + (i % 7) * 3;
|
|
732
|
+
ps[i].y = Math.sin(a) * r + (i % 5) * 3;
|
|
733
|
+
}
|
|
734
|
+
const es = [];
|
|
735
|
+
for (const e of graph.edges) {
|
|
736
|
+
const a = byId.get(e.from), b = byId.get(e.to);
|
|
737
|
+
if (a !== undefined && b !== undefined && a !== b)
|
|
738
|
+
es.push([a, b]);
|
|
739
|
+
}
|
|
740
|
+
const big = N >= 900;
|
|
741
|
+
let alpha = 1;
|
|
742
|
+
for (let t = 0; t < 450; t++) {
|
|
743
|
+
if (big)
|
|
744
|
+
repulseBHLayout(ps);
|
|
745
|
+
else
|
|
746
|
+
repulseNaiveLayout(ps);
|
|
747
|
+
for (const [ai, bi] of es) {
|
|
748
|
+
const a = ps[ai], b = ps[bi];
|
|
749
|
+
const dx = b.x - a.x, dy = b.y - a.y;
|
|
750
|
+
const d = Math.sqrt(dx * dx + dy * dy) + 0.01;
|
|
751
|
+
const f = (d - 90) * 0.02;
|
|
752
|
+
const fx = (dx / d) * f, fy = (dy / d) * f;
|
|
753
|
+
a.vx += fx;
|
|
754
|
+
a.vy += fy;
|
|
755
|
+
b.vx -= fx;
|
|
756
|
+
b.vy -= fy;
|
|
757
|
+
}
|
|
758
|
+
for (let i = 0; i < N; i++) {
|
|
759
|
+
const p = ps[i];
|
|
760
|
+
if (isRec[i]) {
|
|
761
|
+
p.vx -= p.x * 0.012;
|
|
762
|
+
p.vy -= p.y * 0.012;
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
const r = Math.sqrt(p.x * p.x + p.y * p.y) + 0.01;
|
|
766
|
+
const rf = (r - RING) * 0.012;
|
|
767
|
+
p.vx -= (p.x / r) * rf;
|
|
768
|
+
p.vy -= (p.y / r) * rf;
|
|
769
|
+
}
|
|
770
|
+
p.vx *= 0.85;
|
|
771
|
+
p.vy *= 0.85;
|
|
772
|
+
p.x += p.vx * alpha;
|
|
773
|
+
p.y += p.vy * alpha;
|
|
774
|
+
}
|
|
775
|
+
if (alpha > 0.003)
|
|
776
|
+
alpha *= 0.995;
|
|
777
|
+
}
|
|
778
|
+
for (let i = 0; i < N; i++) {
|
|
779
|
+
ns[i].x = Math.round(ps[i].x);
|
|
780
|
+
ns[i].y = Math.round(ps[i].y);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
592
783
|
/**
|
|
593
784
|
* 各レコードの bodyHtml を out/bodies/<i>.html に書き出し、bodyRef にパスを設定して
|
|
594
785
|
* inlineデータ上の bodyHtml を空にする(初期ロードのpayloadから本文を外す)。
|
|
@@ -672,6 +863,7 @@ async function build(opts) {
|
|
|
672
863
|
: new Map();
|
|
673
864
|
const contentEntries = await scanContent(repoRoot, knowledgeRoot ? path.basename(knowledgeRoot) : null, repo.baseUrl, repo.branch);
|
|
674
865
|
const graph = buildGraph(decisions, home, sections, contentEntries, targetUrls, repo.baseUrl, repo.branch);
|
|
866
|
+
computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
|
|
675
867
|
const site = {
|
|
676
868
|
title: opts.title,
|
|
677
869
|
generatedAt: opts.generatedAt,
|
package/dist/render.js
CHANGED
|
@@ -828,21 +828,27 @@ const CLIENT_JS = `
|
|
|
828
828
|
// 弱い辺を消したとき、レコード以外の孤立ノードは隠す
|
|
829
829
|
var used = {};
|
|
830
830
|
allEdges.forEach(function (e) { used[e.from] = true; used[e.to] = true; });
|
|
831
|
+
// ビルド時に算出済みの座標(n.x/n.y)があればそれを使う=ウォームアップ不要で開いた瞬間に整列済み。
|
|
832
|
+
var preLaidOut = SITE.graph.nodes.some(function (n) { return typeof n.x === "number"; });
|
|
831
833
|
var nodes = SITE.graph.nodes
|
|
832
834
|
.filter(function (n) { return n.recordId || used[n.id]; })
|
|
833
835
|
.map(function (n, i) {
|
|
834
836
|
return { id: n.id, label: n.label, group: n.group, recordId: n.recordId, url: n.url,
|
|
835
|
-
x:
|
|
837
|
+
x: typeof n.x === "number" ? n.x : 0, y: typeof n.y === "number" ? n.y : 0,
|
|
838
|
+
vx: 0, vy: 0, phase: i * 2.399 };
|
|
836
839
|
});
|
|
837
840
|
// Gold層を中心に据え、グレーのコンテキストはその外周に配置する(ハブ&スポーク)
|
|
838
841
|
var RING = Math.max(200, 26 * Math.sqrt(nodes.length));
|
|
839
842
|
var byNode = {};
|
|
840
843
|
nodes.forEach(function (n, i) {
|
|
841
844
|
byNode[n.id] = n;
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
845
|
+
// 計算済み座標が無い場合のみ、円状の初期配置を与える(フォールバック)
|
|
846
|
+
if (!preLaidOut) {
|
|
847
|
+
var angle = (i / nodes.length) * Math.PI * 2;
|
|
848
|
+
var r = n.recordId ? 60 : RING;
|
|
849
|
+
n.x = Math.cos(angle) * r + (i % 7) * 3;
|
|
850
|
+
n.y = Math.sin(angle) * r + (i % 5) * 3;
|
|
851
|
+
}
|
|
846
852
|
});
|
|
847
853
|
var edges = allEdges
|
|
848
854
|
.filter(function (e) { return byNode[e.from] && byNode[e.to]; })
|
|
@@ -1332,11 +1338,15 @@ const CLIENT_JS = `
|
|
|
1332
1338
|
view.scale *= factor;
|
|
1333
1339
|
}, { passive: false });
|
|
1334
1340
|
|
|
1335
|
-
//
|
|
1336
|
-
//
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1341
|
+
// ビルド時に座標計算済みなら、ウォームアップ(重い同期計算)を完全に省略する=開いた瞬間ゼロ待ち。
|
|
1342
|
+
// 計算済みが無い場合のみ、従来どおり裏で320回回して整列済みの静かな状態で開く。
|
|
1343
|
+
if (preLaidOut) {
|
|
1344
|
+
alpha = 0.002; // 既に整定済み。ループ側の物理計算(tick)を即停止させる(ドラッグ時のみ再開)
|
|
1345
|
+
} else {
|
|
1346
|
+
for (var warm = 0; warm < 320 && alpha > 0.06; warm++) tick();
|
|
1347
|
+
alpha = Math.min(alpha, 0.05); // 残りの揺れも最小化(落ち着いた状態で開始)
|
|
1348
|
+
}
|
|
1349
|
+
applyFit(true); // 全ノードが収まるよう、開いた瞬間からフレーミングしておく
|
|
1340
1350
|
if (!reduceMotion) introStart = performance.now(); // プリウォーム後を起点にして光の波を出す
|
|
1341
1351
|
|
|
1342
1352
|
var running = true;
|