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 +1 -1
- package/public/app.js +77 -24
- package/public/index.html +1 -0
- package/public/style.css +10 -0
package/package.json
CHANGED
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.
|
|
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
|
-
|
|
325
|
-
l.label.position.copy(_arcTmp).add(lift);
|
|
323
|
+
l.arcTop = (l.arcTop || new THREE.Vector3()).copy(_arcTmp);
|
|
326
324
|
}
|
|
327
325
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1833
|
-
if (
|
|
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
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;
|