local-knowledge-graph 1.10.1 → 1.10.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-knowledge-graph",
3
- "version": "1.10.1",
3
+ "version": "1.10.2",
4
4
  "description": "本地知识图谱整合器:3D可视化 + AI对话建图 + 智能检索 + Git回溯 + RDF合规,全流程本地运行",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -298,7 +298,7 @@ function makeRelLine(pa, pb, arc, color, dashed, opacity, dashSize, gapSize) {
298
298
  return line;
299
299
  }
300
300
 
301
- // 每帧根据节点最新位置刷新弧线几何
301
+ // 每帧根据节点最新位置刷新弧线几何(标签为HTML层,弧顶点存 l.arcTop 供屏幕投影)
302
302
  const _arcDir = new THREE.Vector3(), _arcOff = new THREE.Vector3(), _arcTmp = new THREE.Vector3();
303
303
  function updateRelLine(l) {
304
304
  if (!l.arc) {
@@ -307,7 +307,7 @@ function updateRelLine(l) {
307
307
  posAttr.setXYZ(1, l.b.pos.x, l.b.pos.y, l.b.pos.z);
308
308
  posAttr.needsUpdate = true;
309
309
  if (l.dashed) l.line.computeLineDistances();
310
- l.label.position.copy(l.a.pos).add(l.b.pos).multiplyScalar(0.5);
310
+ l.arcTop = (l.arcTop || new THREE.Vector3()).copy(l.a.pos).add(l.b.pos).multiplyScalar(0.5);
311
311
  return;
312
312
  }
313
313
  _arcDir.subVectors(l.b.pos, l.a.pos);
@@ -319,18 +319,73 @@ function updateRelLine(l) {
319
319
  }
320
320
  posAttr.needsUpdate = true;
321
321
  if (l.dashed) l.line.computeLineDistances();
322
- // 标签置于弧顶(t=0.5)外移一点,跟随弧线弯曲
323
322
  arcPoint(_arcTmp, l.a.pos, l.b.pos, _arcDir, _arcOff, 0.5);
324
- const lift = _arcOff.clone().multiplyScalar(6);
325
- l.label.position.copy(_arcTmp).add(lift);
323
+ l.arcTop = (l.arcTop || new THREE.Vector3()).copy(_arcTmp);
326
324
  }
327
325
 
328
- function setRelLabelPos(lbl, pa, pb, arc) {
329
- if (!arc) { lbl.position.copy(pa).add(pb).multiplyScalar(0.5); return; }
330
- const dir = pb.clone().sub(pa);
331
- const off = arcOffsetVec(dir, arc);
332
- const p = arcPoint(new THREE.Vector3(), pa, pb, dir, off, 0.5);
333
- lbl.position.copy(p).add(off.clone().multiplyScalar(6));
326
+ /* ---- 关系标签:HTML层 + 屏幕空间防重叠 ---- */
327
+ const relLabelEls = new Map(); // relationId -> div
328
+ function rebuildLinkLabelEls() {
329
+ const layer = $('link-labels');
330
+ layer.innerHTML = '';
331
+ relLabelEls.clear();
332
+ for (const l of simLinks) {
333
+ const el = document.createElement('div');
334
+ el.className = 'rel-label';
335
+ const name = l.inferredName || (state.relations.find((x) => x.id === l.id) || {}).name || '';
336
+ el.textContent = name;
337
+ el.style.color = l.colorCss || '#ccc';
338
+ el.style.borderColor = (l.colorCss || '#ccc') + '55';
339
+ el.onclick = () => {
340
+ state.selected = { type: 'relation', id: l.id };
341
+ renderInfoCard();
342
+ };
343
+ layer.appendChild(el);
344
+ relLabelEls.set(l.id, el);
345
+ l.labelEl = el;
346
+ }
347
+ measureLinkLabels();
348
+ }
349
+
350
+ // 一次性测量标签尺寸(投影定位需要 w/h;避免每帧读 offsetWidth 强制回流)
351
+ function measureLinkLabels() {
352
+ for (const el of relLabelEls.values()) {
353
+ el._w = el.offsetWidth;
354
+ el._h = el.offsetHeight;
355
+ }
356
+ }
357
+ window.addEventListener('resize', () => setTimeout(measureLinkLabels, 60));
358
+
359
+ const _projV = new THREE.Vector3();
360
+ function updateLinkLabels() {
361
+ const layer = $('link-labels');
362
+ if (!layer || !simLinks.length) return;
363
+ const W = renderer.domElement.clientWidth, H = renderer.domElement.clientHeight;
364
+ const items = [];
365
+ for (const l of simLinks) {
366
+ const el = l.labelEl;
367
+ if (!el) continue;
368
+ _projV.copy(l.arcTop || l.a.pos).project(camera);
369
+ if (_projV.z > 1 || _projV.z < -1) { el.style.display = 'none'; continue; }
370
+ const x = (_projV.x + 1) / 2 * W, y = (1 - _projV.y) / 2 * H;
371
+ if (x < -80 || x > W + 80 || y < -40 || y > H + 40) { el.style.display = 'none'; continue; }
372
+ el.style.display = 'block';
373
+ items.push({ el, x, y, w: el._w || 40, h: el._h || 18 });
374
+ }
375
+ // 屏幕空间防重叠:按 y 排序,与已放置矩形相交的标签向下推开,直到无碰撞
376
+ items.sort((p, q) => p.y - q.y);
377
+ const placed = [];
378
+ for (const it of items) {
379
+ let ny = it.y, guard = 0;
380
+ while (guard++ < 60) {
381
+ const hit = placed.find((p) => Math.abs(ny - p.y) < (it.h + p.h) / 2 + 2 && Math.abs(it.x - p.x) < (it.w + p.w) / 2 + 4);
382
+ if (!hit) break;
383
+ ny = hit.y + (hit.h + it.h) / 2 + 2;
384
+ }
385
+ it.y = ny;
386
+ placed.push(it);
387
+ it.el.style.transform = `translate(${(it.x - it.w / 2).toFixed(1)}px, ${(it.y - it.h / 2).toFixed(1)}px)`;
388
+ }
334
389
  }
335
390
 
336
391
  function buildNodeMesh(category) {
@@ -444,11 +499,7 @@ function rebuildGraph() {
444
499
  line.userData.relationId = r.id;
445
500
  line.userData.baseOpacity = op;
446
501
  linkGroup.add(line);
447
- const lbl = makeLabelSprite(r.name, st.css, 24);
448
- lbl.userData.text = r.name;
449
- setRelLabelPos(lbl, a.pos, b.pos, arc);
450
- labelGroup.add(lbl);
451
- simLinks.push({ id: r.id, a, b, line, label: lbl, dashed, confidence: conf, arc });
502
+ simLinks.push({ id: r.id, a, b, line, dashed, confidence: conf, arc, colorCss: st.css });
452
503
  });
453
504
 
454
505
  // 推理关系叠加:虚化虚线 + "(推)"标注,负数id与库中显式关系区分;仅显示两端均在当前视图的边
@@ -467,12 +518,10 @@ function rebuildGraph() {
467
518
  const line = makeRelLine(a.pos, b.pos, arc, 0xc792ea, true, 0.35, 3, 5);
468
519
  line.userData.relationId = -(idx + 1);
469
520
  linkGroup.add(line);
470
- const lbl = makeLabelSprite(ir.name + '(推)', '#c792ea', 22);
471
- setRelLabelPos(lbl, a.pos, b.pos, arc);
472
- labelGroup.add(lbl);
473
- simLinks.push({ id: line.userData.relationId, a, b, line, label: lbl, dashed: true, arc });
521
+ simLinks.push({ id: line.userData.relationId, a, b, line, dashed: true, arc, colorCss: '#c792ea', inferredName: ir.name + '(推)' });
474
522
  });
475
523
  }
524
+ rebuildLinkLabelEls();
476
525
 
477
526
  simBudget = 420;
478
527
  settleCount = 0;
@@ -550,6 +599,7 @@ function animate() {
550
599
  if (camFly.t >= 1) camFly = null;
551
600
  }
552
601
  controls.update();
602
+ updateLinkLabels();
553
603
  renderer.render(scene, camera);
554
604
  }
555
605
  animate();
@@ -1523,7 +1573,7 @@ function applyCanvasHi(nodes, rels) {
1523
1573
  const base = l.line.userData.baseOpacity === undefined ? 0.9 : l.line.userData.baseOpacity;
1524
1574
  const on = rels.has(l.id);
1525
1575
  l.line.material.opacity = on ? Math.max(base, 0.95) : 0.05;
1526
- if (l.label) l.label.material.opacity = on ? 1 : 0.06;
1576
+ if (l.labelEl) l.labelEl.style.opacity = on ? 1 : 0.06;
1527
1577
  }
1528
1578
  }
1529
1579
 
@@ -1536,7 +1586,7 @@ function clearCanvasHi() {
1536
1586
  }
1537
1587
  for (const l of simLinks) {
1538
1588
  l.line.material.opacity = l.line.userData.baseOpacity === undefined ? 0.9 : l.line.userData.baseOpacity;
1539
- if (l.label) l.label.material.opacity = 1;
1589
+ if (l.labelEl) l.labelEl.style.opacity = 1;
1540
1590
  }
1541
1591
  }
1542
1592
 
@@ -1829,8 +1879,11 @@ function applyStyleLight(dirty) {
1829
1879
  : new THREE.LineBasicMaterial({ color: st.color, transparent: true, opacity: op });
1830
1880
  if (st.dashed) l.line.computeLineDistances();
1831
1881
  l.dashed = st.dashed;
1832
- const fresh = relabel(l.label, st.css, 24);
1833
- if (fresh) l.label = fresh;
1882
+ // 标签为HTML层:直接换色
1883
+ if (l.labelEl) {
1884
+ l.labelEl.style.color = st.css;
1885
+ l.labelEl.style.borderColor = st.css + '55';
1886
+ }
1834
1887
  }
1835
1888
  }
1836
1889
  }
package/public/index.html CHANGED
@@ -231,6 +231,7 @@
231
231
  </div>
232
232
 
233
233
  <div id="canvas-wrap">
234
+ <div id="link-labels"></div>
234
235
  <div class="legend" id="legend"></div>
235
236
  <button id="m-legend" class="m-btn">图例</button>
236
237
  <div id="ego-bar">
package/public/style.css CHANGED
@@ -98,6 +98,16 @@
98
98
 
99
99
  #canvas-wrap { flex: 1; position: relative; min-width: 0; background: radial-gradient(ellipse at center, #0d1424 0%, #070a12 100%); }
100
100
  #canvas-wrap canvas { display: block; touch-action: none; }
101
+ /* 关系标签层:屏幕空间定位,防重叠 */
102
+ #link-labels { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
103
+ .rel-label {
104
+ position: absolute; left: 0; top: 0; pointer-events: auto; cursor: pointer;
105
+ font-size: 11px; line-height: 1; padding: 3px 7px; border-radius: 9px; white-space: nowrap;
106
+ background: rgba(9,13,24,.78); border: 1px solid transparent; user-select: none;
107
+ will-change: transform; text-shadow: none;
108
+ }
109
+ .rel-label:hover { filter: brightness(1.25); }
110
+ html[data-theme="light"] .rel-label { background: rgba(255,255,255,.86); }
101
111
  .legend {
102
112
  position: absolute; left: 12px; bottom: 12px; background: rgba(13,20,36,.85);
103
113
  border: 1px solid #1e2a44; border-radius: 8px; padding: 9px 12px; font-size: 11px; color: #a9bbd8;