|
123 | 123 | .controls .rst:hover{background:rgba(255,255,255,.08);color:var(--ink)} |
124 | 124 | .controls .sepc{width:1px;height:18px;background:var(--line)} |
125 | 125 |
|
| 126 | +/* ---- provenance tree (outline) ---- */ |
| 127 | +.tree{ |
| 128 | + position:fixed;left:0;top:60px;right:0;bottom:70px;z-index:3; |
| 129 | + overflow:auto;padding:14px 30px 30px; |
| 130 | + font:13px/1.65 var(--mono);color:var(--soft); |
| 131 | +} |
| 132 | +.tree .t-row{ |
| 133 | + display:flex;align-items:center;gap:8px;padding:3px 9px;border-radius:7px; |
| 134 | + cursor:pointer;white-space:nowrap;transition:background .12s ease,color .12s ease; |
| 135 | +} |
| 136 | +.tree .t-row:hover{background:var(--glass);color:var(--ink)} |
| 137 | +.tree .t-caret{width:11px;flex:none;color:var(--mut);font-size:.62rem;text-align:center;transition:transform .15s ease} |
| 138 | +.tree .t-dot{width:9px;height:9px;border-radius:50%;flex:none; |
| 139 | + background:rgb(var(--c,148,163,184));box-shadow:0 0 7px rgba(var(--c,148,163,184),.55)} |
| 140 | +.tree .t-ic{flex:none;opacity:.85} |
| 141 | +.tree .t-ct{margin-left:9px;color:var(--mut);font-size:.7rem} |
| 142 | +.tree .t-doc{color:var(--ink);font-weight:600} |
| 143 | +.tree .t-leaf{color:var(--soft)} |
| 144 | +.tree .t-children{margin-left:15px;padding-left:9px;border-left:1px solid var(--line)} |
| 145 | +.tree .t-group.collapsed > .t-children{display:none} |
| 146 | +.tree .t-group.collapsed > .t-row .t-caret{transform:rotate(-90deg)} |
| 147 | +.tree .t-docgroup{margin:9px 0} |
| 148 | +.tree .t-empty{color:var(--mut);padding:24px} |
| 149 | +.tree::-webkit-scrollbar{width:9px} |
| 150 | +.tree::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:8px} |
| 151 | + |
126 | 152 | /* ---- glass inspector panel ---- */ |
127 | 153 | .panel{ |
128 | 154 | position:fixed;top:0;right:0;z-index:6;width:360px;max-width:88vw;height:100%; |
|
173 | 199 | <div class="grain"></div> |
174 | 200 | <div class="vignette"></div> |
175 | 201 | <canvas id="c"></canvas> |
| 202 | +<div class="tree" id="tree"></div> |
176 | 203 |
|
177 | 204 | <div class="brand">openkb<small>knowledge graph</small></div> |
178 | 205 |
|
|
190 | 217 | <div class="controls"> |
191 | 218 | <button class="rst" id="mode" title="switch between mind-map and 3D graph">3D graph</button> |
192 | 219 | <span class="sepc"></span> |
193 | | - <label>spacing <input id="spread" type="range" min="0.5" max="3" step="0.1" value="1"></label> |
| 220 | + <label id="spacingCtl">spacing <input id="spread" type="range" min="0.5" max="3" step="0.1" value="1"></label> |
194 | 221 | <button class="rst" id="reset" title="reset view, spacing & focus">reset</button> |
195 | 222 | </div> |
196 | 223 |
|
|
226 | 253 | r: 3.5 + Math.min(7, ((n.in || 0) + (n.out || 0)) * 0.42), // small crisp nodes, scaled by degree |
227 | 254 | col: colorOf(n.type), // cached color → no per-frame lookup |
228 | 255 | alpha: 1, // per-node fade (legend toggle) |
229 | | - sx:0, sy:0, rz:0, pp:1, hl:1, pinned:false, // projection cache + smoothed highlight + pinned (drag to pull out) |
230 | | - tx:0, ty:0, mapShow:true // mind-map: radial target position + whether shown in current focus |
| 256 | + sx:0, sy:0, rz:0, pp:1, hl:1, pinned:false // projection cache + smoothed highlight + pinned (drag to pull out) |
231 | 257 | })); |
232 | 258 | const byId = Object.fromEntries(nodes.map(n => [n.id, n])); |
233 | 259 | const edges = GRAPH.edges.filter(e => byId[e.source] && byId[e.target]); |
|
248 | 274 | let scale = DEF_SCALE, panX = 0, panY = 0; |
249 | 275 | let yaw = DEF_YAW, pitch = DEF_PITCH; // 3D orbit angles — drag the background to rotate |
250 | 276 | let orbiting = false, oStartX = 0, oStartY = 0, yawStart = 0, pitchStart = 0; |
251 | | -let panOrigX = 0, panOrigY = 0; // bg-drag pan origin (mind-map mode) |
252 | 277 | let didDrag = false; |
253 | 278 | let alpha = 1; // simulated-annealing "temperature"; cools to 0 so motion stops |
254 | 279 | let spread = 1; // spacing knob: >1 spreads nodes apart, <1 packs them tighter |
255 | | -let mode = "map"; // "map" = radial mind-map (focus + neighbors), "graph" = 3D force layout |
256 | | -let focusId = null; // centered node in mind-map mode |
257 | | -let pendingFocus = null; // node pressed in map mode, awaiting click-release |
| 280 | +let mode = "tree"; // "tree" = provenance outline (default), "graph" = 3D force layout |
258 | 281 |
|
259 | 282 | document.getElementById("ncount").textContent = nodes.length; |
260 | 283 | document.getElementById("ecount").textContent = edges.length; |
|
280 | 303 |
|
281 | 304 | /* ===================== physics (adapted from okf-spec.html step()) ===================== */ |
282 | 305 | const VMAX = 9; // per-frame speed cap → no violent swings, even on a dense hub |
283 | | -const alphaTarget = n => (typeVisible(n.type) && (mode === "graph" || n.mapShow)) ? 1 : 0; |
| 306 | +const alphaTarget = n => typeVisible(n.type) ? 1 : 0; |
284 | 307 | function step(){ |
285 | | - if(mode === "map"){ stepMap(); return; } |
286 | 308 | for(let i=0;i<nodes.length;i++){ |
287 | 309 | const a = nodes[i]; |
288 | 310 | if(!visible(a)) continue; |
|
322 | 344 | alpha += (0 - alpha) * 0.02; // anneal: forces fade → the layout settles and stops |
323 | 345 | } |
324 | 346 |
|
325 | | -/* ===================== mind-map (radial focus layout) ===================== */ |
326 | | -// Place the focus node at the origin and fan its direct neighbours out on 1–2 rings. |
327 | | -// Only focus + neighbours are shown; everything else fades out. Clicking a neighbour re-focuses. |
328 | | -function layoutMap(){ |
329 | | - const f = byId[focusId]; |
330 | | - if(!f){ return; } |
331 | | - nodes.forEach(n => { n.mapShow = false; }); |
332 | | - f.mapShow = true; f.tx = 0; f.ty = 0; |
333 | | - const nbrs = [...adj[focusId]].filter(id => byId[id] && typeVisible(byId[id].type)); |
334 | | - const N = nbrs.length; |
335 | | - const twoRings = N > 18; |
336 | | - const split = twoRings ? Math.ceil(N * 0.42) : N; // inner-ring count |
337 | | - nbrs.forEach((id, i) => { |
338 | | - const ring = (twoRings && i >= split) ? 1 : 0; |
339 | | - const start = ring === 0 ? 0 : split; |
340 | | - const count = ring === 0 ? Math.min(split, N) : (N - split); |
341 | | - const idx = i - start; |
342 | | - const R = (ring === 0 ? 215 : 380) * (0.7 + 0.3 * spread); // spacing slider stretches the rings too |
343 | | - const ang = (idx / Math.max(1, count)) * TAU - Math.PI / 2 + ring * 0.5; |
344 | | - const nb = byId[id]; |
345 | | - nb.mapShow = true; |
346 | | - nb.tx = Math.cos(ang) * R; |
347 | | - nb.ty = Math.sin(ang) * R; |
| 347 | +/* ===================== provenance tree (document → summary → concept/entity) ===================== */ |
| 348 | +// The wiki's real hierarchy lives in the frontmatter: each summary's `sources` is its origin |
| 349 | +// document (full_text), and each concept/entity's `sources` are the summaries it was drawn from. |
| 350 | +// Render that as a collapsible indented outline — a true tree, not the dense wikilink mesh. |
| 351 | +const treeEl = document.getElementById("tree"); |
| 352 | +const spacingCtl = document.getElementById("spacingCtl"); |
| 353 | +let treeQuery = ""; |
| 354 | + |
| 355 | +// concept/entity `sources` look like "summaries/foo.md"; summary node ids are "summaries/foo" → strip .md |
| 356 | +const resolveSrc = s => { |
| 357 | + const id = String(s).replace(/\.md$/i, ""); |
| 358 | + return (byId[id] && byId[id].id.startsWith("summaries/")) ? id : null; |
| 359 | +}; |
| 360 | +function buildTree(){ |
| 361 | + const childrenOf = {}; // summaryId → [concept/entity node] |
| 362 | + nodes.forEach(n => { if(n.id.startsWith("summaries/")) childrenOf[n.id] = []; }); |
| 363 | + const unlinked = []; // concepts/entities with no resolvable summary parent |
| 364 | + nodes.forEach(n => { |
| 365 | + if(n.id.startsWith("summaries/")) return; |
| 366 | + const parents = [...new Set((n.sources || []).map(resolveSrc).filter(Boolean))]; |
| 367 | + if(parents.length) parents.forEach(p => childrenOf[p].push(n)); |
| 368 | + else unlinked.push(n); |
348 | 369 | }); |
349 | | -} |
350 | | -function stepMap(){ |
351 | | - for(const n of nodes){ |
352 | | - n.alpha += (alphaTarget(n) - n.alpha) * 0.14; |
353 | | - if(n.mapShow){ // ease toward radial target (flat: z→0) |
354 | | - n.x += (n.tx - n.x) * 0.16; |
355 | | - n.y += (n.ty - n.y) * 0.16; |
356 | | - n.z += (0 - n.z) * 0.16; |
357 | | - } |
| 370 | + |
| 371 | + const q = treeQuery.toLowerCase(); |
| 372 | + const matches = n => (!q || n.label.toLowerCase().includes(q) || n.id.toLowerCase().includes(q)) && typeVisible(n.type); |
| 373 | + const byLabel = (a, b) => a.label.localeCompare(b.label); |
| 374 | + const leafRow = n => matches(n) |
| 375 | + ? `<div class="t-row t-leaf" data-id="${esc(n.id)}" style="--c:${colorOf(n.type)}"><span class="t-dot"></span>${esc(n.label)}</div>` |
| 376 | + : ""; |
| 377 | + |
| 378 | + // each summary = one ingested document (root); ordered by how many concepts it produced |
| 379 | + const summaries = nodes.filter(n => n.id.startsWith("summaries/")) |
| 380 | + .sort((a, b) => (childrenOf[b.id].length - childrenOf[a.id].length) || byLabel(a, b)); |
| 381 | + let html = ""; |
| 382 | + summaries.forEach(s => { |
| 383 | + const kids = (childrenOf[s.id] || []).slice().sort(byLabel); |
| 384 | + const kidRows = kids.map(leafRow).filter(Boolean).join(""); |
| 385 | + if(!(matches(s) || kidRows)) return; |
| 386 | + html += `<div class="t-group t-docgroup"><div class="t-row t-doc" data-id="${esc(s.id)}" style="--c:${colorOf(s.type)}">` |
| 387 | + + `<span class="t-caret">▾</span><span class="t-ic">📄</span>${esc(s.label)}<span class="t-ct">${kids.length}</span></div>` |
| 388 | + + `<div class="t-children">${kidRows}</div></div>`; |
| 389 | + }); |
| 390 | + const ul = unlinked.slice().sort(byLabel).map(leafRow).filter(Boolean).join(""); |
| 391 | + if(ul){ |
| 392 | + html += `<div class="t-group t-docgroup"><div class="t-row t-doc">` |
| 393 | + + `<span class="t-caret">▾</span><span class="t-ic">○</span>unsourced<span class="t-ct">${unlinked.length}</span></div>` |
| 394 | + + `<div class="t-children">${ul}</div></div>`; |
358 | 395 | } |
359 | | - alpha += (0 - alpha) * 0.02; // reuse the anneal gate as a "still animating" timer |
360 | | -} |
361 | | -function setFocus(id){ |
362 | | - if(!byId[id]) return; |
363 | | - focusId = id; |
364 | | - layoutMap(); |
365 | | - openPanel(byId[id]); // show the centered node's details + links |
366 | | - alpha = 1; // animate the re-layout |
| 396 | + treeEl.innerHTML = html || '<div class="t-empty">Nothing to show.</div>'; |
| 397 | + |
| 398 | + treeEl.querySelectorAll(".t-caret").forEach(c => c.onclick = e => { |
| 399 | + e.stopPropagation(); |
| 400 | + c.closest(".t-group").classList.toggle("collapsed"); |
| 401 | + }); |
| 402 | + treeEl.querySelectorAll(".t-row").forEach(r => r.onclick = () => { |
| 403 | + const id = r.dataset.id; |
| 404 | + if(id && byId[id]) openPanel(byId[id]); // real node → inspect |
| 405 | + else { const g = r.closest(".t-group"); if(g) g.classList.toggle("collapsed"); } // doc row → fold |
| 406 | + }); |
367 | 407 | } |
368 | 408 | function setMode(m){ |
369 | 409 | mode = m; |
370 | | - if(m === "map"){ |
371 | | - yaw = 0; pitch = 0; scale = 1.0; panX = 0; panY = 0; |
372 | | - if(!focusId) focusId = byDeg[0]; |
373 | | - layoutMap(); |
374 | | - } else { |
375 | | - yaw = DEF_YAW; pitch = DEF_PITCH; scale = DEF_SCALE; panX = 0; panY = 0; |
376 | | - closePanel(); seed(); // fresh 3D sphere |
377 | | - } |
378 | | - hover = null; alpha = 1; |
379 | | - modeBtn.textContent = (m === "map") ? "3D graph" : "mind-map"; |
| 410 | + const tree = (m === "tree"); |
| 411 | + canvas.style.display = tree ? "none" : "block"; |
| 412 | + treeEl.style.display = tree ? "block" : "none"; |
| 413 | + spacingCtl.style.display = tree ? "none" : "flex"; // spacing only affects the force layout |
| 414 | + hover = null; closePanel(); |
| 415 | + if(tree){ buildTree(); } |
| 416 | + else { yaw = DEF_YAW; pitch = DEF_PITCH; scale = DEF_SCALE; panX = 0; panY = 0; seed(); alpha = 1; } |
| 417 | + modeBtn.textContent = tree ? "3D graph" : "tree"; |
380 | 418 | updateHint(); |
381 | 419 | } |
382 | 420 | function updateHint(){ |
383 | | - hintEl.innerHTML = (mode === "map") |
384 | | - ? '<kbd>click</kbd> focus & expand <kbd>drag bg</kbd> pan <kbd>scroll</kbd> zoom' |
| 421 | + hintEl.innerHTML = (mode === "tree") |
| 422 | + ? '<kbd>click ▾</kbd> fold / unfold <kbd>click row</kbd> details' |
385 | 423 | : '<kbd>scroll</kbd> zoom <kbd>drag bg</kbd> rotate <kbd>click</kbd> inspect<br>' |
386 | 424 | + '<kbd>drag node</kbd> pull out & pin <kbd>dbl-click</kbd> release'; |
387 | 425 | } |
|
442 | 480 |
|
443 | 481 | /* ===================== rendering ===================== */ |
444 | 482 | function drawEdge(e, t){ |
445 | | - // mind-map: draw only the spokes radiating from the focus node (a clean tree, not the mesh) |
446 | | - if(mode === "map" && focusId && e.source !== focusId && e.target !== focusId) return; |
447 | 483 | const a = byId[e.source], b = byId[e.target]; |
448 | 484 | const al = Math.min(a.alpha, b.alpha); |
449 | 485 | if(al < 0.02) return; |
|
514 | 550 | ctx.shadowBlur = 0; |
515 | 551 | // (flat discs — no glossy core highlight) |
516 | 552 | // label — declutter: hubs at rest; hovered node + neighbors; selection; zoomed in; near depth only |
517 | | - const showLabel = (mode === "map") || (scale > 1.4) || (n.id === selected) || (hover ? hl > 0.55 : labelIds.has(n.id)); |
| 553 | + const showLabel = (scale > 1.4) || (n.id === selected) || (hover ? hl > 0.55 : labelIds.has(n.id)); |
518 | 554 | if(showLabel && fog > 0.55){ |
519 | 555 | ctx.fillStyle = `rgba(238,242,247,${0.4 + 0.55*hl})`; |
520 | 556 | ctx.font = `600 11px ui-monospace,monospace`; |
|
527 | 563 | } |
528 | 564 |
|
529 | 565 | function frame(t){ |
530 | | - if(alpha > 0.005) step(); // once cooled, stop integrating — the graph holds still |
531 | | - project(); // 3D → screen coords for this frame |
532 | | - // ease each node's highlight toward its target so hover brightens/dims smoothly (no flash) |
533 | | - const focus = hover || selected; // hovering OR an open inspector keeps that node's connections lit |
534 | | - for(const n of nodes){ |
535 | | - const near = !focus || n.id===focus || adj[focus].has(n.id); |
536 | | - n.hl += ((near ? 1 : 0.22) - n.hl) * 0.12; |
| 566 | + if(mode === "graph"){ // tree mode is DOM — the canvas idles |
| 567 | + if(alpha > 0.005) step(); // once cooled, stop integrating — the graph holds still |
| 568 | + project(); // 3D → screen coords for this frame |
| 569 | + // ease each node's highlight toward its target so hover brightens/dims smoothly (no flash) |
| 570 | + const focus = hover || selected; // hovering OR an open inspector keeps that node's connections lit |
| 571 | + for(const n of nodes){ |
| 572 | + const near = !focus || n.id===focus || adj[focus].has(n.id); |
| 573 | + n.hl += ((near ? 1 : 0.22) - n.hl) * 0.12; |
| 574 | + } |
| 575 | + ctx.setTransform(DPR,0,0,DPR,0,0); |
| 576 | + ctx.clearRect(0,0,W(),H()); |
| 577 | + edges.forEach(e => drawEdge(e, t)); |
| 578 | + // draw nodes far → near so nearer discs occlude farther ones (painter's algorithm) |
| 579 | + nodes.filter(visible).sort((a,b) => b.rz - a.rz).forEach(n => drawNode(n, t)); |
537 | 580 | } |
538 | | - ctx.setTransform(DPR,0,0,DPR,0,0); |
539 | | - ctx.clearRect(0,0,W(),H()); |
540 | | - edges.forEach(e => drawEdge(e, t)); |
541 | | - // draw nodes far → near so nearer discs occlude farther ones (painter's algorithm) |
542 | | - nodes.filter(visible).sort((a,b) => b.rz - a.rz).forEach(n => drawNode(n, t)); |
543 | 581 | requestAnimationFrame(frame); |
544 | 582 | } |
545 | 583 |
|
|
554 | 592 | alpha = Math.max(alpha, 0.5); // reheat so neighbors re-settle around the dragged node |
555 | 593 | return; |
556 | 594 | } |
557 | | - if(orbiting){ |
558 | | - if(mode === "map"){ // flat map → drag background to pan |
559 | | - panX = panOrigX + (e.offsetX - oStartX); |
560 | | - panY = panOrigY + (e.offsetY - oStartY); |
561 | | - } else { // 3D → drag background to orbit |
562 | | - yaw = yawStart + (e.offsetX - oStartX) * 0.0045; |
563 | | - pitch = Math.max(-1.45, Math.min(1.45, pitchStart - (e.offsetY - oStartY) * 0.0045)); |
564 | | - } |
| 595 | + if(orbiting){ // 3D → drag background to orbit |
| 596 | + yaw = yawStart + (e.offsetX - oStartX) * 0.0045; |
| 597 | + pitch = Math.max(-1.45, Math.min(1.45, pitchStart - (e.offsetY - oStartY) * 0.0045)); |
565 | 598 | didDrag = true; return; |
566 | 599 | } |
567 | 600 | const n = pick(e.offsetX, e.offsetY); |
|
570 | 603 | canvas.addEventListener("mousedown", e => { |
571 | 604 | const n = pick(e.offsetX, e.offsetY); |
572 | 605 | didDrag = false; |
573 | | - oStartX = e.offsetX; oStartY = e.offsetY; // press origin — for the drag threshold and for orbit/pan |
574 | | - if(mode === "map"){ |
575 | | - if(n){ pendingFocus = n; hover = n.id; } |
576 | | - else { orbiting = true; panOrigX = panX; panOrigY = panY; canvas.style.cursor = "grabbing"; } |
577 | | - return; |
578 | | - } |
| 606 | + oStartX = e.offsetX; oStartY = e.offsetY; // press origin — for the drag threshold and for orbit |
579 | 607 | if(n){ drag = n; hover = n.id; canvas.style.cursor = "grabbing"; } |
580 | 608 | else { orbiting = true; yawStart = yaw; pitchStart = pitch; canvas.style.cursor = "grabbing"; } |
581 | 609 | }); |
582 | 610 | window.addEventListener("mouseup", e => { |
583 | | - if(mode === "map"){ |
584 | | - if(pendingFocus && !didDrag) setFocus(pendingFocus.id); // click a node → re-center on it |
585 | | - } else if(drag && !didDrag){ openPanel(drag); } // click (no drag) → inspect |
| 611 | + if(drag && !didDrag){ openPanel(drag); } // click (no drag) → inspect |
586 | 612 | else if(drag && didDrag){ drag.pinned = true; alpha = Math.max(alpha, 0.5); } // pulled out → pin it |
587 | | - drag = null; orbiting = false; pendingFocus = null; |
| 613 | + drag = null; orbiting = false; |
588 | 614 | canvas.style.cursor = "grab"; |
589 | 615 | }); |
590 | 616 | canvas.addEventListener("dblclick", e => { |
|
621 | 647 | panel.querySelectorAll("[data-n]").forEach(a => a.onclick = () => { |
622 | 648 | const m = byId[a.dataset.n]; |
623 | 649 | if(!m) return; |
624 | | - if(mode === "map"){ setFocus(m.id); } // navigate the mind-map by clicking a link |
625 | | - else { hover = m.id; centerOn(m); openPanel(m); } |
| 650 | + if(mode === "graph"){ hover = m.id; centerOn(m); } // graph: fly to the node |
| 651 | + openPanel(m); // both modes: show its details |
626 | 652 | }); |
627 | 653 | } |
628 | 654 | function closePanel(){ panel.classList.remove("open"); selected = null; } |
|
640 | 666 | row.onclick = () => { |
641 | 667 | row.classList.toggle("off"); |
642 | 668 | if(hiddenTypes.has(t)) hiddenTypes.delete(t); else hiddenTypes.add(t); |
643 | | - alpha = Math.max(alpha, 0.4); // reheat so the remaining nodes re-pack into the freed space |
| 669 | + if(mode === "tree") buildTree(); // re-filter the outline |
| 670 | + else alpha = Math.max(alpha, 0.4); // reheat so the remaining nodes re-pack into the freed space |
644 | 671 | }; |
645 | 672 | legend.appendChild(row); |
646 | 673 | }); |
647 | 674 |
|
648 | 675 | /* ===================== search ===================== */ |
649 | 676 | search.addEventListener("input", e => { |
650 | | - const q = e.target.value.toLowerCase().trim(); |
| 677 | + const raw = e.target.value.trim(); |
| 678 | + if(mode === "tree"){ treeQuery = raw; buildTree(); return; } // filter the outline |
| 679 | + const q = raw.toLowerCase(); |
651 | 680 | if(!q){ hover = null; return; } |
652 | 681 | const m = nodes.find(n => n.label.toLowerCase().includes(q)) |
653 | 682 | || nodes.find(n => n.id.toLowerCase().includes(q)); |
|
661 | 690 | }); |
662 | 691 |
|
663 | 692 | /* ===================== controls (mode toggle + spacing slider + reset) ===================== */ |
664 | | -modeBtn.addEventListener("click", () => setMode(mode === "map" ? "graph" : "map")); |
| 693 | +modeBtn.addEventListener("click", () => setMode(mode === "tree" ? "graph" : "tree")); |
665 | 694 |
|
666 | 695 | const spreadEl = document.getElementById("spread"); |
667 | 696 | spreadEl.addEventListener("input", e => { |
668 | 697 | spread = parseFloat(e.target.value) || 1; |
669 | | - if(mode === "map") layoutMap(); // spacing stretches the rings |
670 | 698 | alpha = Math.max(alpha, 0.6); // reheat so the layout re-settles at the new spacing |
671 | 699 | }); |
672 | 700 | document.getElementById("reset").addEventListener("click", () => { |
673 | | - spread = 1; spreadEl.value = "1"; |
674 | 701 | hover = null; closePanel(); |
675 | | - if(mode === "map"){ |
676 | | - scale = 1.0; panX = 0; panY = 0; focusId = byDeg[0]; layoutMap(); // back to the top hub |
| 702 | + if(mode === "tree"){ |
| 703 | + treeQuery = ""; search.value = ""; buildTree(); treeEl.scrollTop = 0; // collapse-all back to top |
677 | 704 | } else { |
| 705 | + spread = 1; spreadEl.value = "1"; |
678 | 706 | scale = DEF_SCALE; panX = 0; panY = 0; yaw = DEF_YAW; pitch = DEF_PITCH; // view |
679 | 707 | nodes.forEach(n => n.pinned = false); // release pinned |
| 708 | + alpha = Math.max(alpha, 0.9); // re-settle from the reset state |
680 | 709 | } |
681 | | - alpha = Math.max(alpha, 0.9); // re-settle from the reset state |
682 | 710 | }); |
683 | 711 |
|
684 | 712 | /* ===================== boot ===================== */ |
685 | 713 | window.addEventListener("resize", size); |
686 | | -size(); |
687 | | -focusId = byDeg[0]; layoutMap(); // open on the most-connected node as the mind-map root |
688 | | -modeBtn.textContent = "3D graph"; // button switches TO the other mode |
689 | | -updateHint(); |
| 714 | +size(); seed(); // seed positions so a later switch to 3D has a layout to settle |
| 715 | +setMode("tree"); // open on the readable provenance outline |
690 | 716 | requestAnimationFrame(frame); |
691 | 717 | </script> |
692 | 718 | </body> |
|
0 commit comments