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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/app.js +105 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-knowledge-graph",
3
- "version": "1.10.0",
3
+ "version": "1.10.1",
4
4
  "description": "本地知识图谱整合器:3D可视化 + AI对话建图 + 智能检索 + Git回溯 + RDF合规,全流程本地运行",
5
5
  "main": "server.js",
6
6
  "bin": {
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 geo = new THREE.BufferGeometry().setFromPoints([a.pos, b.pos]);
351
- const mat = dashed
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.position.copy(mid);
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
- const geo = new THREE.BufferGeometry().setFromPoints([a.pos, b.pos]);
376
- const mat = new THREE.LineDashedMaterial({ color: 0xc792ea, dashSize: 3, gapSize: 5, transparent: true, opacity: 0.35 });
377
- const line = new THREE.Line(geo, mat);
378
- line.computeLineDistances();
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.position.copy(a.pos.clone().add(b.pos).multiplyScalar(0.5));
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
- const posAttr = l.line.geometry.attributes.position;
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);