local-knowledge-graph 1.10.2 → 1.10.4

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.2",
3
+ "version": "1.10.4",
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
- // 每帧根据节点最新位置刷新弧线几何(标签为HTML层,弧顶点存 l.arcTop 供屏幕投影)
301
+ // 每帧根据节点最新位置刷新弧线几何与标签位置(Sprite 标签贴线、近大远小)
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.arcTop = (l.arcTop || new THREE.Vector3()).copy(l.a.pos).add(l.b.pos).multiplyScalar(0.5);
310
+ if (l.label) l.label.position.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,73 +319,19 @@ function updateRelLine(l) {
319
319
  }
320
320
  posAttr.needsUpdate = true;
321
321
  if (l.dashed) l.line.computeLineDistances();
322
- arcPoint(_arcTmp, l.a.pos, l.b.pos, _arcDir, _arcOff, 0.5);
323
- l.arcTop = (l.arcTop || new THREE.Vector3()).copy(_arcTmp);
324
- }
325
-
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;
322
+ // 标签置于弧顶(t=0.5),贴合弧线
323
+ if (l.label) {
324
+ arcPoint(_arcTmp, l.a.pos, l.b.pos, _arcDir, _arcOff, 0.5);
325
+ l.label.position.copy(_arcTmp);
355
326
  }
356
327
  }
357
- window.addEventListener('resize', () => setTimeout(measureLinkLabels, 60));
358
328
 
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
- }
329
+ // 弧顶标签初始定位(构建时用;此后每帧由 updateRelLine 跟随)
330
+ function setRelLabelPos(lbl, pa, pb, arc) {
331
+ if (!arc) { lbl.position.copy(pa).add(pb).multiplyScalar(0.5); return; }
332
+ const dir = pb.clone().sub(pa);
333
+ const off = arcOffsetVec(dir, arc);
334
+ lbl.position.copy(arcPoint(new THREE.Vector3(), pa, pb, dir, off, 0.5));
389
335
  }
390
336
 
391
337
  function buildNodeMesh(category) {
@@ -499,7 +445,12 @@ function rebuildGraph() {
499
445
  line.userData.relationId = r.id;
500
446
  line.userData.baseOpacity = op;
501
447
  linkGroup.add(line);
502
- simLinks.push({ id: r.id, a, b, line, dashed, confidence: conf, arc, colorCss: st.css });
448
+ // 线标注显示具体关系名(如"父子"),线型/颜色仍由大类规定;Sprite 保持贴线与近大远小
449
+ const lbl = makeLabelSprite(r.name, st.css, 24);
450
+ lbl.userData.text = r.name;
451
+ setRelLabelPos(lbl, a.pos, b.pos, arc);
452
+ labelGroup.add(lbl);
453
+ simLinks.push({ id: r.id, a, b, line, label: lbl, dashed, confidence: conf, arc });
503
454
  });
504
455
 
505
456
  // 推理关系叠加:虚化虚线 + "(推)"标注,负数id与库中显式关系区分;仅显示两端均在当前视图的边
@@ -518,10 +469,12 @@ function rebuildGraph() {
518
469
  const line = makeRelLine(a.pos, b.pos, arc, 0xc792ea, true, 0.35, 3, 5);
519
470
  line.userData.relationId = -(idx + 1);
520
471
  linkGroup.add(line);
521
- simLinks.push({ id: line.userData.relationId, a, b, line, dashed: true, arc, colorCss: '#c792ea', inferredName: ir.name + '(推)' });
472
+ const lbl = makeLabelSprite(ir.name + '(推)', '#c792ea', 22);
473
+ setRelLabelPos(lbl, a.pos, b.pos, arc);
474
+ labelGroup.add(lbl);
475
+ simLinks.push({ id: line.userData.relationId, a, b, line, label: lbl, dashed: true, arc });
522
476
  });
523
477
  }
524
- rebuildLinkLabelEls();
525
478
 
526
479
  simBudget = 420;
527
480
  settleCount = 0;
@@ -599,7 +552,6 @@ function animate() {
599
552
  if (camFly.t >= 1) camFly = null;
600
553
  }
601
554
  controls.update();
602
- updateLinkLabels();
603
555
  renderer.render(scene, camera);
604
556
  }
605
557
  animate();
@@ -1573,7 +1525,7 @@ function applyCanvasHi(nodes, rels) {
1573
1525
  const base = l.line.userData.baseOpacity === undefined ? 0.9 : l.line.userData.baseOpacity;
1574
1526
  const on = rels.has(l.id);
1575
1527
  l.line.material.opacity = on ? Math.max(base, 0.95) : 0.05;
1576
- if (l.labelEl) l.labelEl.style.opacity = on ? 1 : 0.06;
1528
+ if (l.label) l.label.material.opacity = on ? 1 : 0.06;
1577
1529
  }
1578
1530
  }
1579
1531
 
@@ -1586,7 +1538,7 @@ function clearCanvasHi() {
1586
1538
  }
1587
1539
  for (const l of simLinks) {
1588
1540
  l.line.material.opacity = l.line.userData.baseOpacity === undefined ? 0.9 : l.line.userData.baseOpacity;
1589
- if (l.labelEl) l.labelEl.style.opacity = 1;
1541
+ if (l.label) l.label.material.opacity = 1;
1590
1542
  }
1591
1543
  }
1592
1544
 
@@ -1879,11 +1831,8 @@ function applyStyleLight(dirty) {
1879
1831
  : new THREE.LineBasicMaterial({ color: st.color, transparent: true, opacity: op });
1880
1832
  if (st.dashed) l.line.computeLineDistances();
1881
1833
  l.dashed = st.dashed;
1882
- // 标签为HTML层:直接换色
1883
- if (l.labelEl) {
1884
- l.labelEl.style.color = st.css;
1885
- l.labelEl.style.borderColor = st.css + '55';
1886
- }
1834
+ const fresh = relabel(l.label, st.css, 24);
1835
+ if (fresh) l.label = fresh;
1887
1836
  }
1888
1837
  }
1889
1838
  }
package/public/index.html CHANGED
@@ -231,7 +231,6 @@
231
231
  </div>
232
232
 
233
233
  <div id="canvas-wrap">
234
- <div id="link-labels"></div>
235
234
  <div class="legend" id="legend"></div>
236
235
  <button id="m-legend" class="m-btn">图例</button>
237
236
  <div id="ego-bar">
package/public/style.css CHANGED
@@ -98,16 +98,6 @@
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); }
111
101
  .legend {
112
102
  position: absolute; left: 12px; bottom: 12px; background: rgba(13,20,36,.85);
113
103
  border: 1px solid #1e2a44; border-radius: 8px; padding: 9px 12px; font-size: 11px; color: #a9bbd8;