@takagaki/cortex-decisions-viewer 0.4.6 → 0.4.8

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 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
- return null; // 外部リンクはノード化しない
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,30 +508,35 @@ 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
511
  const addRelationEdges = (fromId, relations) => {
517
- for (const r of relations)
518
- pushEdge(fromId, classifyTarget(r.target), r.rel);
512
+ for (const r of relations) {
513
+ edges.push({ from: fromId, to: classifyTarget(r.target), rel: r.rel });
514
+ }
519
515
  };
520
516
  const addSourceEdge = (fromId, source) => {
521
517
  if (source && source.trim() !== "")
522
- pushEdge(fromId, classifyTarget(source.trim()), "source");
518
+ edges.push({ from: fromId, to: classifyTarget(source.trim()), rel: "source" });
523
519
  };
524
520
  const addReferenceEdges = (fromId, references) => {
525
- // ref.url(http(s))は classifyTarget が null を返す=外部はノード化しない。
526
- // URLが無い参照(課題キー・議事録ID等)は従来どおり該当ノードへ繋ぐ。
527
521
  for (const ref of references) {
528
- pushEdge(fromId, classifyTarget(ref.url ?? ref.label), "reference");
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
+ }
529
534
  }
530
535
  };
531
536
  const addLinkEdges = (fromId, links) => {
532
537
  for (const link of links) {
533
538
  const key = backlogViewKey(link);
534
- pushEdge(fromId, classifyTarget(key ?? link), "link");
539
+ edges.push({ from: fromId, to: classifyTarget(key ?? link), rel: "link" });
535
540
  }
536
541
  };
537
542
  // Bronze/Silverのノードを先に登録する(Gold・Bronze/Silver双方からの参照が同一ノードに解決されるように)
@@ -568,7 +573,7 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
568
573
  }
569
574
  for (const c of content) {
570
575
  for (const e of c.edges) {
571
- pushEdge(c.id, classifyTarget(e.target), e.rel);
576
+ edges.push({ from: c.id, to: classifyTarget(e.target), rel: e.rel });
572
577
  }
573
578
  }
574
579
  // 自己ループと重複辺を除去する(同じURLが複数回貼られていても辺は1本)
@@ -582,15 +587,7 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
582
587
  seen.add(key);
583
588
  return true;
584
589
  });
585
- // 描画対象は「レコード(Gold) or 辺に接続のあるノード」だけ。外部リンク除去で孤立した
586
- // 非レコードノードは描画もされないため、ペイロードからも落としておく(初期ロードを軽くする)。
587
- const usedIds = new Set();
588
- for (const e of deduped) {
589
- usedIds.add(e.from);
590
- usedIds.add(e.to);
591
- }
592
- const prunedNodes = [...nodes.values()].filter((n) => n.recordId || usedIds.has(n.id));
593
- return { nodes: prunedNodes, edges: deduped };
590
+ return { nodes: [...nodes.values()], edges: deduped };
594
591
  }
595
592
  /**
596
593
  * 各レコードの bodyHtml を out/bodies/<i>.html に書き出し、bodyRef にパスを設定して
package/dist/render.js CHANGED
@@ -979,8 +979,8 @@ const CLIENT_JS = `
979
979
  }
980
980
  resize();
981
981
 
982
- function tick() {
983
- // 反発
982
+ // 反発(O()・全ペア総当たり)。点が少ないうちはこれが最速。
983
+ function repulseNaive() {
984
984
  for (var i = 0; i < nodes.length; i++) {
985
985
  for (var j = i + 1; j < nodes.length; j++) {
986
986
  var a = nodes[i], b = nodes[j];
@@ -992,6 +992,60 @@ const CLIENT_JS = `
992
992
  a.vx -= fx; a.vy -= fy; b.vx += fx; b.vy += fy;
993
993
  }
994
994
  }
995
+ }
996
+ // 反発(Barnes-Hut・O(n log n))。四分木で遠いノード群を重心1点に近似する。
997
+ // 点が多い案件(フリート拡大時)でも tick が重くならない=点を削らずに固まりを防ぐための土台。
998
+ function repulseBH() {
999
+ var nn = nodes.length, i;
1000
+ if (nn < 2) return;
1001
+ var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1002
+ for (i = 0; i < nn; i++) { var p0 = nodes[i]; if (p0.x < minX) minX = p0.x; if (p0.y < minY) minY = p0.y; if (p0.x > maxX) maxX = p0.x; if (p0.y > maxY) maxY = p0.y; }
1003
+ var size = Math.max(maxX - minX, maxY - minY, 1) + 1;
1004
+ // 配列ベースの四分木。BODY: -1=空 / >=0=葉(そのノードindex) / -2=内部ノード
1005
+ var QX = [minX], QY = [minY], QS = [size], MASS = [0], CX = [0], CY = [0];
1006
+ var C0 = [-1], C1 = [-1], C2 = [-1], C3 = [-1], BODY = [-1];
1007
+ function 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; }
1008
+ function childOf(c, q) { return q === 0 ? C0[c] : q === 1 ? C1[c] : q === 2 ? C2[c] : C3[c]; }
1009
+ function setChild(c, q, v) { if (q === 0) C0[c] = v; else if (q === 1) C1[c] = v; else if (q === 2) C2[c] = v; else C3[c] = v; }
1010
+ function pushDown(c, bi, depth) {
1011
+ var hx = QX[c] + QS[c] / 2, hy = QY[c] + QS[c] / 2;
1012
+ var q = (nodes[bi].x >= hx ? 1 : 0) + (nodes[bi].y >= hy ? 2 : 0);
1013
+ var ch = childOf(c, q);
1014
+ if (ch === -1) { var s = QS[c] / 2; ch = nc(QX[c] + ((q & 1) ? s : 0), QY[c] + ((q & 2) ? s : 0), s); setChild(c, q, ch); }
1015
+ insert(ch, bi, depth + 1);
1016
+ }
1017
+ function insert(c, bi, depth) {
1018
+ var x = nodes[bi].x, y = nodes[bi].y;
1019
+ if (MASS[c] === 0) { CX[c] = x; CY[c] = y; } else { CX[c] = (CX[c] * MASS[c] + x) / (MASS[c] + 1); CY[c] = (CY[c] * MASS[c] + y) / (MASS[c] + 1); }
1020
+ if (MASS[c] === 0 && BODY[c] === -1) { BODY[c] = bi; MASS[c] = 1; return; }
1021
+ MASS[c]++;
1022
+ if (depth >= 24) return; // 深さ上限(ほぼ同一座標): これ以上分割せず集約
1023
+ if (BODY[c] !== -2) { var old = BODY[c]; BODY[c] = -2; pushDown(c, old, depth); }
1024
+ pushDown(c, bi, depth);
1025
+ }
1026
+ for (i = 0; i < nn; i++) insert(0, i, 0);
1027
+ var theta2 = 1.0; // 受容角θ=1.0 の二乗(セルサイズ/距離 < θ なら重心1点で近似)
1028
+ for (i = 0; i < nn; i++) {
1029
+ var p = nodes[i]; var stack = [0];
1030
+ while (stack.length) {
1031
+ var c = stack.pop();
1032
+ if (MASS[c] === 0) continue;
1033
+ if (BODY[c] === i && MASS[c] === 1) continue; // 自分自身の葉
1034
+ var dx = CX[c] - p.x, dy = CY[c] - p.y; var d2 = dx * dx + dy * dy + 0.01;
1035
+ if (BODY[c] !== -2 || QS[c] * QS[c] < theta2 * d2) {
1036
+ var f = 1800 * MASS[c] / d2; var d = Math.sqrt(d2);
1037
+ p.vx -= dx / d * f; p.vy -= dy / d * f;
1038
+ } else {
1039
+ if (C0[c] !== -1) stack.push(C0[c]); if (C1[c] !== -1) stack.push(C1[c]);
1040
+ if (C2[c] !== -1) stack.push(C2[c]); if (C3[c] !== -1) stack.push(C3[c]);
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ // 約900点を境に切替: 小規模=naive(オーバーヘッド無しで最速) / 大規模=Barnes-Hut(固まらない)。
1046
+ var REPULSE_NAIVE_MAX = 900;
1047
+ function tick() {
1048
+ if (nodes.length < REPULSE_NAIVE_MAX) repulseNaive(); else repulseBH();
995
1049
  // ばね(辺)
996
1050
  edges.forEach(function (e) {
997
1051
  var dx = e.b.x - e.a.x, dy = e.b.y - e.a.y;
@@ -1000,7 +1054,9 @@ const CLIENT_JS = `
1000
1054
  var fx = (dx / d) * f, fy = (dy / d) * f;
1001
1055
  e.a.vx += fx; e.a.vy += fy; e.b.vx -= fx; e.b.vy -= fy;
1002
1056
  });
1003
- // Goldは中心への重力、グレーは外周リングへの引力 + 減衰
1057
+ // Goldは中心への重力、グレーは外周リングへの引力 + 減衰(元のロジック)。
1058
+ // 外周リングを多数のグレー(外部リンク等の枝先)が均等に埋めることで全体が円形になり、
1059
+ // Goldが中心に締まる。この見た目は「点を全部残す」ことで成立する(重さはBarnes-Hut+収束後停止で別途解消)。
1004
1060
  nodes.forEach(function (n) {
1005
1061
  if (n.recordId) {
1006
1062
  n.vx -= n.x * 0.012; n.vy -= n.y * 0.012;
@@ -1013,7 +1069,10 @@ const CLIENT_JS = `
1013
1069
  n.vx *= 0.85; n.vy *= 0.85;
1014
1070
  n.x += n.vx * alpha; n.y += n.vy * alpha;
1015
1071
  });
1016
- if (alpha > 0.0015) alpha *= 0.97; // 収束まで減衰しきる(下げ止まりさせず、整定後はループ側で物理計算を止める)
1072
+ // 元のゆっくり減衰(*0.995)に戻す: ウォームアップ320回が高エネルギーで回り、Goldが中心に締まる
1073
+ // 深い平衡(元の見た目)まで到達する。floorは0.05ではなく0.003まで下げ、整定後はループ側で停止できる
1074
+ // ようにする(②の固まり対策と両立)。*0.97の速い減衰だとウォームアップが早期終了し浅い配置で固まっていた。
1075
+ if (alpha > 0.003) alpha *= 0.995;
1017
1076
  }
1018
1077
 
1019
1078
  function nodeRadius(n) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,