@glossarist/concept-browser 0.7.90 → 0.7.91
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/src/components/RelationSphere.vue +35 -313
- package/src/components/RelationSphere.vue.bak +1921 -0
- package/src/components/relation-sphere/forces.ts +156 -0
- package/src/components/relation-sphere/rake-bundles.ts +221 -0
- package/src/components/relation-sphere/svg-markers.ts +52 -0
- package/src/components/relation-sphere/types.ts +51 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.91",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,16 @@ import { useI18n } from '../i18n';
|
|
|
27
27
|
const { t, locale } = useI18n();
|
|
28
28
|
import type { Concept, Manifest, GraphEdge, PartitiveRelationWire, PartitiveMemberWire } from '../adapters/types';
|
|
29
29
|
import { rakeStrokeStyle } from '../utils/partitive-multiplicity';
|
|
30
|
+
import { drawRakeBundles } from './relation-sphere/rake-bundles';
|
|
31
|
+
import { ensureMarkers, ensureTypeMarker } from './relation-sphere/svg-markers';
|
|
32
|
+
import {
|
|
33
|
+
sphereConstraint,
|
|
34
|
+
velocityClamp,
|
|
35
|
+
repulsion,
|
|
36
|
+
linkForce,
|
|
37
|
+
focusPin,
|
|
38
|
+
} from './relation-sphere/forces';
|
|
39
|
+
import type { ForcesContext } from './relation-sphere/types';
|
|
30
40
|
import { conceptUri } from '../adapters/model-bridge';
|
|
31
41
|
|
|
32
42
|
const props = defineProps<{
|
|
@@ -429,33 +439,6 @@ function buildGraph() {
|
|
|
429
439
|
|
|
430
440
|
/* Reverse lookup: URI → node id. Linear scan is fine for ≤36 nodes. */
|
|
431
441
|
/* ── Custom forces (EXACT prototype replica) ────────────── */
|
|
432
|
-
function sphereConstraint() {
|
|
433
|
-
let nL: SNode[] = nodes;
|
|
434
|
-
function force() {
|
|
435
|
-
for (const n of nL) {
|
|
436
|
-
const len = Math.sqrt(n.x*n.x + n.y*n.y + n.z*n.z);
|
|
437
|
-
if (len > 0.001) {
|
|
438
|
-
const nx = n.x/len, ny = n.y/len, nz = n.z/len;
|
|
439
|
-
n.x = nx; n.y = ny; n.z = nz;
|
|
440
|
-
const rad = n.vx*nx + n.vy*ny + n.vz*nz;
|
|
441
|
-
n.vx -= rad*nx; n.vy -= rad*ny; n.vz -= rad*nz;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
(force as any).initialize = (n: SNode[]) => { nL = n; };
|
|
446
|
-
return force;
|
|
447
|
-
}
|
|
448
|
-
function velocityClamp(maxV: number) {
|
|
449
|
-
let nL: SNode[] = nodes;
|
|
450
|
-
function force() {
|
|
451
|
-
for (const n of nL) {
|
|
452
|
-
const v2 = n.vx*n.vx + n.vy*n.vy + n.vz*n.vz;
|
|
453
|
-
if (v2 > maxV*maxV) { const s = maxV/Math.sqrt(v2); n.vx*=s; n.vy*=s; n.vz*=s; }
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
(force as any).initialize = (n: SNode[]) => { nL = n; };
|
|
457
|
-
return force;
|
|
458
|
-
}
|
|
459
442
|
/* Pairwise repulsion — pushes nodes apart on the sphere surface so cards
|
|
460
443
|
don't stack. Operates in 3D (chord distance), with the sphereConstraint
|
|
461
444
|
normalizing positions back to the unit sphere each tick. Focus node is
|
|
@@ -472,31 +455,23 @@ let linkStrength = expandParams(5).linkStrength;
|
|
|
472
455
|
let repulseMinDist = expandParams(5).repMin;
|
|
473
456
|
let repulseStrength = expandParams(5).repStrength;
|
|
474
457
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
a.vx += fx; a.vy += fy; a.vz += fz;
|
|
493
|
-
if (b.depth !== 0) { b.vx -= fx; b.vy -= fy; b.vz -= fz; }
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
(force as any).initialize = (n: SNode[]) => { nL = n; };
|
|
498
|
-
return force;
|
|
499
|
-
}
|
|
458
|
+
/**
|
|
459
|
+
* Read-only getter bundle for the extracted force factories in
|
|
460
|
+
* ./relation-sphere/forces.ts. Forces read the mutable sim state
|
|
461
|
+
* via these getters each tick so user input (Expand slider, mute
|
|
462
|
+
* toggles, navigation target) takes effect on the next step.
|
|
463
|
+
*/
|
|
464
|
+
const forcesContext: ForcesContext = {
|
|
465
|
+
nodes: () => nodes,
|
|
466
|
+
links: () => links,
|
|
467
|
+
mutedTypes: () => mutedTypes.value,
|
|
468
|
+
mutedRegisters: () => mutedRegisters.value,
|
|
469
|
+
linkDistance: () => linkDistance,
|
|
470
|
+
linkStrength: () => linkStrength,
|
|
471
|
+
repulseMinDist: () => repulseMinDist,
|
|
472
|
+
repulseStrength: () => repulseStrength,
|
|
473
|
+
};
|
|
474
|
+
|
|
500
475
|
const navForce = (() => {
|
|
501
476
|
let nL: SNode[] = nodes;
|
|
502
477
|
function force() {
|
|
@@ -524,63 +499,16 @@ const clampF = velocityClamp(0.20);
|
|
|
524
499
|
/* minDist=0.95 ≈ 56° separation on the unit sphere — strong spread so
|
|
525
500
|
cards don't cluster around the focus. strength is high enough that
|
|
526
501
|
nodes reach their spread positions within ~2 seconds of sim start. */
|
|
527
|
-
const repulseF = repulsion();
|
|
502
|
+
const repulseF = repulsion(forcesContext);
|
|
528
503
|
|
|
529
504
|
/* 3D spring (link) force — pulls connected nodes toward a target chord
|
|
530
505
|
distance. This is what actually spreads cards apart: longer springs
|
|
531
506
|
(higher Expand level) push the graph outward along its edges. Stock
|
|
532
507
|
d3-forceLink only handles x/y; this custom version operates in full
|
|
533
508
|
3D so the sphereConstraint can renormalize positions correctly. */
|
|
534
|
-
|
|
535
|
-
let nL: SNode[] = nodes;
|
|
536
|
-
let idToIdx = new Map<string, number>();
|
|
537
|
-
function force() {
|
|
538
|
-
if (links.length === 0) return;
|
|
539
|
-
const target = linkDistance;
|
|
540
|
-
const strength = linkStrength;
|
|
541
|
-
/* Read muted sets each tick so toggleType/toggleRegister take effect
|
|
542
|
-
immediately on the next simulation step. */
|
|
543
|
-
const mutedT = mutedTypes.value;
|
|
544
|
-
const mutedR = mutedRegisters.value;
|
|
545
|
-
for (const link of links) {
|
|
546
|
-
if (mutedT.has(link.type)) continue;
|
|
547
|
-
const ai = idToIdx.get(link.source);
|
|
548
|
-
const bi = idToIdx.get(link.target);
|
|
549
|
-
if (ai === undefined || bi === undefined) continue;
|
|
550
|
-
const a = nL[ai], b = nL[bi];
|
|
551
|
-
/* Skip edges involving a muted register (unless the node is the focus) */
|
|
552
|
-
if (a.depth !== 0 && mutedR.has(a.register)) continue;
|
|
553
|
-
if (b.depth !== 0 && mutedR.has(b.register)) continue;
|
|
554
|
-
const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
|
|
555
|
-
const d2 = dx*dx + dy*dy + dz*dz;
|
|
556
|
-
if (d2 < 1e-6) continue;
|
|
557
|
-
const d = Math.sqrt(d2);
|
|
558
|
-
const diff = (d - target) / d * strength;
|
|
559
|
-
const fx = dx * diff, fy = dy * diff, fz = dz * diff;
|
|
560
|
-
if (a.depth !== 0) { a.vx += fx; a.vy += fy; a.vz += fz; }
|
|
561
|
-
if (b.depth !== 0) { b.vx -= fx; b.vy -= fy; b.vz -= fz; }
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
(force as any).initialize = (n: SNode[]) => {
|
|
565
|
-
nL = n;
|
|
566
|
-
idToIdx = new Map();
|
|
567
|
-
for (let i = 0; i < n.length; i++) idToIdx.set(n[i].id, i);
|
|
568
|
-
};
|
|
569
|
-
return force;
|
|
570
|
-
}
|
|
571
|
-
const linkF = linkForce();
|
|
509
|
+
const linkF = linkForce(forcesContext);
|
|
572
510
|
/* Pin the focus node at (0, 0, 1) so neighbor repulsion doesn't shove it.
|
|
573
511
|
Without this, the focus drifts and the whole sphere wobbles. */
|
|
574
|
-
function focusPin() {
|
|
575
|
-
let nL: SNode[] = nodes;
|
|
576
|
-
function force() {
|
|
577
|
-
for (const n of nL) {
|
|
578
|
-
if (n.depth === 0) { n.x = 0; n.y = 0; n.z = 1; n.vx = 0; n.vy = 0; n.vz = 0; }
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
(force as any).initialize = (n: SNode[]) => { nL = n; };
|
|
582
|
-
return force;
|
|
583
|
-
}
|
|
584
512
|
const focusPinF = focusPin();
|
|
585
513
|
|
|
586
514
|
/* ── Simulation ─────────────────────────────────────────── */
|
|
@@ -757,218 +685,12 @@ function drawEdges(cx: number, cy: number) {
|
|
|
757
685
|
svg.appendChild(label);
|
|
758
686
|
}
|
|
759
687
|
|
|
760
|
-
drawRakeBundles(svg, pos,
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
* Each relation renders as a right-angle bracket with stem + spine + drops
|
|
767
|
-
* (geometry described in detail above). Stem and spine are the structural
|
|
768
|
-
* frame — always single solid at FRAME_STROKE_WIDTH.
|
|
769
|
-
*
|
|
770
|
-
* Per-member multiplicity + is_delimiting is encoded on each DROP via
|
|
771
|
-
* rakeStrokeStyle() (see utils/partitive-multiplicity.ts):
|
|
772
|
-
*
|
|
773
|
-
* multiplicity is_delimiting drop visual
|
|
774
|
-
* ------------------- -------------- --------------------------------
|
|
775
|
-
* compulsory false 1 solid @ 1.5px
|
|
776
|
-
* compulsory true 1 solid @ 4.5px (3× bold)
|
|
777
|
-
* optional false 1 dashed @ 1.5px
|
|
778
|
-
* optional true 1 dashed @ 4.5px
|
|
779
|
-
* compulsory_multiple false 2 solid @ 1.5px
|
|
780
|
-
* compulsory_multiple true 2 solid @ 4.5px
|
|
781
|
-
* optional_multiple false 2 dashed @ 1.5px
|
|
782
|
-
* optional_multiple true 2 dashed @ 4.5px
|
|
783
|
-
* compulsory_at_least_one false 1 solid + 1 dashed @ 1.5px
|
|
784
|
-
* compulsory_at_least_one true 1 solid + 1 dashed @ 4.5px
|
|
785
|
-
*
|
|
786
|
-
* completeness === 'partial' : spine extends past last tooth (continued
|
|
787
|
-
* backline — "more exist but not shown")
|
|
788
|
-
*
|
|
789
|
-
* Diamond marker at the comprehensive end signals the hyperedge origin.
|
|
790
|
-
* Junction marker at the spine midpoint signals the right-angle corner.
|
|
791
|
-
*/
|
|
792
|
-
function drawRakeBundles(
|
|
793
|
-
svg: SVGSVGElement,
|
|
794
|
-
pos: Map<string, {x: number; y: number}>,
|
|
795
|
-
_cx: number,
|
|
796
|
-
_cy: number,
|
|
797
|
-
) {
|
|
798
|
-
if (mutedTypes.value.has('__partitive__')) return;
|
|
799
|
-
const color = uiStore.isDark ? '#2dd4bf' : '#0d9488';
|
|
800
|
-
const DOUBLE_GAP = 4;
|
|
801
|
-
const DASH = '4 3';
|
|
802
|
-
const FRAME_STROKE_WIDTH = 1.4;
|
|
803
|
-
const SPINE_FRACTION = 0.55; // spine sits 55% from comp toward centroid
|
|
804
|
-
const PARTIAL_TAIL = 28; // px extension for completeness: 'partial'
|
|
805
|
-
|
|
806
|
-
for (const rel of props.partitiveRelations ?? []) {
|
|
807
|
-
const compParsed = UriRouter.parseUri(rel.comprehensive);
|
|
808
|
-
if (!compParsed) continue;
|
|
809
|
-
const compNodeId = `${compParsed.registerId}/${compParsed.conceptId}`;
|
|
810
|
-
const compPos = pos.get(compNodeId);
|
|
811
|
-
if (!compPos) continue;
|
|
812
|
-
|
|
813
|
-
const memberPositions = rel.partitives
|
|
814
|
-
.map(member => {
|
|
815
|
-
const parsed = UriRouter.parseUri(member.uri);
|
|
816
|
-
if (!parsed) return null;
|
|
817
|
-
const nid = `${parsed.registerId}/${parsed.conceptId}`;
|
|
818
|
-
const p = pos.get(nid);
|
|
819
|
-
return p ? { member, pos: p } : null;
|
|
820
|
-
})
|
|
821
|
-
.filter((x): x is { member: PartitiveMemberWire; pos: {x: number; y: number} } => x !== null);
|
|
822
|
-
|
|
823
|
-
if (memberPositions.length === 0) continue;
|
|
824
|
-
|
|
825
|
-
// 1. Compute stem-axis (comp → centroid of partitives) and spine-axis (perpendicular)
|
|
826
|
-
const centroidX = memberPositions.reduce((s, m) => s + m.pos.x, 0) / memberPositions.length;
|
|
827
|
-
const centroidY = memberPositions.reduce((s, m) => s + m.pos.y, 0) / memberPositions.length;
|
|
828
|
-
const stemDx = centroidX - compPos.x;
|
|
829
|
-
const stemDy = centroidY - compPos.y;
|
|
830
|
-
const stemLen = Math.hypot(stemDx, stemDy) || 1;
|
|
831
|
-
const stemDirX = stemDx / stemLen; // unit vector along stem
|
|
832
|
-
const stemDirY = stemDy / stemLen;
|
|
833
|
-
const spineDirX = -stemDirY; // perpendicular = spine direction
|
|
834
|
-
const spineDirY = stemDirX;
|
|
835
|
-
|
|
836
|
-
// 2. Spine center: along stem-axis at SPINE_FRACTION from comp
|
|
837
|
-
const spineCx = compPos.x + stemDirX * stemLen * SPINE_FRACTION;
|
|
838
|
-
const spineCy = compPos.y + stemDirY * stemLen * SPINE_FRACTION;
|
|
839
|
-
|
|
840
|
-
// 3. Tooth positions = projection of each partitive onto spine line
|
|
841
|
-
const teeth = memberPositions.map(({ member, pos: pPos }) => {
|
|
842
|
-
const vx = pPos.x - spineCx;
|
|
843
|
-
const vy = pPos.y - spineCy;
|
|
844
|
-
const along = vx * spineDirX + vy * spineDirY;
|
|
845
|
-
const toothX = spineCx + spineDirX * along;
|
|
846
|
-
const toothY = spineCy + spineDirY * along;
|
|
847
|
-
return {
|
|
848
|
-
member,
|
|
849
|
-
partitivePos: pPos,
|
|
850
|
-
toothX,
|
|
851
|
-
toothY,
|
|
852
|
-
along,
|
|
853
|
-
style: rakeStrokeStyle(member.presence, member.count, member.isDelimiting),
|
|
854
|
-
};
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
// 4. Spine extents (leftmost to rightmost tooth along spine-axis)
|
|
858
|
-
let minAlong = teeth[0].along;
|
|
859
|
-
let maxAlong = teeth[0].along;
|
|
860
|
-
for (const t of teeth) {
|
|
861
|
-
if (t.along < minAlong) minAlong = t.along;
|
|
862
|
-
if (t.along > maxAlong) maxAlong = t.along;
|
|
863
|
-
}
|
|
864
|
-
// For completeness: 'partial', extend past last tooth (continued backline)
|
|
865
|
-
if (rel.completeness === 'partial') maxAlong += PARTIAL_TAIL;
|
|
866
|
-
const spineStartX = spineCx + spineDirX * minAlong;
|
|
867
|
-
const spineStartY = spineCy + spineDirY * minAlong;
|
|
868
|
-
const spineEndX = spineCx + spineDirX * maxAlong;
|
|
869
|
-
const spineEndY = spineCy + spineDirY * maxAlong;
|
|
870
|
-
|
|
871
|
-
/**
|
|
872
|
-
* Draw a single straight segment.
|
|
873
|
-
*
|
|
874
|
-
* Stem + spine use FRAME_STROKE_WIDTH (single solid frame).
|
|
875
|
-
* Drops use the per-member multiplicity + delimiting style from
|
|
876
|
-
* rakeStrokeStyle().
|
|
877
|
-
*/
|
|
878
|
-
const drawSegment = (
|
|
879
|
-
ax: number, ay: number, bx: number, by: number,
|
|
880
|
-
opts: { width: number; dashed?: boolean; offset?: number; opacity?: number },
|
|
881
|
-
) => {
|
|
882
|
-
const opacity = opts.opacity ?? 0.85;
|
|
883
|
-
const offset = opts.offset ?? 0;
|
|
884
|
-
const dashed = opts.dashed ?? false;
|
|
885
|
-
const dx = bx - ax, dy = by - ay;
|
|
886
|
-
const len = Math.hypot(dx, dy) || 1;
|
|
887
|
-
const px = -dy / len, py = dx / len;
|
|
888
|
-
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
889
|
-
line.setAttribute('class', 'rake-seg');
|
|
890
|
-
line.setAttribute('x1', String(ax + px * offset));
|
|
891
|
-
line.setAttribute('y1', String(ay + py * offset));
|
|
892
|
-
line.setAttribute('x2', String(bx + px * offset));
|
|
893
|
-
line.setAttribute('y2', String(by + py * offset));
|
|
894
|
-
line.setAttribute('stroke', color);
|
|
895
|
-
line.setAttribute('stroke-width', String(opts.width));
|
|
896
|
-
line.setAttribute('opacity', String(opacity));
|
|
897
|
-
line.setAttribute('stroke-linecap', 'square');
|
|
898
|
-
if (dashed) line.setAttribute('stroke-dasharray', DASH);
|
|
899
|
-
svg.appendChild(line);
|
|
900
|
-
};
|
|
901
|
-
|
|
902
|
-
// 5. Stem: comp → spine center (frame — single solid)
|
|
903
|
-
drawSegment(compPos.x, compPos.y, spineCx, spineCy, { width: FRAME_STROKE_WIDTH });
|
|
904
|
-
|
|
905
|
-
// 6. Spine: leftmost tooth → rightmost tooth (frame — single solid)
|
|
906
|
-
if (Math.abs(maxAlong - minAlong) > 1) {
|
|
907
|
-
drawSegment(spineStartX, spineStartY, spineEndX, spineEndY, { width: FRAME_STROKE_WIDTH });
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// 7. Drops: tooth → partitive (per-member multiplicity + delimiting)
|
|
911
|
-
// Draw 1 or 2 parallel lines per the style.
|
|
912
|
-
for (const tooth of teeth) {
|
|
913
|
-
drawSegment(tooth.toothX, tooth.toothY, tooth.partitivePos.x, tooth.partitivePos.y, {
|
|
914
|
-
width: tooth.style.strokeWidth,
|
|
915
|
-
dashed: tooth.style.primaryDashed,
|
|
916
|
-
});
|
|
917
|
-
if (tooth.style.lineCount === 2) {
|
|
918
|
-
drawSegment(tooth.toothX, tooth.toothY, tooth.partitivePos.x, tooth.partitivePos.y, {
|
|
919
|
-
width: tooth.style.strokeWidth,
|
|
920
|
-
dashed: tooth.style.secondaryDashed,
|
|
921
|
-
offset: DOUBLE_GAP,
|
|
922
|
-
});
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// 8. Junction marker at spine center (the right-angle corner)
|
|
927
|
-
const junction = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
928
|
-
junction.setAttribute('class', 'rake-junction');
|
|
929
|
-
junction.setAttribute('cx', String(spineCx));
|
|
930
|
-
junction.setAttribute('cy', String(spineCy));
|
|
931
|
-
junction.setAttribute('r', '2.5');
|
|
932
|
-
junction.setAttribute('fill', color);
|
|
933
|
-
junction.setAttribute('opacity', '0.9');
|
|
934
|
-
junction.setAttribute('stroke', uiStore.isDark ? '#0a1f1c' : '#ffffff');
|
|
935
|
-
junction.setAttribute('stroke-width', '1');
|
|
936
|
-
svg.appendChild(junction);
|
|
937
|
-
|
|
938
|
-
// 9. Diamond marker at comprehensive end (hyperedge origin)
|
|
939
|
-
const diamond = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
940
|
-
const dsize = 5;
|
|
941
|
-
diamond.setAttribute('d',
|
|
942
|
-
`M ${compPos.x} ${compPos.y - dsize} L ${compPos.x + dsize} ${compPos.y} L ${compPos.x} ${compPos.y + dsize} L ${compPos.x - dsize} ${compPos.y} Z`);
|
|
943
|
-
diamond.setAttribute('fill', color);
|
|
944
|
-
diamond.setAttribute('opacity', '0.9');
|
|
945
|
-
diamond.setAttribute('stroke', uiStore.isDark ? '#0a1f1c' : '#ffffff');
|
|
946
|
-
diamond.setAttribute('stroke-width', '1');
|
|
947
|
-
svg.appendChild(diamond);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
function ensureMarkers(svg: SVGSVGElement) {
|
|
952
|
-
let defs = svg.querySelector('defs');
|
|
953
|
-
if (!defs) { defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); svg.appendChild(defs); }
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/* Lazy per-type marker — arrow color matches the edge's per-type color
|
|
957
|
-
(which may differ from the category color via TYPE_COLOR_OVERRIDE). */
|
|
958
|
-
function ensureTypeMarker(svg: SVGSVGElement, typeId: string, color: string): string {
|
|
959
|
-
const safeId = typeId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
960
|
-
const markerId = `rel-arrow-t-${safeId}`;
|
|
961
|
-
if (svg.querySelector(`#${markerId}`)) return markerId;
|
|
962
|
-
let defs = svg.querySelector('defs');
|
|
963
|
-
if (!defs) { defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); svg.appendChild(defs); }
|
|
964
|
-
const m = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
|
|
965
|
-
m.setAttribute('id', markerId);
|
|
966
|
-
m.setAttribute('viewBox', '0 0 8 8'); m.setAttribute('refX', '6'); m.setAttribute('refY', '4');
|
|
967
|
-
m.setAttribute('markerWidth', '5'); m.setAttribute('markerHeight', '5'); m.setAttribute('orient', 'auto');
|
|
968
|
-
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
969
|
-
p.setAttribute('d', 'M 0 0 L 6 4 L 0 8 z'); p.setAttribute('fill', color);
|
|
970
|
-
m.appendChild(p); defs.appendChild(m);
|
|
971
|
-
return markerId;
|
|
688
|
+
drawRakeBundles(svg, pos, {
|
|
689
|
+
isDark: uiStore.isDark,
|
|
690
|
+
color: uiStore.isDark ? '#2dd4bf' : '#0d9488',
|
|
691
|
+
isMuted: mutedTypes.value.has('__partitive__'),
|
|
692
|
+
relations: props.partitiveRelations ?? [],
|
|
693
|
+
});
|
|
972
694
|
}
|
|
973
695
|
|
|
974
696
|
/* ── Render DOM nodes ───────────────────────────────────── */
|