@takagaki/cortex-decisions-viewer 0.4.7 → 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 +218 -43
- package/dist/render.js +32 -19
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -472,9 +472,7 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
472
472
|
for (const sec of sections)
|
|
473
473
|
for (const r of sec.records)
|
|
474
474
|
fileNameToId.set(r.fileName, r.id);
|
|
475
|
-
// 参照先ID
|
|
476
|
-
// 外部リンク(http(s))はノード化せず null を返す(外部URLの点はグラフ肥大化=描画負荷の主因。
|
|
477
|
-
// リンク自体は各レコードの詳細・参照欄に残るため情報は失われない)。
|
|
475
|
+
// 参照先IDから層を推定してノード化する
|
|
478
476
|
const classifyTarget = (target) => {
|
|
479
477
|
if (nodes.has(target))
|
|
480
478
|
return target;
|
|
@@ -498,7 +496,9 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
498
496
|
return addNode({ id: target, label, group, url });
|
|
499
497
|
}
|
|
500
498
|
else if (/^https?:\/\//.test(target)) {
|
|
501
|
-
|
|
499
|
+
group = "external";
|
|
500
|
+
label = target.replace(/^https?:\/\//, "").slice(0, 60);
|
|
501
|
+
return addNode({ id: target, label, group, url: target });
|
|
502
502
|
}
|
|
503
503
|
else if (target.includes("/") || target.toLowerCase().endsWith(".md")) {
|
|
504
504
|
group = "silver"; // リポジトリ内ファイルへのパス参照 → GitHubのコピーへ飛べるようにする
|
|
@@ -508,45 +508,36 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
508
508
|
}
|
|
509
509
|
return addNode({ id: target, label, group });
|
|
510
510
|
};
|
|
511
|
-
// 解決先が null(外部リンク等で非ノード化)の辺は張らない
|
|
512
|
-
const pushEdge = (fromId, target, rel) => {
|
|
513
|
-
if (target)
|
|
514
|
-
edges.push({ from: fromId, to: target, rel });
|
|
515
|
-
};
|
|
516
|
-
// 外部URLノードを作る(source/references の出典用フォールバック)。
|
|
517
|
-
const externalNode = (url, label) => addNode({
|
|
518
|
-
id: url,
|
|
519
|
-
label: (label ?? url.replace(/^https?:\/\//, "")).slice(0, 60),
|
|
520
|
-
group: "external",
|
|
521
|
-
url,
|
|
522
|
-
});
|
|
523
|
-
// 出典(source/reference)の解決: Backlog課題URLは実ノード(bronze)へ、議事録ID等は該当ノードへ。
|
|
524
|
-
// 解決できない外部URL(Wiki・GitHub等)は外部ノードとして必ず繋ぐ(出典は少数で意味があるため残す。
|
|
525
|
-
// 大量の「本文リンク」の騒音は addLinkEdges 側でドロップしたまま)。
|
|
526
|
-
const provenanceTarget = (raw, label) => {
|
|
527
|
-
const v = raw.trim();
|
|
528
|
-
return classifyTarget(backlogViewKey(v) ?? v) ?? externalNode(v, label);
|
|
529
|
-
};
|
|
530
511
|
const addRelationEdges = (fromId, relations) => {
|
|
531
|
-
for (const r of relations)
|
|
532
|
-
|
|
512
|
+
for (const r of relations) {
|
|
513
|
+
edges.push({ from: fromId, to: classifyTarget(r.target), rel: r.rel });
|
|
514
|
+
}
|
|
533
515
|
};
|
|
534
516
|
const addSourceEdge = (fromId, source) => {
|
|
535
517
|
if (source && source.trim() !== "")
|
|
536
|
-
edges.push({ from: fromId, to:
|
|
518
|
+
edges.push({ from: fromId, to: classifyTarget(source.trim()), rel: "source" });
|
|
537
519
|
};
|
|
538
520
|
const addReferenceEdges = (fromId, references) => {
|
|
539
521
|
for (const ref of references) {
|
|
540
|
-
|
|
541
|
-
|
|
522
|
+
if (ref.url) {
|
|
523
|
+
const id = addNode({
|
|
524
|
+
id: ref.url,
|
|
525
|
+
label: ref.label.slice(0, 60),
|
|
526
|
+
group: "external",
|
|
527
|
+
url: ref.url,
|
|
528
|
+
});
|
|
529
|
+
edges.push({ from: fromId, to: id, rel: "reference" });
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
edges.push({ from: fromId, to: classifyTarget(ref.label), rel: "reference" });
|
|
533
|
+
}
|
|
542
534
|
}
|
|
543
535
|
};
|
|
544
|
-
// Gold層(Decision/用語/レポート/Home)の本文リンクは必ず繋ぐ(外部URLでも外部ノードとして接続)。
|
|
545
|
-
// =frontmatterを持つレコードは孤立させない。大量の騒音は「コンテキスト側の本文リンク」(content edges)
|
|
546
|
-
// のみドロップする(下の content ループは classifyTarget のまま)。
|
|
547
536
|
const addLinkEdges = (fromId, links) => {
|
|
548
|
-
for (const link of links)
|
|
549
|
-
|
|
537
|
+
for (const link of links) {
|
|
538
|
+
const key = backlogViewKey(link);
|
|
539
|
+
edges.push({ from: fromId, to: classifyTarget(key ?? link), rel: "link" });
|
|
540
|
+
}
|
|
550
541
|
};
|
|
551
542
|
// Bronze/Silverのノードを先に登録する(Gold・Bronze/Silver双方からの参照が同一ノードに解決されるように)
|
|
552
543
|
for (const c of content) {
|
|
@@ -582,7 +573,7 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
582
573
|
}
|
|
583
574
|
for (const c of content) {
|
|
584
575
|
for (const e of c.edges) {
|
|
585
|
-
|
|
576
|
+
edges.push({ from: c.id, to: classifyTarget(e.target), rel: e.rel });
|
|
586
577
|
}
|
|
587
578
|
}
|
|
588
579
|
// 自己ループと重複辺を除去する(同じURLが複数回貼られていても辺は1本)
|
|
@@ -596,15 +587,198 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
|
|
|
596
587
|
seen.add(key);
|
|
597
588
|
return true;
|
|
598
589
|
});
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
590
|
+
return { nodes: [...nodes.values()], edges: deduped };
|
|
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
|
+
}
|
|
608
782
|
}
|
|
609
783
|
/**
|
|
610
784
|
* 各レコードの bodyHtml を out/bodies/<i>.html に書き出し、bodyRef にパスを設定して
|
|
@@ -689,6 +863,7 @@ async function build(opts) {
|
|
|
689
863
|
: new Map();
|
|
690
864
|
const contentEntries = await scanContent(repoRoot, knowledgeRoot ? path.basename(knowledgeRoot) : null, repo.baseUrl, repo.branch);
|
|
691
865
|
const graph = buildGraph(decisions, home, sections, contentEntries, targetUrls, repo.baseUrl, repo.branch);
|
|
866
|
+
computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
|
|
692
867
|
const site = {
|
|
693
868
|
title: opts.title,
|
|
694
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]; })
|
|
@@ -1054,22 +1060,25 @@ const CLIENT_JS = `
|
|
|
1054
1060
|
var fx = (dx / d) * f, fy = (dy / d) * f;
|
|
1055
1061
|
e.a.vx += fx; e.a.vy += fy; e.b.vx -= fx; e.b.vy -= fy;
|
|
1056
1062
|
});
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
1063
|
+
// Goldは中心への重力、グレーは外周リングへの引力 + 減衰(元のロジック)。
|
|
1064
|
+
// 外周リングを多数のグレー(外部リンク等の枝先)が均等に埋めることで全体が円形になり、
|
|
1065
|
+
// Goldが中心に締まる。この見た目は「点を全部残す」ことで成立する(重さはBarnes-Hut+収束後停止で別途解消)。
|
|
1060
1066
|
nodes.forEach(function (n) {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
var rf = (r - RING) * 0.
|
|
1067
|
+
if (n.recordId) {
|
|
1068
|
+
n.vx -= n.x * 0.012; n.vy -= n.y * 0.012;
|
|
1069
|
+
} else {
|
|
1070
|
+
var r = Math.sqrt(n.x * n.x + n.y * n.y) + 0.01;
|
|
1071
|
+
var rf = (r - RING) * 0.012;
|
|
1066
1072
|
n.vx -= (n.x / r) * rf; n.vy -= (n.y / r) * rf;
|
|
1067
1073
|
}
|
|
1068
1074
|
if (n === draggingNode) { n.vx = 0; n.vy = 0; return; }
|
|
1069
1075
|
n.vx *= 0.85; n.vy *= 0.85;
|
|
1070
1076
|
n.x += n.vx * alpha; n.y += n.vy * alpha;
|
|
1071
1077
|
});
|
|
1072
|
-
|
|
1078
|
+
// 元のゆっくり減衰(*0.995)に戻す: ウォームアップ320回が高エネルギーで回り、Goldが中心に締まる
|
|
1079
|
+
// 深い平衡(元の見た目)まで到達する。floorは0.05ではなく0.003まで下げ、整定後はループ側で停止できる
|
|
1080
|
+
// ようにする(②の固まり対策と両立)。*0.97の速い減衰だとウォームアップが早期終了し浅い配置で固まっていた。
|
|
1081
|
+
if (alpha > 0.003) alpha *= 0.995;
|
|
1073
1082
|
}
|
|
1074
1083
|
|
|
1075
1084
|
function nodeRadius(n) {
|
|
@@ -1329,11 +1338,15 @@ const CLIENT_JS = `
|
|
|
1329
1338
|
view.scale *= factor;
|
|
1330
1339
|
}, { passive: false });
|
|
1331
1340
|
|
|
1332
|
-
//
|
|
1333
|
-
//
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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); // 全ノードが収まるよう、開いた瞬間からフレーミングしておく
|
|
1337
1350
|
if (!reduceMotion) introStart = performance.now(); // プリウォーム後を起点にして光の波を出す
|
|
1338
1351
|
|
|
1339
1352
|
var running = true;
|