local-knowledge-graph 1.10.0 → 1.10.1
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 +105 -21
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -256,6 +256,83 @@ function makeLabelSprite(text, cssColor, fontSize) {
|
|
|
256
256
|
return sprite;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/* ---- 平行边弧形分离 ---- */
|
|
260
|
+
// 同对节点多条关系时线弯曲错开:offset 为弧的偏移强度(0=直线),标签置于各弧顶
|
|
261
|
+
const ARC_SEGMENTS = 16;
|
|
262
|
+
function arcOffsetVec(dir, arc) {
|
|
263
|
+
// 弧偏移方向:取与边垂直的平面,按边序号均匀转开角度;强度=边长*比例,保证不同长度边分离度一致
|
|
264
|
+
const up = Math.abs(dir.y) > 0.92 ? new THREE.Vector3(1, 0, 0) : new THREE.Vector3(0, 1, 0);
|
|
265
|
+
const u = new THREE.Vector3().crossVectors(dir, up).normalize();
|
|
266
|
+
const v = new THREE.Vector3().crossVectors(dir, u).normalize();
|
|
267
|
+
const ang = (arc.idx / arc.total) * Math.PI * 2 + 0.6; // 固定相位避免与常用方向重合
|
|
268
|
+
const strength = 0.14; // 弧顶偏移 = 边长 * strength
|
|
269
|
+
return u.multiplyScalar(Math.cos(ang)).add(v.multiplyScalar(Math.sin(ang))).multiplyScalar(dir.length() * strength);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 曲线上参数 t∈[0,1] 的点:a→b 直线叠加抛物弧偏移(中点最大,两端为0)
|
|
273
|
+
function arcPoint(out, a, b, dir, off, t) {
|
|
274
|
+
const sag = 4 * t * (1 - t);
|
|
275
|
+
out.set(
|
|
276
|
+
a.x + dir.x * t + off.x * sag,
|
|
277
|
+
a.y + dir.y * t + off.y * sag,
|
|
278
|
+
a.z + dir.z * t + off.z * sag
|
|
279
|
+
);
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function makeRelLine(pa, pb, arc, color, dashed, opacity, dashSize, gapSize) {
|
|
284
|
+
const pts = [];
|
|
285
|
+
if (arc) {
|
|
286
|
+
const dir = pb.clone().sub(pa);
|
|
287
|
+
const off = arcOffsetVec(dir, arc);
|
|
288
|
+
for (let i = 0; i <= ARC_SEGMENTS; i++) pts.push(arcPoint(new THREE.Vector3(), pa, pb, dir, off, i / ARC_SEGMENTS).clone());
|
|
289
|
+
} else {
|
|
290
|
+
pts.push(pa.clone(), pb.clone());
|
|
291
|
+
}
|
|
292
|
+
const geo = new THREE.BufferGeometry().setFromPoints(pts);
|
|
293
|
+
const mat = dashed
|
|
294
|
+
? new THREE.LineDashedMaterial({ color, dashSize, gapSize, transparent: true, opacity })
|
|
295
|
+
: new THREE.LineBasicMaterial({ color, transparent: true, opacity });
|
|
296
|
+
const line = new THREE.Line(geo, mat);
|
|
297
|
+
if (dashed) line.computeLineDistances();
|
|
298
|
+
return line;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 每帧根据节点最新位置刷新弧线几何
|
|
302
|
+
const _arcDir = new THREE.Vector3(), _arcOff = new THREE.Vector3(), _arcTmp = new THREE.Vector3();
|
|
303
|
+
function updateRelLine(l) {
|
|
304
|
+
if (!l.arc) {
|
|
305
|
+
const posAttr = l.line.geometry.attributes.position;
|
|
306
|
+
posAttr.setXYZ(0, l.a.pos.x, l.a.pos.y, l.a.pos.z);
|
|
307
|
+
posAttr.setXYZ(1, l.b.pos.x, l.b.pos.y, l.b.pos.z);
|
|
308
|
+
posAttr.needsUpdate = true;
|
|
309
|
+
if (l.dashed) l.line.computeLineDistances();
|
|
310
|
+
l.label.position.copy(l.a.pos).add(l.b.pos).multiplyScalar(0.5);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
_arcDir.subVectors(l.b.pos, l.a.pos);
|
|
314
|
+
_arcOff.copy(arcOffsetVec(_arcDir, l.arc));
|
|
315
|
+
const posAttr = l.line.geometry.attributes.position;
|
|
316
|
+
for (let i = 0; i <= ARC_SEGMENTS; i++) {
|
|
317
|
+
arcPoint(_arcTmp, l.a.pos, l.b.pos, _arcDir, _arcOff, i / ARC_SEGMENTS);
|
|
318
|
+
posAttr.setXYZ(i, _arcTmp.x, _arcTmp.y, _arcTmp.z);
|
|
319
|
+
}
|
|
320
|
+
posAttr.needsUpdate = true;
|
|
321
|
+
if (l.dashed) l.line.computeLineDistances();
|
|
322
|
+
// 标签置于弧顶(t=0.5)外移一点,跟随弧线弯曲
|
|
323
|
+
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);
|
|
326
|
+
}
|
|
327
|
+
|
|
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));
|
|
334
|
+
}
|
|
335
|
+
|
|
259
336
|
function buildNodeMesh(category) {
|
|
260
337
|
const st = ENTITY_STYLE[category] || { color: 0xaaaaaa };
|
|
261
338
|
let mesh;
|
|
@@ -338,6 +415,21 @@ function rebuildGraph() {
|
|
|
338
415
|
simNodes.push({ id: e.id, pos: mesh.position.clone(), vel: new THREE.Vector3(), mesh, label, radius: 10, sizeScale: ENTITY_STYLE[e.category] ? (ENTITY_STYLE[e.category].size || 1) : 1 });
|
|
339
416
|
});
|
|
340
417
|
|
|
418
|
+
// 同节点对的平行边统一编号:线作弧形分离,标签各置弧顶,避免文字与线完全重叠
|
|
419
|
+
const pairCount = new Map(); // 'a|b'(无向) -> 同对节点边的总数
|
|
420
|
+
for (const r of rels) {
|
|
421
|
+
const k = r.source_id < r.target_id ? `${r.source_id}|${r.target_id}` : `${r.target_id}|${r.source_id}`;
|
|
422
|
+
pairCount.set(k, (pairCount.get(k) || 0) + 1);
|
|
423
|
+
}
|
|
424
|
+
const pairSeen = new Map();
|
|
425
|
+
const arcOf = (r) => {
|
|
426
|
+
const k = r.source_id < r.target_id ? `${r.source_id}|${r.target_id}` : `${r.target_id}|${r.source_id}`;
|
|
427
|
+
const n = pairCount.get(k) || 1;
|
|
428
|
+
const i = pairSeen.get(k) || 0;
|
|
429
|
+
pairSeen.set(k, i + 1);
|
|
430
|
+
return n > 1 ? { idx: i, total: n } : null;
|
|
431
|
+
};
|
|
432
|
+
|
|
341
433
|
rels.forEach((r) => {
|
|
342
434
|
const a = simNodes.find((n) => n.id === r.source_id);
|
|
343
435
|
const b = simNodes.find((n) => n.id === r.target_id);
|
|
@@ -347,21 +439,16 @@ function rebuildGraph() {
|
|
|
347
439
|
const baseOp = st.opacity === undefined ? 0.9 : st.opacity;
|
|
348
440
|
const op = conf === '存疑' ? Math.min(baseOp, 0.35) : baseOp; // 存疑降不透明度
|
|
349
441
|
const dashed = st.dashed || conf === '推测'; // 推测强制虚线
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
? new THREE.LineDashedMaterial({ color: st.color, dashSize: st.dashSize || 6, gapSize: st.gapSize || 4, transparent: true, opacity: op })
|
|
353
|
-
: new THREE.LineBasicMaterial({ color: st.color, transparent: true, opacity: op });
|
|
354
|
-
const line = new THREE.Line(geo, mat);
|
|
442
|
+
const arc = arcOf(r);
|
|
443
|
+
const line = makeRelLine(a.pos, b.pos, arc, st.color, dashed, op, st.dashSize || 6, st.gapSize || 4);
|
|
355
444
|
line.userData.relationId = r.id;
|
|
356
445
|
line.userData.baseOpacity = op;
|
|
357
446
|
linkGroup.add(line);
|
|
358
|
-
const mid = a.pos.clone().add(b.pos).multiplyScalar(0.5);
|
|
359
|
-
// 线标注显示具体关系名(如"父子"),线型/颜色仍由大类规定
|
|
360
447
|
const lbl = makeLabelSprite(r.name, st.css, 24);
|
|
361
448
|
lbl.userData.text = r.name;
|
|
362
|
-
lbl.
|
|
449
|
+
setRelLabelPos(lbl, a.pos, b.pos, arc);
|
|
363
450
|
labelGroup.add(lbl);
|
|
364
|
-
simLinks.push({ id: r.id, a, b, line, label: lbl, dashed, confidence: conf });
|
|
451
|
+
simLinks.push({ id: r.id, a, b, line, label: lbl, dashed, confidence: conf, arc });
|
|
365
452
|
});
|
|
366
453
|
|
|
367
454
|
// 推理关系叠加:虚化虚线 + "(推)"标注,负数id与库中显式关系区分;仅显示两端均在当前视图的边
|
|
@@ -372,16 +459,18 @@ function rebuildGraph() {
|
|
|
372
459
|
const a = simNodes.find((n) => n.id === ir.source_id);
|
|
373
460
|
const b = simNodes.find((n) => n.id === ir.target_id);
|
|
374
461
|
if (!a || !b) return;
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
462
|
+
// 推理边并入平行边分组:同对节点的显式边与推理边互不错开编号,仅继续排号
|
|
463
|
+
const k = ir.source_id < ir.target_id ? `${ir.source_id}|${ir.target_id}` : `${ir.target_id}|${ir.source_id}`;
|
|
464
|
+
const n = (pairCount.get(k) || 0) + 1;
|
|
465
|
+
pairCount.set(k, n);
|
|
466
|
+
const arc = n > 1 ? { idx: n - 1, total: n } : null;
|
|
467
|
+
const line = makeRelLine(a.pos, b.pos, arc, 0xc792ea, true, 0.35, 3, 5);
|
|
379
468
|
line.userData.relationId = -(idx + 1);
|
|
380
469
|
linkGroup.add(line);
|
|
381
470
|
const lbl = makeLabelSprite(ir.name + '(推)', '#c792ea', 22);
|
|
382
|
-
lbl
|
|
471
|
+
setRelLabelPos(lbl, a.pos, b.pos, arc);
|
|
383
472
|
labelGroup.add(lbl);
|
|
384
|
-
simLinks.push({ id: line.userData.relationId, a, b, line, label: lbl, dashed: true });
|
|
473
|
+
simLinks.push({ id: line.userData.relationId, a, b, line, label: lbl, dashed: true, arc });
|
|
385
474
|
});
|
|
386
475
|
}
|
|
387
476
|
|
|
@@ -429,12 +518,7 @@ function simStep() {
|
|
|
429
518
|
}
|
|
430
519
|
settleCount++;
|
|
431
520
|
for (const l of simLinks) {
|
|
432
|
-
|
|
433
|
-
posAttr.setXYZ(0, l.a.pos.x, l.a.pos.y, l.a.pos.z);
|
|
434
|
-
posAttr.setXYZ(1, l.b.pos.x, l.b.pos.y, l.b.pos.z);
|
|
435
|
-
posAttr.needsUpdate = true;
|
|
436
|
-
if (l.dashed) l.line.computeLineDistances();
|
|
437
|
-
l.label.position.copy(l.a.pos).add(l.b.pos).multiplyScalar(0.5);
|
|
521
|
+
updateRelLine(l);
|
|
438
522
|
}
|
|
439
523
|
for (const nd of simNodes) {
|
|
440
524
|
nd.mesh.position.copy(nd.pos);
|