@takagaki/cortex-decisions-viewer 0.4.6 → 0.4.7

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
@@ -513,26 +513,40 @@ function buildGraph(decisions, home, sections, content, targetUrls, repoBaseUrl,
513
513
  if (target)
514
514
  edges.push({ from: fromId, to: target, rel });
515
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
+ };
516
530
  const addRelationEdges = (fromId, relations) => {
517
531
  for (const r of relations)
518
532
  pushEdge(fromId, classifyTarget(r.target), r.rel);
519
533
  };
520
534
  const addSourceEdge = (fromId, source) => {
521
535
  if (source && source.trim() !== "")
522
- pushEdge(fromId, classifyTarget(source.trim()), "source");
536
+ edges.push({ from: fromId, to: provenanceTarget(source), rel: "source" });
523
537
  };
524
538
  const addReferenceEdges = (fromId, references) => {
525
- // ref.url(http(s))は classifyTarget が null を返す=外部はノード化しない。
526
- // URLが無い参照(課題キー・議事録ID等)は従来どおり該当ノードへ繋ぐ。
527
539
  for (const ref of references) {
528
- pushEdge(fromId, classifyTarget(ref.url ?? ref.label), "reference");
540
+ const to = ref.url ? provenanceTarget(ref.url, ref.label) : classifyTarget(ref.label);
541
+ pushEdge(fromId, to, "reference");
529
542
  }
530
543
  };
544
+ // Gold層(Decision/用語/レポート/Home)の本文リンクは必ず繋ぐ(外部URLでも外部ノードとして接続)。
545
+ // =frontmatterを持つレコードは孤立させない。大量の騒音は「コンテキスト側の本文リンク」(content edges)
546
+ // のみドロップする(下の content ループは classifyTarget のまま)。
531
547
  const addLinkEdges = (fromId, links) => {
532
- for (const link of links) {
533
- const key = backlogViewKey(link);
534
- pushEdge(fromId, classifyTarget(key ?? link), "link");
535
- }
548
+ for (const link of links)
549
+ edges.push({ from: fromId, to: provenanceTarget(link), rel: "link" });
536
550
  };
537
551
  // Bronze/Silverのノードを先に登録する(Gold・Bronze/Silver双方からの参照が同一ノードに解決されるように)
538
552
  for (const c of content) {
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,13 +1054,15 @@ 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
+ // さらに半径 RING の円で外周を押し戻して封じ込める。これで対角の伸び・飛び出したスポークが
1059
+ // 抑えられ、全体が円形のディスクに収まる(force-directed本来の「丸める力の不在」を補う)。
1004
1060
  nodes.forEach(function (n) {
1005
- if (n.recordId) {
1006
- n.vx -= n.x * 0.012; n.vy -= n.y * 0.012;
1007
- } else {
1008
- var r = Math.sqrt(n.x * n.x + n.y * n.y) + 0.01;
1009
- var rf = (r - RING) * 0.012;
1061
+ var g = n.recordId ? 0.012 : 0.005;
1062
+ n.vx -= n.x * g; n.vy -= n.y * g;
1063
+ var r = Math.sqrt(n.x * n.x + n.y * n.y) + 0.01;
1064
+ if (r > RING) {
1065
+ var rf = (r - RING) * 0.05; // 円の外に出た分だけ内側へ強めに戻す(円形の境界)
1010
1066
  n.vx -= (n.x / r) * rf; n.vy -= (n.y / r) * rf;
1011
1067
  }
1012
1068
  if (n === draggingNode) { n.vx = 0; n.vy = 0; return; }
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.7",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,