@takagaki/cortex-decisions-viewer 0.12.113 → 0.12.115
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 +41 -0
- package/dist/ranking.js +89 -0
- package/dist/render.js +324 -38
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -50,6 +50,7 @@ exports.collectMeetings = collectMeetings;
|
|
|
50
50
|
exports.findBacklogDir = findBacklogDir;
|
|
51
51
|
exports.figmaUrlFromRef = figmaUrlFromRef;
|
|
52
52
|
exports.identifyEvidence = identifyEvidence;
|
|
53
|
+
exports.attachGlow = attachGlow;
|
|
53
54
|
exports.bodyFileSlug = bodyFileSlug;
|
|
54
55
|
exports.bodyStampId = bodyStampId;
|
|
55
56
|
exports.bodyStampComment = bodyStampComment;
|
|
@@ -66,6 +67,7 @@ const evidence_names_1 = require("./evidence-names");
|
|
|
66
67
|
const repo_1 = require("./repo");
|
|
67
68
|
const engagement_1 = require("./engagement");
|
|
68
69
|
const story_1 = require("./story");
|
|
70
|
+
const ranking_1 = require("./ranking");
|
|
69
71
|
const rederivation_1 = require("./rederivation");
|
|
70
72
|
// 直近のGold昇格で追加されたレコードのパス集合。レコード生成の各所から参照するのでモジュールスコープに置く
|
|
71
73
|
// (引数で引き回すと呼び出し側の改修が広範囲になる)。build() の冒頭で1回だけ解決する。
|
|
@@ -1854,6 +1856,30 @@ function identifyEvidence(value, ctx, humanLabel) {
|
|
|
1854
1856
|
kind: canon.kind,
|
|
1855
1857
|
};
|
|
1856
1858
|
}
|
|
1859
|
+
/**
|
|
1860
|
+
* 順位のファイルが持つ 4 区画の置き場(`Cortex/<区画>/records/` の下か)。分類(frontmatter の `type`)では決めない——
|
|
1861
|
+
* engine は置き場で順位を付けるので、`type` が無い・違う記録だけが一律の大きさで目立つ、を防ぐ
|
|
1862
|
+
*/
|
|
1863
|
+
const RANKED_DIR_RE = /(?:^|\/)(?:Decisions|Rules|Glossary|Members)\/(?:records\/)?[^/]+$/;
|
|
1864
|
+
/**
|
|
1865
|
+
* 記録の点に、目次と同じ順位から決めた明るさ(`glow`・0〜1)を付ける(engine #551・オーナー決定 10/1)。
|
|
1866
|
+
*
|
|
1867
|
+
* 順位は engine の書いたファイル(`ranking.ts`)をそのまま読み、ここでは並べ替えない。
|
|
1868
|
+
* - 4 区画(決定・ルール・用語・関係者)の記録で順位に載っていないもの(目次に出さない割り振りの決定・順位を書いた夜より
|
|
1869
|
+
* 後に足された記録)は**一番暗い 0**(目次に載らない=一番下。一律の大きさのままだと中位より目立ってしまう)
|
|
1870
|
+
* - 根拠の点・Home・4 区画の外の記録には付けない(今までどおりの見た目)
|
|
1871
|
+
*/
|
|
1872
|
+
function attachGlow(nodes, glow, recordRelPaths) {
|
|
1873
|
+
for (const n of nodes) {
|
|
1874
|
+
if (!n.recordId)
|
|
1875
|
+
continue;
|
|
1876
|
+
const rel = recordRelPaths.get(n.recordId);
|
|
1877
|
+
if (rel === undefined || !RANKED_DIR_RE.test(rel))
|
|
1878
|
+
continue;
|
|
1879
|
+
const g = glow.get(rel);
|
|
1880
|
+
n.glow = g === undefined ? 0 : g;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1857
1883
|
/**
|
|
1858
1884
|
* 全レコードのfrontmatter(relations / source / references)と本文リンクから関係グラフを構築する(engine #551・分類A 10/1)。
|
|
1859
1885
|
*
|
|
@@ -2395,12 +2421,21 @@ async function build(opts) {
|
|
|
2395
2421
|
// 関与の帳簿(便 14b・#709)。関係者の一覧を目次と同じ「関わりの多い順」に並べる材料。
|
|
2396
2422
|
// **読めない・使えないなら `null`** で、関係者の並びは今までと 1 バイトも同じ
|
|
2397
2423
|
const engagement = await (0, engagement_1.readEngagementLedger)(repoRoot);
|
|
2424
|
+
// 目次と同じ順位(engine #551)。**読めないなら `null`** で、つながり画面の点は今までどおり一律
|
|
2425
|
+
const memoryRanking = await (0, ranking_1.readMemoryRanking)(repoRoot);
|
|
2426
|
+
// 記録の id → リポジトリルート相対のパス(順位のファイルの鍵と突き合わせる。`repoRoot` 起点・cwd 起点にしない)
|
|
2427
|
+
const recordRelPaths = new Map();
|
|
2428
|
+
const relOf = (dir, fileName) => {
|
|
2429
|
+
const rel = path.relative(repoRoot, dir).split(path.sep).join("/");
|
|
2430
|
+
return rel ? `${rel}/${fileName}` : fileName;
|
|
2431
|
+
};
|
|
2398
2432
|
const decisions = [];
|
|
2399
2433
|
const rawDates = new Map();
|
|
2400
2434
|
for (const fileName of mdFiles) {
|
|
2401
2435
|
const raw = await node_fs_1.promises.readFile(path.join(dirAbs, fileName), "utf8");
|
|
2402
2436
|
const rec = await parseDecision(raw, fileName, repoRelDir, repo.baseUrl, repo.branch, refs);
|
|
2403
2437
|
decisions.push(rec);
|
|
2438
|
+
recordRelPaths.set(rec.id, relOf(dirAbs, fileName));
|
|
2404
2439
|
// 置き換えの逆引きの決定日(書かれたままの文字列・SiteData には載せない)
|
|
2405
2440
|
const rd = rawFrontmatterDate(raw);
|
|
2406
2441
|
if (rd !== undefined)
|
|
@@ -2475,6 +2510,9 @@ async function build(opts) {
|
|
|
2475
2510
|
// **点と線は同じなのに地図の絵が毎晩組み変わる**(cortex-context で 1414/1414 の
|
|
2476
2511
|
// 座標が動くのを実測)。並びが効くのは一覧だけ、という約束をここで固定する
|
|
2477
2512
|
graphSections.push({ name: e.name, records: records });
|
|
2513
|
+
for (const r of records)
|
|
2514
|
+
if (!recordRelPaths.has(r.id))
|
|
2515
|
+
recordRelPaths.set(r.id, relOf(recordsDir, r.fileName));
|
|
2478
2516
|
}
|
|
2479
2517
|
}
|
|
2480
2518
|
sections.sort((a, b) => a.name.localeCompare(b.name, "ja"));
|
|
@@ -2514,6 +2552,9 @@ async function build(opts) {
|
|
|
2514
2552
|
}
|
|
2515
2553
|
const graph = buildGraph(decisions, home, graphSections, targetUrls, repo.baseUrl, evidenceBook);
|
|
2516
2554
|
computeLayout(graph); // 各ノードに x/y を書き込む(クライアントはこれを初期位置に使いウォームアップ省略)
|
|
2555
|
+
// 点の明るさと大きさ(engine #551)。順位のファイルが読めない案件では 1 点も付かない(今までと同じ画面)
|
|
2556
|
+
if (memoryRanking)
|
|
2557
|
+
attachGlow(graph.nodes, (0, ranking_1.glowByPath)(memoryRanking), recordRelPaths);
|
|
2517
2558
|
// データソースと自動化(fleet-status.json 由来・上で 1 回だけ読んだもの)。無ければ undefined でセクション非表示。
|
|
2518
2559
|
// すでにある会議(投入モーダルの「会議名」の選択肢)。intake の有無に関わらず載せる——
|
|
2519
2560
|
// 出し分けは表示側の仕事で、ここで絞ると「投入の口を後から付けたら選択肢が空」になる
|
package/dist/ranking.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MEMORY_RANKING_SECTIONS = exports.MEMORY_RANKING_VERSION = exports.MEMORY_RANKING_PATH = void 0;
|
|
7
|
+
exports.parseMemoryRanking = parseMemoryRanking;
|
|
8
|
+
exports.readMemoryRanking = readMemoryRanking;
|
|
9
|
+
exports.glowByPath = glowByPath;
|
|
10
|
+
/**
|
|
11
|
+
* つながり画面の点の明るさと大きさ(cortex-engine #551・オーナー決定 2026-10-01)。
|
|
12
|
+
*
|
|
13
|
+
* ## 🔴 順位の決め方を画面に持たない
|
|
14
|
+
*
|
|
15
|
+
* 「光の強さ(大きさ)は目次(`Cortex/Index.md`)と同じ順位で変える。順位は全件ぶんを目次と同じ 1 つの決め方で出し、
|
|
16
|
+
* 画面はそれを読む(決め方を画面側に写さない)」。だからここは engine が書いた `.cortex/memory-ranking.json`
|
|
17
|
+
* (engine `scripts/memory-ranking.mjs`・目次の書き手が目次と同じコミットで書く)を**読むだけ**で、並べ替えない。
|
|
18
|
+
* 決めるのは「区画の中で何番目か」を明るさ(0〜1)にする写し方だけ(一番上=1・一番下=0・間は等間隔)。
|
|
19
|
+
*
|
|
20
|
+
* ## 🔴 ファイルが無い・壊れている・版が違う案件は「今までと 1 バイトも同じ画面」
|
|
21
|
+
*
|
|
22
|
+
* 読めなければ `null` を返し、点に `glow` を付けない(画面は今までどおり一律の大きさ・光り)。
|
|
23
|
+
* 読めた案件で 4 区画の記録なのに載っていないもの(目次に出さない割り振りの決定・順位を書いた夜より後に足された記録)は
|
|
24
|
+
* 一番暗い 0 にする(`build.ts` の `attachGlow`)。Home・区画の外の記録・根拠の点には付けない。
|
|
25
|
+
*/
|
|
26
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
27
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
28
|
+
/** 置き場(engine `scripts/memory-ranking.mjs` の `RANKING_PATH`) */
|
|
29
|
+
exports.MEMORY_RANKING_PATH = ".cortex/memory-ranking.json";
|
|
30
|
+
/** 形の版(engine の `RANKING_VERSION`。違ったら読まない) */
|
|
31
|
+
exports.MEMORY_RANKING_VERSION = 1;
|
|
32
|
+
/** 載る区画(engine の `RANKING_SECTIONS`) */
|
|
33
|
+
exports.MEMORY_RANKING_SECTIONS = ["Decisions", "Rules", "Glossary", "Members"];
|
|
34
|
+
/** 中身を読む(投げない)。形が違う・版が違う → `null` */
|
|
35
|
+
function parseMemoryRanking(text) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(text);
|
|
38
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
39
|
+
return null;
|
|
40
|
+
const o = parsed;
|
|
41
|
+
if (o.version !== exports.MEMORY_RANKING_VERSION)
|
|
42
|
+
return null;
|
|
43
|
+
const r = o.ranking;
|
|
44
|
+
if (!r || typeof r !== "object" || Array.isArray(r))
|
|
45
|
+
return null;
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const key of exports.MEMORY_RANKING_SECTIONS) {
|
|
48
|
+
const list = r[key];
|
|
49
|
+
if (!Array.isArray(list))
|
|
50
|
+
return null;
|
|
51
|
+
out[key] = list.filter((p) => typeof p === "string" && p !== "");
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** 案件リポから読む。無い → `null`(黙る)・壊れている → `null`(build のログに 1 行) */
|
|
60
|
+
async function readMemoryRanking(repoRoot) {
|
|
61
|
+
let text;
|
|
62
|
+
try {
|
|
63
|
+
text = await promises_1.default.readFile(node_path_1.default.join(repoRoot, exports.MEMORY_RANKING_PATH), "utf8");
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const parsed = parseMemoryRanking(text);
|
|
69
|
+
if (!parsed)
|
|
70
|
+
console.warn(`${node_path_1.default.basename(exports.MEMORY_RANKING_PATH)}: 読めないので、つながり画面の点は一律の明るさで出します`);
|
|
71
|
+
return parsed;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 記録のパス → 明るさ(0〜1)。区画の中の位置だけで決める(一番上=1・一番下=0・1 件だけの区画は 1)。
|
|
75
|
+
* 小数は 3 桁に丸める(画面のデータを太らせない)。
|
|
76
|
+
*/
|
|
77
|
+
function glowByPath(ranking) {
|
|
78
|
+
const out = new Map();
|
|
79
|
+
for (const key of exports.MEMORY_RANKING_SECTIONS) {
|
|
80
|
+
const list = ranking[key];
|
|
81
|
+
const last = list.length - 1;
|
|
82
|
+
list.forEach((p, i) => {
|
|
83
|
+
if (out.has(p))
|
|
84
|
+
return;
|
|
85
|
+
out.set(p, last <= 0 ? 1 : Math.round((1 - i / last) * 1000) / 1000);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
package/dist/render.js
CHANGED
|
@@ -4789,6 +4789,12 @@ button.site-icon[aria-busy="true"] .site-icon-plus { opacity: 0; }
|
|
|
4789
4789
|
background: rgba(13,20,45,0.75); color: #cdd8f2; font-size: 12.5px; font-weight: 600; cursor: pointer;
|
|
4790
4790
|
}
|
|
4791
4791
|
.graph-reset:hover { background: rgba(30,40,80,0.9); border-color: rgba(160,180,230,0.7); }
|
|
4792
|
+
/* 期間で光らせる(engine #551・オーナー決定 10/1)。キーワードの検索の下の 1 行 */
|
|
4793
|
+
.graph-period { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 10px; font-size: 13px; color: var(--muted); }
|
|
4794
|
+
.graph-period select, .graph-period input[type="date"] { padding: 5px 8px; border: 1px solid var(--border); border-radius: 8px; font: inherit; color: var(--text); background: var(--surface); }
|
|
4795
|
+
.graph-period button { padding: 4px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: inherit; cursor: pointer; font: inherit; }
|
|
4796
|
+
.graph-period button:disabled, .graph-period input:disabled { opacity: 0.45; cursor: default; }
|
|
4797
|
+
.graph-period-text { color: var(--text); font-weight: 600; }
|
|
4792
4798
|
|
|
4793
4799
|
/* ---- knowledge view switch(ストーリーの日次/週次/月次タブも同じ見た目・分類B 2026-09-30) ---- */
|
|
4794
4800
|
.view-switch, .story-sub-tabs {
|
|
@@ -4984,6 +4990,7 @@ a.pager-btn:hover { text-decoration: none; }
|
|
|
4984
4990
|
.story-card {
|
|
4985
4991
|
border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 18px; background: var(--surface);
|
|
4986
4992
|
}
|
|
4993
|
+
.story-card-focus { border-color: #2563eb; box-shadow: 0 0 0 2px rgba(37,99,235,0.25); }
|
|
4987
4994
|
.story-quiet { margin: -6px 0; padding: 0 18px; font-size: 12.5px; color: var(--muted); }
|
|
4988
4995
|
.story-quiet-date { font-variant-numeric: tabular-nums; }
|
|
4989
4996
|
.story-card-heading { margin: 0 0 8px; font-size: 15px; }
|
|
@@ -8536,6 +8543,7 @@ ${calendarAddUrl.toString()}
|
|
|
8536
8543
|
// 文中のリンクは「押したら進む=戻れる」に見えるので、そちらだけ積む
|
|
8537
8544
|
function switchTab(tabKey, push) {
|
|
8538
8545
|
listState.tab = tabKey;
|
|
8546
|
+
storyFocus = null;
|
|
8539
8547
|
listState.page = 1;
|
|
8540
8548
|
if (push) history.pushState({}, "", tabUrl(tabKey));
|
|
8541
8549
|
else history.replaceState(null, "", tabUrl(tabKey));
|
|
@@ -8817,6 +8825,8 @@ ${calendarAddUrl.toString()}
|
|
|
8817
8825
|
var STORY_SUB_DEFAULT = "daily";
|
|
8818
8826
|
var STORY_SUBS = [["daily", "日次"], ["weekly", "週次"], ["monthly", "月次"]];
|
|
8819
8827
|
var storySub = STORY_SUB_DEFAULT;
|
|
8828
|
+
// 記録の詳細の「この日のストーリー」から着いたときに目立たせる日(日次)・週の月曜(週次)。URL の ?day= / ?week=(engine #551)
|
|
8829
|
+
var storyFocus = null;
|
|
8820
8830
|
|
|
8821
8831
|
function storySubUrl(sub) {
|
|
8822
8832
|
return sub === STORY_SUB_DEFAULT ? "?tab=story" : "?tab=story&sub=" + encodeURIComponent(sub);
|
|
@@ -8824,6 +8834,7 @@ ${calendarAddUrl.toString()}
|
|
|
8824
8834
|
function switchStorySub(sub) {
|
|
8825
8835
|
if (storySub === sub) return;
|
|
8826
8836
|
storySub = sub;
|
|
8837
|
+
storyFocus = null;
|
|
8827
8838
|
history.replaceState(null, "", storySubUrl(sub));
|
|
8828
8839
|
renderList();
|
|
8829
8840
|
}
|
|
@@ -8846,7 +8857,7 @@ ${calendarAddUrl.toString()}
|
|
|
8846
8857
|
]));
|
|
8847
8858
|
return;
|
|
8848
8859
|
}
|
|
8849
|
-
list.appendChild(el("section", { class: "story-card" }, [
|
|
8860
|
+
list.appendChild(el("section", { class: "story-card", "data-story-key": d.date }, [
|
|
8850
8861
|
el("h3", { class: "story-card-heading", text: d.heading || d.date }),
|
|
8851
8862
|
d.bodyHtml ? el("div", { class: "body", html: d.bodyHtml }) : null,
|
|
8852
8863
|
// その日のファイルを GitHub で開く(表示の文字にファイル名を出さない)。URL が無い案件は出さない
|
|
@@ -8862,7 +8873,7 @@ ${calendarAddUrl.toString()}
|
|
|
8862
8873
|
el("h4", { text: "人の追記" }),
|
|
8863
8874
|
el("div", { class: "body", html: entry.humanHtml }),
|
|
8864
8875
|
]) : null;
|
|
8865
|
-
return el("section", { class: "story-card" }, [
|
|
8876
|
+
return el("section", { class: "story-card", "data-story-key": entry.key }, [
|
|
8866
8877
|
el("h3", { class: "story-card-heading", text: entry.heading }),
|
|
8867
8878
|
entry.phase ? el("p", { class: "story-phase" }, [
|
|
8868
8879
|
el("span", { class: "story-phase-label", text: phaseLabel }),
|
|
@@ -8964,6 +8975,53 @@ ${calendarAddUrl.toString()}
|
|
|
8964
8975
|
if (storySub === "weekly") renderStoryWeekly();
|
|
8965
8976
|
else if (storySub === "monthly") renderStoryMonthly();
|
|
8966
8977
|
else renderStoryDaily();
|
|
8978
|
+
// 記録の詳細から着いた日(週)のカードを目立たせて、そこまで送る
|
|
8979
|
+
if (storyFocus) {
|
|
8980
|
+
var focused = null;
|
|
8981
|
+
app.querySelectorAll(".story-card[data-story-key]").forEach(function (c) {
|
|
8982
|
+
if (!focused && c.getAttribute("data-story-key") === storyFocus) focused = c;
|
|
8983
|
+
});
|
|
8984
|
+
if (focused) {
|
|
8985
|
+
focused.classList.add("story-card-focus");
|
|
8986
|
+
if (typeof focused.scrollIntoView === "function") focused.scrollIntoView({ block: "start" });
|
|
8987
|
+
}
|
|
8988
|
+
}
|
|
8989
|
+
}
|
|
8990
|
+
|
|
8991
|
+
/**
|
|
8992
|
+
* 記録の日付から「その日のストーリー」の行き先を決める(engine #551・オーナー決定 10/1)。
|
|
8993
|
+
* その日の日次のカード(動きのあった日)があれば日次へ、無ければその日を含む週のあらすじへ。どちらも無ければ null(リンクを出さない)
|
|
8994
|
+
*/
|
|
8995
|
+
function storyTargetOf(date) {
|
|
8996
|
+
if (!date || !/^\\d{4}-\\d{2}-\\d{2}$/.test(date) || !hasTab("story")) return null;
|
|
8997
|
+
var days = SITE.story || [];
|
|
8998
|
+
for (var i = 0; i < days.length; i++) {
|
|
8999
|
+
if (!days[i].quiet && days[i].date === date) return { sub: "daily", key: date, href: "?tab=story&day=" + encodeURIComponent(date) };
|
|
9000
|
+
}
|
|
9001
|
+
var weeks = (SITE.storyDigest && SITE.storyDigest.weeks) || [];
|
|
9002
|
+
var monday = storyMondayOf(date);
|
|
9003
|
+
for (var j = 0; j < weeks.length; j++) {
|
|
9004
|
+
if (weeks[j].key === monday) return { sub: "weekly", key: monday, href: "?tab=story&sub=weekly&week=" + encodeURIComponent(monday) };
|
|
9005
|
+
}
|
|
9006
|
+
return null;
|
|
9007
|
+
}
|
|
9008
|
+
/** 記録の詳細の「この日のストーリー」(行き先が無ければ null) */
|
|
9009
|
+
function storyLinkFor(rec) {
|
|
9010
|
+
var target = storyTargetOf(rec && rec.date);
|
|
9011
|
+
if (!target) return null;
|
|
9012
|
+
var a = el("a", { class: "trace-btn story-link-btn", href: target.href, text: target.sub === "daily" ? "📖 この日のストーリー" : "📖 この週のストーリー" });
|
|
9013
|
+
a.addEventListener("click", function (ev) {
|
|
9014
|
+
// 新しいタブで開く操作(修飾キー・中ボタン)はブラウザに任せる(href は本物の URL)
|
|
9015
|
+
if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey || (typeof ev.button === "number" && ev.button !== 0)) return;
|
|
9016
|
+
ev.preventDefault();
|
|
9017
|
+
listState.tab = "story";
|
|
9018
|
+
storySub = target.sub;
|
|
9019
|
+
storyFocus = target.key;
|
|
9020
|
+
lastWasDetail = false;
|
|
9021
|
+
history.pushState({}, "", target.href);
|
|
9022
|
+
renderList();
|
|
9023
|
+
});
|
|
9024
|
+
return a;
|
|
8967
9025
|
}
|
|
8968
9026
|
|
|
8969
9027
|
// 導入経緯(overview.adoption)の表示情報。Home見出し横のチップに出すラベルだけを返す。
|
|
@@ -20275,7 +20333,7 @@ ${calendarAddUrl.toString()}
|
|
|
20275
20333
|
// 警告帯が出ない記録の削除の口はここ(編集の行の末尾)。draft には出さない
|
|
20276
20334
|
var delMsgR = attachDeleteToEditRow(r, editRowR);
|
|
20277
20335
|
head.appendChild(el("div", { class: "detail-actions" }, [
|
|
20278
|
-
el("div", { class: "detail-actions-row" }, [ traceBtn2 ]),
|
|
20336
|
+
el("div", { class: "detail-actions-row" }, [ traceBtn2, storyLinkFor(r) ]),
|
|
20279
20337
|
editRowR.children.length ? editRowR : null,
|
|
20280
20338
|
delMsgR,
|
|
20281
20339
|
]));
|
|
@@ -20353,7 +20411,32 @@ ${calendarAddUrl.toString()}
|
|
|
20353
20411
|
REL_COLOR[r] = REL_META[r].color;
|
|
20354
20412
|
if (REL_META[r].kind === "evidence" || REL_META[r].kind === "topic") WEAK_RELS[r] = true;
|
|
20355
20413
|
});
|
|
20356
|
-
|
|
20414
|
+
// period: 期間で光らせる(engine #551)。unit は all(光らせない)/day/week、day は基準の日(YYYY-MM-DD)
|
|
20415
|
+
var graphState = { showWeak: true, q: "", period: { unit: "all", day: null } };
|
|
20416
|
+
|
|
20417
|
+
/** 期間の範囲(両端を含む YYYY-MM-DD)。「すべて」・日付が無いときは null。週は月曜〜日曜(ストーリーの週と同じ区切り) */
|
|
20418
|
+
function graphPeriodRange(period) {
|
|
20419
|
+
if (!period || !period.day || !/^\\d{4}-\\d{2}-\\d{2}$/.test(period.day)) return null;
|
|
20420
|
+
if (period.unit === "day") return { from: period.day, to: period.day };
|
|
20421
|
+
if (period.unit === "week") {
|
|
20422
|
+
var monday = storyMondayOf(period.day);
|
|
20423
|
+
return { from: monday, to: storyDayOfMs(storyUtcMs(monday) + 6 * 86400000) };
|
|
20424
|
+
}
|
|
20425
|
+
return null;
|
|
20426
|
+
}
|
|
20427
|
+
/** 期間の見出し(「10/1」「9/28〜10/4 の週」) */
|
|
20428
|
+
function graphPeriodLabel(period) {
|
|
20429
|
+
var range = graphPeriodRange(period);
|
|
20430
|
+
if (!range) return "";
|
|
20431
|
+
if (period.unit === "week") return storyWeekLabel(range.from);
|
|
20432
|
+
var p = range.from.split("-");
|
|
20433
|
+
return parseInt(p[1], 10) + "/" + parseInt(p[2], 10) + " の日";
|
|
20434
|
+
}
|
|
20435
|
+
/** 記録の日付(生まれた・変わった日)がその期間に入るか。日付の無い記録(関係者など)は入らない */
|
|
20436
|
+
function recordInPeriod(rec, range) {
|
|
20437
|
+
var d = rec && rec.date;
|
|
20438
|
+
return !!(d && /^\\d{4}-\\d{2}-\\d{2}$/.test(d) && d >= range.from && d <= range.to);
|
|
20439
|
+
}
|
|
20357
20440
|
|
|
20358
20441
|
function renderGraph() {
|
|
20359
20442
|
// 削除済みを除いたグラフを1本作り、件数・凡例・描画の全部をこれで賄う
|
|
@@ -20365,6 +20448,8 @@ ${calendarAddUrl.toString()}
|
|
|
20365
20448
|
search.value = graphState.q;
|
|
20366
20449
|
filters.appendChild(search);
|
|
20367
20450
|
app.appendChild(filters);
|
|
20451
|
+
var periodCtl = buildGraphPeriod(graph);
|
|
20452
|
+
app.appendChild(periodCtl.row);
|
|
20368
20453
|
|
|
20369
20454
|
var wrap = el("div", { class: "graph-wrap" });
|
|
20370
20455
|
var canvas = el("canvas", { class: "graph-canvas" });
|
|
@@ -20405,10 +20490,67 @@ ${calendarAddUrl.toString()}
|
|
|
20405
20490
|
legend.appendChild(toggle);
|
|
20406
20491
|
app.appendChild(legend);
|
|
20407
20492
|
|
|
20408
|
-
startGraph(canvas, search, graph);
|
|
20493
|
+
startGraph(canvas, search, graph, periodCtl);
|
|
20409
20494
|
}
|
|
20410
20495
|
|
|
20411
|
-
|
|
20496
|
+
/**
|
|
20497
|
+
* 期間で光らせるつまみ(engine #551・オーナー決定 10/1)。「すべて/日/週」と基準の日・前後の送り。
|
|
20498
|
+
* 選ぶと、記録の日付(生まれた・変わった日)がその期間に入る記録と、そのつながりだけが光る。基準の日の既定は、点の中で一番新しい記録の日
|
|
20499
|
+
*/
|
|
20500
|
+
function buildGraphPeriod(graph) {
|
|
20501
|
+
var latest = "";
|
|
20502
|
+
graph.nodes.forEach(function (n) {
|
|
20503
|
+
var rec = n.recordId ? byId[n.recordId] : null;
|
|
20504
|
+
if (rec && rec.date && /^\\d{4}-\\d{2}-\\d{2}$/.test(rec.date) && rec.date > latest) latest = rec.date;
|
|
20505
|
+
});
|
|
20506
|
+
// 日付を持つ記録が 1 件も無い案件は、ビルドの日(日本時間)を基準にする(「日」「週」を選んでも何も起きない、を防ぐ)
|
|
20507
|
+
if (!graphState.period.day) graphState.period.day = latest || SITE.storyToday || (SITE.generatedAt ? String(SITE.generatedAt).slice(0, 10) : null);
|
|
20508
|
+
var row = el("div", { class: "graph-period" });
|
|
20509
|
+
var unit = el("select", { class: "graph-period-unit", "aria-label": "期間の区切り" });
|
|
20510
|
+
[["all", "すべて"], ["day", "日"], ["week", "週"]].forEach(function (o) {
|
|
20511
|
+
var opt = el("option", { value: o[0], text: o[1] });
|
|
20512
|
+
if (graphState.period.unit === o[0]) opt.selected = true;
|
|
20513
|
+
unit.appendChild(opt);
|
|
20514
|
+
});
|
|
20515
|
+
var day = el("input", { type: "date", class: "graph-period-day", "aria-label": "基準の日" });
|
|
20516
|
+
if (graphState.period.day) day.value = graphState.period.day;
|
|
20517
|
+
var prev = el("button", { type: "button", class: "graph-period-prev", text: "◀ 前", title: "前の期間" });
|
|
20518
|
+
var next = el("button", { type: "button", class: "graph-period-next", text: "次 ▶", title: "次の期間" });
|
|
20519
|
+
var label = el("span", { class: "graph-period-text" });
|
|
20520
|
+
row.appendChild(el("span", { text: "期間で光らせる:" }));
|
|
20521
|
+
[unit, day, prev, next, label].forEach(function (c) { row.appendChild(c); });
|
|
20522
|
+
var listeners = [];
|
|
20523
|
+
var shown = null; // 今の期間で光っている記録の数(つながり画面が数えて渡す)
|
|
20524
|
+
function paint() {
|
|
20525
|
+
var off = graphState.period.unit === "all";
|
|
20526
|
+
day.disabled = off; prev.disabled = off; next.disabled = off;
|
|
20527
|
+
label.textContent = off ? "" : graphPeriodLabel(graphState.period) + (shown === null ? "" : "に生まれた・変わった記録 " + shown + " 件");
|
|
20528
|
+
}
|
|
20529
|
+
function sync() {
|
|
20530
|
+
shown = null;
|
|
20531
|
+
paint();
|
|
20532
|
+
listeners.forEach(function (f) { f(); });
|
|
20533
|
+
}
|
|
20534
|
+
function step(sign) {
|
|
20535
|
+
if (!graphState.period.day) return;
|
|
20536
|
+
var days = graphState.period.unit === "week" ? 7 : 1;
|
|
20537
|
+
graphState.period.day = storyDayOfMs(storyUtcMs(graphState.period.day) + sign * days * 86400000);
|
|
20538
|
+
day.value = graphState.period.day;
|
|
20539
|
+
sync();
|
|
20540
|
+
}
|
|
20541
|
+
unit.addEventListener("change", function () { graphState.period.unit = unit.value; sync(); });
|
|
20542
|
+
day.addEventListener("change", function () { if (/^\\d{4}-\\d{2}-\\d{2}$/.test(day.value)) { graphState.period.day = day.value; sync(); } });
|
|
20543
|
+
prev.addEventListener("click", function () { step(-1); });
|
|
20544
|
+
next.addEventListener("click", function () { step(1); });
|
|
20545
|
+
sync();
|
|
20546
|
+
return {
|
|
20547
|
+
row: row,
|
|
20548
|
+
onChange: function (f) { listeners.push(f); },
|
|
20549
|
+
showCount: function (n) { shown = n; paint(); },
|
|
20550
|
+
};
|
|
20551
|
+
}
|
|
20552
|
+
|
|
20553
|
+
function startGraph(canvas, searchInput, graph, periodCtl) {
|
|
20412
20554
|
var allEdges = graph.edges.filter(function (e) {
|
|
20413
20555
|
return graphState.showWeak || !WEAK_RELS[e.rel];
|
|
20414
20556
|
});
|
|
@@ -20421,6 +20563,7 @@ ${calendarAddUrl.toString()}
|
|
|
20421
20563
|
.filter(function (n) { return n.recordId || used[n.id]; })
|
|
20422
20564
|
.map(function (n, i) {
|
|
20423
20565
|
return { id: n.id, label: n.label, group: n.group, recordId: n.recordId, url: n.url,
|
|
20566
|
+
glow: typeof n.glow === "number" ? n.glow : null,
|
|
20424
20567
|
x: typeof n.x === "number" ? n.x : 0, y: typeof n.y === "number" ? n.y : 0,
|
|
20425
20568
|
vx: 0, vy: 0, phase: i * 2.399 };
|
|
20426
20569
|
});
|
|
@@ -20460,24 +20603,37 @@ ${calendarAddUrl.toString()}
|
|
|
20460
20603
|
(inAdj[e.b.id] = inAdj[e.b.id] || []).push(e.a.id);
|
|
20461
20604
|
});
|
|
20462
20605
|
|
|
20606
|
+
// 描き直しの間引き(engine #551 のカクつき対策)。操作・検索・期間の切り替え・ホバーの波の間は毎フレーム描き、
|
|
20607
|
+
// それが済んで止まっている間は、星の瞬き(ゆっくり)のためだけに IDLE_FRAME_MS ごとに描く
|
|
20608
|
+
var IDLE_FRAME_MS = 100;
|
|
20609
|
+
var activeUntil = 0;
|
|
20610
|
+
function poke(ms) { activeUntil = Math.max(activeUntil, performance.now() + ms); }
|
|
20611
|
+
|
|
20463
20612
|
// 検索: 一致ノード(matched)が光り、その隣接(lit)は残し、他は減光する
|
|
20464
20613
|
var matched = {}, lit = {}, searching = false;
|
|
20614
|
+
// 期間(engine #551)を選んでいるときは、その期間に生まれた・変わった記録だけが一致になる(キーワードと両方なら両方を満たす記録)
|
|
20465
20615
|
function applySearch() {
|
|
20466
20616
|
var q = graphState.q.trim().toLowerCase();
|
|
20467
|
-
|
|
20617
|
+
var range = graphPeriodRange(graphState.period.unit === "all" ? null : graphState.period);
|
|
20618
|
+
searching = q.length > 0 || !!range;
|
|
20468
20619
|
matched = {}; lit = {};
|
|
20620
|
+
poke(250);
|
|
20469
20621
|
if (!searching) {
|
|
20470
20622
|
countEl.textContent = "点 " + nodes.length + " / つながり " + edges.length;
|
|
20471
20623
|
return;
|
|
20472
20624
|
}
|
|
20473
20625
|
var count = 0;
|
|
20474
20626
|
nodes.forEach(function (n) {
|
|
20475
|
-
if (n.search.indexOf(q)
|
|
20627
|
+
if (q && n.search.indexOf(q) === -1) return;
|
|
20628
|
+
if (range && !recordInPeriod(n.recordId ? byId[n.recordId] : null, range)) return;
|
|
20629
|
+
matched[n.id] = true; lit[n.id] = true; count++;
|
|
20476
20630
|
});
|
|
20477
20631
|
edges.forEach(function (e) {
|
|
20478
20632
|
if (matched[e.a.id] || matched[e.b.id]) { lit[e.a.id] = true; lit[e.b.id] = true; }
|
|
20479
20633
|
});
|
|
20480
20634
|
countEl.textContent = count + " 件ヒット(つながりも一緒に光っています)";
|
|
20635
|
+
// 期間を選んでいるときは、つまみの行に「◯◯に生まれた・変わった記録 N 件」(キーワードと両方なら両方を満たす数)
|
|
20636
|
+
if (periodCtl && range) periodCtl.showCount(count);
|
|
20481
20637
|
}
|
|
20482
20638
|
applySearch();
|
|
20483
20639
|
if (searchInput) {
|
|
@@ -20486,6 +20642,7 @@ ${calendarAddUrl.toString()}
|
|
|
20486
20642
|
applySearch();
|
|
20487
20643
|
});
|
|
20488
20644
|
}
|
|
20645
|
+
if (periodCtl) periodCtl.onChange(applySearch);
|
|
20489
20646
|
|
|
20490
20647
|
var dpr = window.devicePixelRatio || 1;
|
|
20491
20648
|
var ctx = canvas.getContext("2d");
|
|
@@ -20509,6 +20666,8 @@ ${calendarAddUrl.toString()}
|
|
|
20509
20666
|
var UPSTREAM_HOPS = 1; // 参照元(入辺)を辿る深さ
|
|
20510
20667
|
var SWEEP_STEP_MS = 120; // 波が1段(1ホップ)進むごとの間隔
|
|
20511
20668
|
var SWEEP_GROW_MS = 200; // 辺・点が灯り切るまでの時間
|
|
20669
|
+
// ホバーの波が流れ切るまで毎フレーム描く時間(参照元 1 段+参照先 2 段=順番 3 まで+灯り切る時間に余裕を見る)
|
|
20670
|
+
var HOVER_SWEEP_MS = (DOWNSTREAM_HOPS + UPSTREAM_HOPS + 2) * SWEEP_STEP_MS + SWEEP_GROW_MS + 300;
|
|
20512
20671
|
function computeFitTarget() {
|
|
20513
20672
|
if (!nodes.length) return null;
|
|
20514
20673
|
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
@@ -20536,7 +20695,10 @@ ${calendarAddUrl.toString()}
|
|
|
20536
20695
|
view.x += (t.x - view.x) * 0.18;
|
|
20537
20696
|
view.y += (t.y - view.y) * 0.18;
|
|
20538
20697
|
}
|
|
20698
|
+
// まだ目に見えて動いているか(止まっていれば描き直しを間引ける)
|
|
20699
|
+
return Math.abs(t.scale - view.scale) > view.scale * 0.0005 || Math.abs(t.x - view.x) > 0.1 || Math.abs(t.y - view.y) > 0.1;
|
|
20539
20700
|
}
|
|
20701
|
+
var fitMoving = true;
|
|
20540
20702
|
|
|
20541
20703
|
// ロード時に1回だけ: Goldの中心から外へ「ふわっ」と光が広がるイントロ。
|
|
20542
20704
|
// 金色(#fcd34d)はホバー連鎖・検索の専用色なので、イントロは寒色で別物として見せる。
|
|
@@ -20550,6 +20712,7 @@ ${calendarAddUrl.toString()}
|
|
|
20550
20712
|
resetBtn.addEventListener("click", function () {
|
|
20551
20713
|
autoFit = true; // 全ノードが収まる表示へ滑らかに戻す
|
|
20552
20714
|
resize();
|
|
20715
|
+
poke(250);
|
|
20553
20716
|
if (!reduceMotion) introStart = performance.now(); // 中心から光が広がるイントロを再生
|
|
20554
20717
|
});
|
|
20555
20718
|
canvas.parentNode.appendChild(resetBtn);
|
|
@@ -20668,9 +20831,111 @@ ${calendarAddUrl.toString()}
|
|
|
20668
20831
|
if (alpha > 0.003) alpha *= 0.995;
|
|
20669
20832
|
}
|
|
20670
20833
|
|
|
20834
|
+
// 記録の点の大きさ: 目次と同じ順位から決めた明るさ(glow・0〜1・engine #551)があれば 6〜12、無ければ今までどおり一律 9
|
|
20671
20835
|
function nodeRadius(n) {
|
|
20672
|
-
|
|
20836
|
+
if (!n.recordId) return 6;
|
|
20837
|
+
if (n.glow === null) return 9;
|
|
20838
|
+
var r = 6 + 6 * n.glow;
|
|
20839
|
+
// 一致した点は少なくとも今までの一律の大きさ(9)にする(順位の低い点でも見つけられるように)
|
|
20840
|
+
return matched[n.id] ? Math.max(r, 9) : r;
|
|
20841
|
+
}
|
|
20842
|
+
// 記録の点の明るさの係数(glow が無ければ 1=今までどおり)。目次の上位ほど明るく、下位は沈む
|
|
20843
|
+
// 🔴 検索・期間で一致した点とホバー中の点は順位で沈めない(その日に足されたばかりで順位に載っていない記録が、
|
|
20844
|
+
// 「この期間の記録」を選んだのに一番暗く小さく出る、を防ぐ)
|
|
20845
|
+
function glowAlpha(n) {
|
|
20846
|
+
if (matched[n.id] || n === hovered) return 1;
|
|
20847
|
+
return n.recordId && n.glow !== null ? 0.4 + 0.6 * n.glow : 1;
|
|
20848
|
+
}
|
|
20849
|
+
|
|
20850
|
+
// 点の光(にじみ・shadowBlur)は、点ごとに毎フレーム描くと 700 点で 1 フレーム 30ms を超える(engine #551 のカクつき)。
|
|
20851
|
+
// にじみだけを小さな絵に前もって描いて使い回し、本体の円はその上に正確な半径で描く。
|
|
20852
|
+
// 絵は画面の画素(device px)で作る(shadowBlur は座標変換に影響されない=元の描き方と同じ幅)。
|
|
20853
|
+
// 半径は 0.25px・にじみの幅は小さいうちは 0.5 刻み、大きいと約 4% 刻みに丸めて、絵の数を抑える
|
|
20854
|
+
// (小さい点のにじみの明るさは円の面積に比例するので、1px 刻みでは引いた表示で明るさが ±数十% ずれる)
|
|
20855
|
+
var TAU = Math.PI * 2;
|
|
20856
|
+
// 使い回しの置き場は「最近使った順」の上限つき(上限を超えたら一番古いものから捨てる=ズームし続けても一度に作り直さない)
|
|
20857
|
+
function spriteStore(max) {
|
|
20858
|
+
var m = new Map();
|
|
20859
|
+
return {
|
|
20860
|
+
get: function (k) { var v = m.get(k); if (v) { m.delete(k); m.set(k, v); } return v; },
|
|
20861
|
+
set: function (k, v) { m.set(k, v); if (m.size > max) m.delete(m.keys().next().value); },
|
|
20862
|
+
};
|
|
20863
|
+
}
|
|
20864
|
+
var haloCache = spriteStore(800);
|
|
20865
|
+
// これより大きいにじみ(大きく寄ったとき・画面にわずかな点しか無い)は絵にせず直接描く(大きな絵を溜めない)
|
|
20866
|
+
var HALO_SPRITE_MAX = 512;
|
|
20867
|
+
function haloBlurStep(blur) {
|
|
20868
|
+
if (blur < 0.25) return 0;
|
|
20869
|
+
if (blur <= 12) return Math.round(blur * 2) / 2;
|
|
20870
|
+
return Math.round(12 * Math.pow(1.04, Math.round(Math.log(blur / 12) / Math.log(1.04))) * 2) / 2;
|
|
20871
|
+
}
|
|
20872
|
+
function haloSprite(color, R, blur) {
|
|
20873
|
+
var bq = haloBlurStep(blur);
|
|
20874
|
+
if (!bq) return null;
|
|
20875
|
+
var rq = Math.max(0.25, Math.round(R * 4) / 4);
|
|
20876
|
+
var half = Math.ceil(rq + bq * 1.5 + 2); // にじみ(σ=blur/2)が見える範囲 ≒ 3σ
|
|
20877
|
+
if (half * 2 > HALO_SPRITE_MAX) return false;
|
|
20878
|
+
var key = color + "|" + rq + "|" + bq;
|
|
20879
|
+
var sp = haloCache.get(key);
|
|
20880
|
+
if (sp) return sp;
|
|
20881
|
+
var size = half * 2;
|
|
20882
|
+
var c = document.createElement("canvas");
|
|
20883
|
+
c.width = size; c.height = size;
|
|
20884
|
+
var g = c.getContext("2d");
|
|
20885
|
+
if (!g) return null;
|
|
20886
|
+
// 円は絵の外(左へ size ずらす)に描き、影だけを絵の中央へ落とす=にじみだけが残る
|
|
20887
|
+
g.shadowColor = color;
|
|
20888
|
+
g.shadowBlur = bq;
|
|
20889
|
+
g.shadowOffsetX = size;
|
|
20890
|
+
g.fillStyle = color;
|
|
20891
|
+
g.beginPath();
|
|
20892
|
+
g.arc(half - size, half, rq, 0, TAU);
|
|
20893
|
+
g.fill();
|
|
20894
|
+
sp = { c: c, h: half };
|
|
20895
|
+
haloCache.set(key, sp);
|
|
20896
|
+
return sp;
|
|
20897
|
+
}
|
|
20898
|
+
// X・Y は画素の境目に揃えた値を渡す(半端な位置だと絵の引き伸ばしが入り、1 枚ごとに何倍も重くなる)
|
|
20899
|
+
function drawHalo(color, R, blur, X, Y) {
|
|
20900
|
+
var sp = haloSprite(color, R, blur);
|
|
20901
|
+
if (sp) { ctx.drawImage(sp.c, X - sp.h, Y - sp.h); return; }
|
|
20902
|
+
if (sp === false) {
|
|
20903
|
+
ctx.save();
|
|
20904
|
+
ctx.shadowColor = color;
|
|
20905
|
+
ctx.shadowBlur = blur;
|
|
20906
|
+
ctx.fillStyle = color;
|
|
20907
|
+
ctx.beginPath();
|
|
20908
|
+
ctx.arc(X, Y, R, 0, TAU);
|
|
20909
|
+
ctx.fill();
|
|
20910
|
+
ctx.restore();
|
|
20911
|
+
}
|
|
20673
20912
|
}
|
|
20913
|
+
// ラベルも文字を絵にして使い回す(検索で数百のラベルを毎フレーム文字から描くと 1 フレーム 100ms を超える)。
|
|
20914
|
+
// 文字の大きさは画面の画素で一定(11px)なので、ズームしても同じ絵が使える
|
|
20915
|
+
var labelCache = spriteStore(3000);
|
|
20916
|
+
function labelFont(bold) { return (bold ? "600 " : "") + (11 * dpr) + "px 'Noto Sans JP', sans-serif"; }
|
|
20917
|
+
function drawLabel(text, color, bold, X, baseY) {
|
|
20918
|
+
var key = color + "|" + (bold ? 1 : 0) + "|" + text;
|
|
20919
|
+
var sp = labelCache.get(key);
|
|
20920
|
+
if (!sp) {
|
|
20921
|
+
var font = labelFont(bold);
|
|
20922
|
+
var c = document.createElement("canvas");
|
|
20923
|
+
var g = c.getContext("2d");
|
|
20924
|
+
if (!g) return;
|
|
20925
|
+
g.font = font;
|
|
20926
|
+
var asc = Math.ceil(13 * dpr), desc = Math.ceil(4 * dpr); // 11px の文字の上下+少しの余白
|
|
20927
|
+
c.width = Math.max(1, Math.ceil(g.measureText(text).width) + 2);
|
|
20928
|
+
c.height = asc + desc;
|
|
20929
|
+
g.font = font; // 大きさを変えると描き方の設定が戻るので、もう一度
|
|
20930
|
+
g.fillStyle = color;
|
|
20931
|
+
g.fillText(text, 0, asc);
|
|
20932
|
+
sp = { c: c, asc: asc };
|
|
20933
|
+
labelCache.set(key, sp);
|
|
20934
|
+
}
|
|
20935
|
+
ctx.drawImage(sp.c, Math.round(X), Math.round(baseY - sp.asc));
|
|
20936
|
+
}
|
|
20937
|
+
// ホバーの連鎖(参照先・参照元)は触った点が変わったときだけ辿り直す
|
|
20938
|
+
var linFor = null, linCache = null;
|
|
20674
20939
|
|
|
20675
20940
|
function draw() {
|
|
20676
20941
|
var rect = canvas.getBoundingClientRect();
|
|
@@ -20700,7 +20965,7 @@ ${calendarAddUrl.toString()}
|
|
|
20700
20965
|
ctx.fill();
|
|
20701
20966
|
});
|
|
20702
20967
|
ctx.globalAlpha = 1;
|
|
20703
|
-
|
|
20968
|
+
fitMoving = autoFit ? !!applyFit(false) : false; // 全ノードが収まる位置・倍率へ滑らかに追従
|
|
20704
20969
|
ctx.translate(view.x, view.y);
|
|
20705
20970
|
ctx.scale(view.scale, view.scale);
|
|
20706
20971
|
|
|
@@ -20720,7 +20985,8 @@ ${calendarAddUrl.toString()}
|
|
|
20720
20985
|
}
|
|
20721
20986
|
return hop;
|
|
20722
20987
|
}
|
|
20723
|
-
if (hovered)
|
|
20988
|
+
if (hovered && linFor === hovered) lin = linCache;
|
|
20989
|
+
else if (hovered) {
|
|
20724
20990
|
var downHop = bfsDir(outAdj, DOWNSTREAM_HOPS); // 参照先
|
|
20725
20991
|
var upHop = bfsDir(inAdj, UPSTREAM_HOPS); // 参照元
|
|
20726
20992
|
var maxUp = 0;
|
|
@@ -20735,6 +21001,7 @@ ${calendarAddUrl.toString()}
|
|
|
20735
21001
|
if (id === hovered.id || lin[id] !== undefined) return;
|
|
20736
21002
|
lin[id] = { order: maxUp + downHop[id], hop: downHop[id], dir: "down" };
|
|
20737
21003
|
});
|
|
21004
|
+
linFor = hovered; linCache = lin;
|
|
20738
21005
|
}
|
|
20739
21006
|
function hopAlpha(d) {
|
|
20740
21007
|
return d <= 1 ? 1 : Math.max(0.6, 1 - d * 0.1);
|
|
@@ -20753,7 +21020,13 @@ ${calendarAddUrl.toString()}
|
|
|
20753
21020
|
reference: "rgba(180,196,230,0.32)", source: "rgba(180,196,230,0.32)", link: "rgba(180,196,230,0.22)",
|
|
20754
21021
|
about: "rgba(196,181,253,0.22)",
|
|
20755
21022
|
};
|
|
21023
|
+
// 画面に入っている範囲(世界座標)。両端が同じ側の外にある線は描かない
|
|
21024
|
+
var em = 4 / view.scale; // 線の太さの分の余白(画面で 4px)
|
|
21025
|
+
var wx0 = -view.x / view.scale - em, wy0 = -view.y / view.scale - em;
|
|
21026
|
+
var wx1 = (rect.width - view.x) / view.scale + em, wy1 = (rect.height - view.y) / view.scale + em;
|
|
20756
21027
|
edges.forEach(function (e) {
|
|
21028
|
+
if ((e.a.x < wx0 && e.b.x < wx0) || (e.a.x > wx1 && e.b.x > wx1) ||
|
|
21029
|
+
(e.a.y < wy0 && e.b.y < wy0) || (e.a.y > wy1 && e.b.y > wy1)) return;
|
|
20757
21030
|
// ホバー中: リネージに入った辺だけ、波の順に「order の低い側→高い側」へ伸びながら金色に。
|
|
20758
21031
|
if (hovered) {
|
|
20759
21032
|
var la = lin[e.a.id], lb = lin[e.b.id];
|
|
@@ -20793,6 +21066,10 @@ ${calendarAddUrl.toString()}
|
|
|
20793
21066
|
ctx.stroke();
|
|
20794
21067
|
});
|
|
20795
21068
|
|
|
21069
|
+
// 点は画面の画素の座標で描く(にじみの絵をそのまま置くため)
|
|
21070
|
+
var k = view.scale * dpr, ox = view.x * dpr, oy = view.y * dpr;
|
|
21071
|
+
var cw = canvas.width, ch = canvas.height;
|
|
21072
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
20796
21073
|
nodes.forEach(function (n) {
|
|
20797
21074
|
var style = GROUP_STYLE[n.group] || GROUP_STYLE.phantom;
|
|
20798
21075
|
var alphaN = 1;
|
|
@@ -20815,46 +21092,45 @@ ${calendarAddUrl.toString()}
|
|
|
20815
21092
|
}
|
|
20816
21093
|
var dim = alphaN < 0.3;
|
|
20817
21094
|
var isGold = !!n.recordId;
|
|
21095
|
+
var r0 = nodeRadius(n);
|
|
21096
|
+
// 画面の画素での位置と半径(ラベル・輪・にじみはすべて画素で描く)
|
|
21097
|
+
// 中心は画素の境目に揃える(にじみの絵と本体の円の中心を合わせる。dpr 2 なら 0.25px 以内の差)
|
|
21098
|
+
var X = Math.round(n.x * k + ox), Y = Math.round(n.y * k + oy), R = r0 * k;
|
|
21099
|
+
var inChain = hovered && n !== hovered && lin[n.id] !== undefined;
|
|
21100
|
+
var blur =
|
|
21101
|
+
(isGold ? (n.glow === null || matched[n.id] ? Math.max(16, n.glow === null ? 16 : 6 + 22 * n.glow) : 6 + 22 * n.glow) : 7) * (dim ? 0.3 : 1) * (inChain ? 1.7 : 1) * (1 + introFlash * 1.8);
|
|
21102
|
+
// ラベル: Gold層レコードはズームイン時のみ表示(引いて全体表示中は出さない)。
|
|
21103
|
+
// それ以外はホバーの連鎖に入ったときか検索ヒット時のみ(ズームに関係なく表示)。
|
|
21104
|
+
var showLabel = (n.recordId && view.scale >= LABEL_MIN_SCALE) || (hovered && lin[n.id] !== undefined && sweepRamp(lin[n.id]) > 0.15) || (searching && lit[n.id]);
|
|
21105
|
+
// 画面の外の点は描かない(にじみの届く幅・輪・右に出るラベルの幅を見込む)
|
|
21106
|
+
var reach = R + Math.max(blur, 18) * 1.5 + 6 * dpr;
|
|
21107
|
+
if (X + reach + (showLabel ? 300 * dpr : 0) < 0 || X - reach > cw || Y + reach < 0 || Y - reach > ch) return;
|
|
20818
21108
|
// Gold以外の星はゆっくり瞬く
|
|
20819
21109
|
var tw = isGold ? 1 : 0.72 + 0.28 * (0.5 + 0.5 * Math.sin(now * 0.0017 + n.phase));
|
|
20820
|
-
ctx.globalAlpha = alphaN * tw;
|
|
20821
|
-
var r0 = nodeRadius(n);
|
|
21110
|
+
ctx.globalAlpha = alphaN * tw * glowAlpha(n);
|
|
20822
21111
|
var fill = isGold ? style.color : "#c9d6f2";
|
|
20823
21112
|
// 検索一致ノードとホバー中のノードは金色のリングで発光させる
|
|
20824
21113
|
if ((matched[n.id] || n === hovered) && !dim) {
|
|
20825
|
-
|
|
20826
|
-
ctx.shadowColor = "#fcd34d";
|
|
20827
|
-
ctx.shadowBlur = 18;
|
|
21114
|
+
drawHalo("#fcd34d", R + 2 * dpr, 18, X, Y);
|
|
20828
21115
|
ctx.beginPath();
|
|
20829
|
-
ctx.arc(
|
|
21116
|
+
ctx.arc(X, Y, R + 2 * dpr, 0, TAU);
|
|
20830
21117
|
ctx.fillStyle = fill;
|
|
20831
21118
|
ctx.fill();
|
|
20832
|
-
ctx.restore();
|
|
20833
21119
|
ctx.strokeStyle = "#fcd34d";
|
|
20834
|
-
ctx.lineWidth = 2.5
|
|
21120
|
+
ctx.lineWidth = 2.5 * dpr;
|
|
20835
21121
|
ctx.beginPath();
|
|
20836
|
-
ctx.arc(
|
|
21122
|
+
ctx.arc(X, Y, R + 3.5 * dpr, 0, TAU);
|
|
20837
21123
|
ctx.stroke();
|
|
20838
21124
|
}
|
|
20839
21125
|
// 星本体(色のにじみ=グロー付き)
|
|
20840
|
-
|
|
20841
|
-
ctx.save();
|
|
20842
|
-
ctx.shadowColor =
|
|
20843
|
-
introFlash > 0.01 ? "#bcd0ff" : isGold ? style.color : "#9fb4e8";
|
|
20844
|
-
ctx.shadowBlur =
|
|
20845
|
-
(isGold ? 16 : 7) * (dim ? 0.3 : 1) * (inChain ? 1.7 : 1) * (1 + introFlash * 1.8);
|
|
21126
|
+
drawHalo(introFlash > 0.01 ? "#bcd0ff" : isGold ? style.color : "#9fb4e8", R, blur, X, Y);
|
|
20846
21127
|
ctx.beginPath();
|
|
20847
|
-
ctx.arc(
|
|
21128
|
+
ctx.arc(X, Y, R, 0, TAU);
|
|
20848
21129
|
ctx.fillStyle = fill;
|
|
20849
21130
|
ctx.fill();
|
|
20850
|
-
|
|
20851
|
-
// ラベル: Gold層レコードはズームイン時のみ表示(引いて全体表示中は出さない)。
|
|
20852
|
-
// それ以外はホバーの連鎖に入ったときか検索ヒット時のみ(ズームに関係なく表示)。
|
|
20853
|
-
if ((n.recordId && view.scale >= LABEL_MIN_SCALE) || (hovered && lin[n.id] !== undefined && sweepRamp(lin[n.id]) > 0.15) || (searching && lit[n.id])) {
|
|
20854
|
-
ctx.fillStyle = dim ? "#3d4763" : matched[n.id] ? "#fde68a" : isGold ? "#e7edfb" : "#9fb0d6";
|
|
20855
|
-
ctx.font = (matched[n.id] ? "600 " : "") + (11 / view.scale) + "px 'Noto Sans JP', sans-serif";
|
|
21131
|
+
if (showLabel) {
|
|
20856
21132
|
var label = n.label.length > 24 ? n.label.slice(0, 23) + "…" : n.label;
|
|
20857
|
-
|
|
21133
|
+
drawLabel(label, dim ? "#3d4763" : matched[n.id] ? "#fde68a" : isGold ? "#e7edfb" : "#9fb0d6", !!matched[n.id], X + R + 4 * dpr, Y + 4 * dpr);
|
|
20858
21134
|
}
|
|
20859
21135
|
});
|
|
20860
21136
|
ctx.globalAlpha = 1;
|
|
@@ -20892,7 +21168,7 @@ ${calendarAddUrl.toString()}
|
|
|
20892
21168
|
return;
|
|
20893
21169
|
}
|
|
20894
21170
|
var nh = nodeAt(ev.clientX, ev.clientY);
|
|
20895
|
-
if (nh !== hovered) { hovered = nh; hoverStart = nh ? performance.now() : 0; } // 別ノードでスイープ再生
|
|
21171
|
+
if (nh !== hovered) { hovered = nh; hoverStart = nh ? performance.now() : 0; poke(HOVER_SWEEP_MS); } // 別ノードでスイープ再生
|
|
20896
21172
|
canvas.style.cursor = hovered ? "pointer" : "grab";
|
|
20897
21173
|
});
|
|
20898
21174
|
canvas.addEventListener("mousedown", function (ev) {
|
|
@@ -20924,6 +21200,7 @@ ${calendarAddUrl.toString()}
|
|
|
20924
21200
|
view.x = mx - (mx - view.x) * factor;
|
|
20925
21201
|
view.y = my - (my - view.y) * factor;
|
|
20926
21202
|
view.scale *= factor;
|
|
21203
|
+
poke(250);
|
|
20927
21204
|
}, { passive: false });
|
|
20928
21205
|
|
|
20929
21206
|
// ビルド時に座標計算済みなら、ウォームアップ(重い同期計算)を完全に省略する=開いた瞬間ゼロ待ち。
|
|
@@ -20942,10 +21219,17 @@ ${calendarAddUrl.toString()}
|
|
|
20942
21219
|
if (!running || !document.body.contains(canvas)) { running = false; return; }
|
|
20943
21220
|
// 収束後(alpha減衰しきり・無操作)は O(n²) の物理計算 tick を止める。これが「固まり続ける」主因。
|
|
20944
21221
|
// ドラッグ/パン中は動かす必要があるので回す。描画(draw)はホバー演出・イントロのため毎フレーム続ける。
|
|
20945
|
-
|
|
20946
|
-
|
|
21222
|
+
var moving = alpha > 0.004 || draggingNode || panning;
|
|
21223
|
+
if (moving) tick();
|
|
21224
|
+
// 動いている・操作の直後・イントロ・自動フレーミングの途中は毎フレーム描く。止まっている間は瞬きのためだけに間引いて描く(engine #551)
|
|
21225
|
+
var now = performance.now();
|
|
21226
|
+
if (moving || introStart || fitMoving || now < activeUntil || now - lastDrawAt >= IDLE_FRAME_MS) {
|
|
21227
|
+
lastDrawAt = now;
|
|
21228
|
+
draw();
|
|
21229
|
+
}
|
|
20947
21230
|
requestAnimationFrame(loop);
|
|
20948
21231
|
}
|
|
21232
|
+
var lastDrawAt = 0;
|
|
20949
21233
|
loop();
|
|
20950
21234
|
}
|
|
20951
21235
|
|
|
@@ -20979,7 +21263,7 @@ ${calendarAddUrl.toString()}
|
|
|
20979
21263
|
// 警告帯が出ない記録の削除の口はここ(編集の行の末尾)。draft には出さない
|
|
20980
21264
|
var delMsgD = attachDeleteToEditRow(d, editRowD);
|
|
20981
21265
|
head.appendChild(el("div", { class: "detail-actions" }, [
|
|
20982
|
-
el("div", { class: "detail-actions-row" }, [ traceBtn ]),
|
|
21266
|
+
el("div", { class: "detail-actions-row" }, [ traceBtn, storyLinkFor(d) ]),
|
|
20983
21267
|
editRowD.children.length ? editRowD : null,
|
|
20984
21268
|
delMsgD,
|
|
20985
21269
|
]));
|
|
@@ -21493,6 +21777,8 @@ ${calendarAddUrl.toString()}
|
|
|
21493
21777
|
var validSub = false;
|
|
21494
21778
|
STORY_SUBS.forEach(function (s) { if (s[0] === sub) validSub = true; });
|
|
21495
21779
|
storySub = validSub ? sub : STORY_SUB_DEFAULT;
|
|
21780
|
+
var focusKey = storySub === "weekly" ? getParam("week") : storySub === "daily" ? getParam("day") : null;
|
|
21781
|
+
storyFocus = focusKey && /^\\d{4}-\\d{2}-\\d{2}$/.test(focusKey) ? focusKey : null;
|
|
21496
21782
|
}
|
|
21497
21783
|
renderList();
|
|
21498
21784
|
}
|
package/package.json
CHANGED