@glossarist/concept-browser 0.7.84 → 0.7.85
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 +163 -81
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.85",
|
|
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": {
|
|
@@ -537,11 +537,19 @@ function linkForce() {
|
|
|
537
537
|
if (links.length === 0) return;
|
|
538
538
|
const target = linkDistance;
|
|
539
539
|
const strength = linkStrength;
|
|
540
|
+
/* Read muted sets each tick so toggleType/toggleRegister take effect
|
|
541
|
+
immediately on the next simulation step. */
|
|
542
|
+
const mutedT = mutedTypes.value;
|
|
543
|
+
const mutedR = mutedRegisters.value;
|
|
540
544
|
for (const link of links) {
|
|
545
|
+
if (mutedT.has(link.type)) continue;
|
|
541
546
|
const ai = idToIdx.get(link.source);
|
|
542
547
|
const bi = idToIdx.get(link.target);
|
|
543
548
|
if (ai === undefined || bi === undefined) continue;
|
|
544
549
|
const a = nL[ai], b = nL[bi];
|
|
550
|
+
/* Skip edges involving a muted register (unless the node is the focus) */
|
|
551
|
+
if (a.depth !== 0 && mutedR.has(a.register)) continue;
|
|
552
|
+
if (b.depth !== 0 && mutedR.has(b.register)) continue;
|
|
545
553
|
const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
|
|
546
554
|
const d2 = dx*dx + dy*dy + dz*dz;
|
|
547
555
|
if (d2 < 1e-6) continue;
|
|
@@ -752,38 +760,41 @@ function drawEdges(cx: number, cy: number) {
|
|
|
752
760
|
}
|
|
753
761
|
|
|
754
762
|
/**
|
|
755
|
-
* Render PartitiveRelations as ISO 704 rake
|
|
763
|
+
* Render PartitiveRelations as ISO 704 right-angle rake brackets.
|
|
756
764
|
*
|
|
757
|
-
* Each relation renders as a
|
|
765
|
+
* Each relation renders as a true bracket with 90° angles:
|
|
758
766
|
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
+
* [comprehensive]
|
|
768
|
+
* │
|
|
769
|
+
* │ ← stem (along stem-axis)
|
|
770
|
+
* │
|
|
771
|
+
* ┌──────────●──────────┐ ← spine (perpendicular to stem-axis; right angle at junction)
|
|
772
|
+
* │ │ │
|
|
773
|
+
* │ │ │ ← drops (parallel to stem-axis; right angle at each tooth)
|
|
774
|
+
* [p1] [p2] [p3]
|
|
767
775
|
*
|
|
768
|
-
*
|
|
769
|
-
* comprehensive to
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
776
|
+
* Geometry:
|
|
777
|
+
* 1. stem-axis = direction from comprehensive to centroid of partitives
|
|
778
|
+
* 2. spine-axis = perpendicular to stem-axis
|
|
779
|
+
* 3. spine-center = point along stem-axis at SPINE_FRACTION (60%) from comp
|
|
780
|
+
* 4. For each partitive Pi:
|
|
781
|
+
* - Project Pi onto spine-axis → tooth position
|
|
782
|
+
* - Drop goes from tooth straight to Pi (parallel to stem-axis by construction)
|
|
773
783
|
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
* - plurality.isShared && !isUncertain : two parallel solid lines
|
|
777
|
-
* (close-set; offset ±DOUBLE_GAP/2)
|
|
778
|
-
* - plurality.isShared && isUncertain : one solid + one dashed line
|
|
784
|
+
* All bends are exactly 90° because tooth_i is the foot of perpendicular
|
|
785
|
+
* from Pi to the spine line.
|
|
779
786
|
*
|
|
780
|
-
*
|
|
781
|
-
* -
|
|
782
|
-
* -
|
|
783
|
-
*
|
|
784
|
-
*
|
|
787
|
+
* ISO 704 line conventions:
|
|
788
|
+
* - default : single solid line per segment
|
|
789
|
+
* - plurality.isShared && !isUncertain : TWO parallel solid lines (close-set)
|
|
790
|
+
* - plurality.isShared && isUncertain : one solid + one dashed line
|
|
791
|
+
* - completeness === 'partial' : spine extends past last tooth
|
|
792
|
+
* (continued backline — "more exist")
|
|
785
793
|
*
|
|
786
|
-
*
|
|
794
|
+
* Per-member certainty 'possible' → that drop is dashed + reduced opacity
|
|
795
|
+
* (stem and spine keep relation style).
|
|
796
|
+
*
|
|
797
|
+
* Diamond marker at the comprehensive end signals the hyperedge origin.
|
|
787
798
|
*/
|
|
788
799
|
function drawRakeBundles(
|
|
789
800
|
svg: SVGSVGElement,
|
|
@@ -791,9 +802,12 @@ function drawRakeBundles(
|
|
|
791
802
|
_cx: number,
|
|
792
803
|
_cy: number,
|
|
793
804
|
) {
|
|
805
|
+
if (mutedTypes.value.has('__partitive__')) return;
|
|
794
806
|
const color = uiStore.isDark ? '#2dd4bf' : '#0d9488';
|
|
795
807
|
const DOUBLE_GAP = 4;
|
|
796
808
|
const DASH = '4 3';
|
|
809
|
+
const SPINE_FRACTION = 0.55; // spine sits 55% from comp toward centroid
|
|
810
|
+
const PARTIAL_TAIL = 28; // px extension for completeness: 'partial'
|
|
797
811
|
|
|
798
812
|
for (const rel of props.partitiveRelations ?? []) {
|
|
799
813
|
const compParsed = UriRouter.parseUri(rel.comprehensive);
|
|
@@ -814,17 +828,51 @@ function drawRakeBundles(
|
|
|
814
828
|
|
|
815
829
|
if (memberPositions.length === 0) continue;
|
|
816
830
|
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
const
|
|
831
|
+
// 1. Compute stem-axis (comp → centroid of partitives) and spine-axis (perpendicular)
|
|
832
|
+
const centroidX = memberPositions.reduce((s, m) => s + m.pos.x, 0) / memberPositions.length;
|
|
833
|
+
const centroidY = memberPositions.reduce((s, m) => s + m.pos.y, 0) / memberPositions.length;
|
|
834
|
+
const stemDx = centroidX - compPos.x;
|
|
835
|
+
const stemDy = centroidY - compPos.y;
|
|
836
|
+
const stemLen = Math.hypot(stemDx, stemDy) || 1;
|
|
837
|
+
const stemDirX = stemDx / stemLen; // unit vector along stem
|
|
838
|
+
const stemDirY = stemDy / stemLen;
|
|
839
|
+
const spineDirX = -stemDirY; // perpendicular = spine direction
|
|
840
|
+
const spineDirY = stemDirX;
|
|
841
|
+
|
|
842
|
+
// 2. Spine center: along stem-axis at SPINE_FRACTION from comp
|
|
843
|
+
const spineCx = compPos.x + stemDirX * stemLen * SPINE_FRACTION;
|
|
844
|
+
const spineCy = compPos.y + stemDirY * stemLen * SPINE_FRACTION;
|
|
845
|
+
|
|
846
|
+
// 3. Tooth positions = projection of each partitive onto spine line
|
|
847
|
+
const teeth = memberPositions.map(({ member, pos: pPos }) => {
|
|
848
|
+
const vx = pPos.x - spineCx;
|
|
849
|
+
const vy = pPos.y - spineCy;
|
|
850
|
+
const along = vx * spineDirX + vy * spineDirY;
|
|
851
|
+
const toothX = spineCx + spineDirX * along;
|
|
852
|
+
const toothY = spineCy + spineDirY * along;
|
|
853
|
+
return { member, partitivePos: pPos, toothX, toothY, along };
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
// 4. Spine extents (leftmost to rightmost tooth along spine-axis)
|
|
857
|
+
let minAlong = teeth[0].along;
|
|
858
|
+
let maxAlong = teeth[0].along;
|
|
859
|
+
for (const t of teeth) {
|
|
860
|
+
if (t.along < minAlong) minAlong = t.along;
|
|
861
|
+
if (t.along > maxAlong) maxAlong = t.along;
|
|
862
|
+
}
|
|
863
|
+
// For completeness: 'partial', extend past last tooth (continued backline)
|
|
864
|
+
if (rel.completeness === 'partial') maxAlong += PARTIAL_TAIL;
|
|
865
|
+
const spineStartX = spineCx + spineDirX * minAlong;
|
|
866
|
+
const spineStartY = spineCy + spineDirY * minAlong;
|
|
867
|
+
const spineEndX = spineCx + spineDirX * maxAlong;
|
|
868
|
+
const spineEndY = spineCy + spineDirY * maxAlong;
|
|
821
869
|
|
|
822
870
|
const isShared = !!rel.plurality?.isShared;
|
|
823
871
|
const isUncertain = !!rel.plurality?.isUncertain;
|
|
824
872
|
|
|
825
873
|
/**
|
|
826
|
-
* Draw a single straight
|
|
827
|
-
*
|
|
874
|
+
* Draw a single straight segment with optional parallel offset
|
|
875
|
+
* (used to render the close-set double line) and dash style.
|
|
828
876
|
*/
|
|
829
877
|
const drawSegment = (
|
|
830
878
|
ax: number, ay: number, bx: number, by: number,
|
|
@@ -833,42 +881,27 @@ function drawRakeBundles(
|
|
|
833
881
|
const offset = opts.offset ?? 0;
|
|
834
882
|
const dashed = opts.dashed ?? false;
|
|
835
883
|
const opacity = opts.opacity ?? 0.85;
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
const len = Math.hypot(dx, dy) || 1;
|
|
853
|
-
const px = -dy / len, py = dx / len;
|
|
854
|
-
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
855
|
-
line.setAttribute('class', 'rake-seg');
|
|
856
|
-
line.setAttribute('x1', String(ax + px * offset));
|
|
857
|
-
line.setAttribute('y1', String(ay + py * offset));
|
|
858
|
-
line.setAttribute('x2', String(bx + px * offset));
|
|
859
|
-
line.setAttribute('y2', String(by + py * offset));
|
|
860
|
-
line.setAttribute('stroke', color);
|
|
861
|
-
line.setAttribute('stroke-width', '1.6');
|
|
862
|
-
line.setAttribute('opacity', String(opacity));
|
|
863
|
-
line.setAttribute('stroke-linecap', 'round');
|
|
864
|
-
if (dashed) line.setAttribute('stroke-dasharray', DASH);
|
|
865
|
-
svg.appendChild(line);
|
|
866
|
-
}
|
|
884
|
+
const dx = bx - ax, dy = by - ay;
|
|
885
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
886
|
+
// Perpendicular to THIS segment (not the spine-axis) for parallel offset
|
|
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', '1.6');
|
|
896
|
+
line.setAttribute('opacity', String(opacity));
|
|
897
|
+
line.setAttribute('stroke-linecap', 'square'); // sharp corners at right angles
|
|
898
|
+
if (dashed) line.setAttribute('stroke-dasharray', DASH);
|
|
899
|
+
svg.appendChild(line);
|
|
867
900
|
};
|
|
868
901
|
|
|
869
902
|
/**
|
|
870
|
-
* Draw a segment with the relation's plurality style
|
|
871
|
-
*
|
|
903
|
+
* Draw a segment with the relation's plurality style.
|
|
904
|
+
* Single solid (default), double solid (isShared),
|
|
872
905
|
* or solid + dashed (isShared + isUncertain).
|
|
873
906
|
*/
|
|
874
907
|
const drawSegmentWithPlurality = (
|
|
@@ -879,7 +912,6 @@ function drawRakeBundles(
|
|
|
879
912
|
if (!isShared) {
|
|
880
913
|
drawSegment(ax, ay, bx, by, { opacity, dashed: opts.perMemberPossible });
|
|
881
914
|
} else {
|
|
882
|
-
// Close-set double: two parallel lines offset ±DOUBLE_GAP/2
|
|
883
915
|
drawSegment(ax, ay, bx, by, { offset: -DOUBLE_GAP / 2, opacity });
|
|
884
916
|
drawSegment(ax, ay, bx, by, {
|
|
885
917
|
offset: DOUBLE_GAP / 2,
|
|
@@ -889,30 +921,36 @@ function drawRakeBundles(
|
|
|
889
921
|
}
|
|
890
922
|
};
|
|
891
923
|
|
|
892
|
-
//
|
|
893
|
-
drawSegmentWithPlurality(compPos.x, compPos.y,
|
|
924
|
+
// 5. Stem: comp → spine center (along stem-axis)
|
|
925
|
+
drawSegmentWithPlurality(compPos.x, compPos.y, spineCx, spineCy);
|
|
894
926
|
|
|
895
|
-
//
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
927
|
+
// 6. Spine: leftmost tooth → rightmost tooth (along spine-axis)
|
|
928
|
+
// (skip if only one tooth — no horizontal extent)
|
|
929
|
+
if (Math.abs(maxAlong - minAlong) > 1) {
|
|
930
|
+
drawSegmentWithPlurality(spineStartX, spineStartY, spineEndX, spineEndY);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// 7. Drops: tooth → partitive (parallel to stem-axis by construction)
|
|
934
|
+
for (const tooth of teeth) {
|
|
935
|
+
drawSegmentWithPlurality(tooth.toothX, tooth.toothY, tooth.partitivePos.x, tooth.partitivePos.y, {
|
|
936
|
+
opacity: tooth.member.certainty === 'possible' ? 0.5 : 0.85,
|
|
937
|
+
perMemberPossible: tooth.member.certainty === 'possible',
|
|
900
938
|
});
|
|
901
939
|
}
|
|
902
940
|
|
|
903
|
-
//
|
|
941
|
+
// 8. Junction marker at spine center (the right-angle corner)
|
|
904
942
|
const junction = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
905
943
|
junction.setAttribute('class', 'rake-junction');
|
|
906
|
-
junction.setAttribute('cx', String(
|
|
907
|
-
junction.setAttribute('cy', String(
|
|
908
|
-
junction.setAttribute('r', '
|
|
944
|
+
junction.setAttribute('cx', String(spineCx));
|
|
945
|
+
junction.setAttribute('cy', String(spineCy));
|
|
946
|
+
junction.setAttribute('r', '2.5');
|
|
909
947
|
junction.setAttribute('fill', color);
|
|
910
|
-
junction.setAttribute('opacity', '0.
|
|
948
|
+
junction.setAttribute('opacity', '0.9');
|
|
911
949
|
junction.setAttribute('stroke', uiStore.isDark ? '#0a1f1c' : '#ffffff');
|
|
912
950
|
junction.setAttribute('stroke-width', '1');
|
|
913
951
|
svg.appendChild(junction);
|
|
914
952
|
|
|
915
|
-
//
|
|
953
|
+
// 9. Diamond marker at comprehensive end (hyperedge origin)
|
|
916
954
|
const diamond = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
917
955
|
const dsize = 5;
|
|
918
956
|
diamond.setAttribute('d',
|
|
@@ -1249,6 +1287,8 @@ watch(() => [props.concept?.id, props.registerId] as const, ([newId, newReg]) =>
|
|
|
1249
1287
|
/* ── Legend — grouped by relation TYPE (not category) ───── */
|
|
1250
1288
|
const legendItems = computed(() => {
|
|
1251
1289
|
graphVersion.value; /* reactive dependency */
|
|
1290
|
+
uiStore.isDark; /* re-evaluate when dark mode toggles (color depends on it) */
|
|
1291
|
+
mutedTypes.value; /* re-evaluate when mute set changes */
|
|
1252
1292
|
const typeMap = new Map<string, { type: string; label: string; color: string; count: number }>();
|
|
1253
1293
|
for (const link of links) {
|
|
1254
1294
|
/* Per-type color (with override) so e.g. 'see' and 'references' differ */
|
|
@@ -1261,6 +1301,18 @@ const legendItems = computed(() => {
|
|
|
1261
1301
|
}
|
|
1262
1302
|
typeMap.get(key)!.count++;
|
|
1263
1303
|
}
|
|
1304
|
+
/* Synthetic entry for PartitiveRelations (hyperedge rake bundles).
|
|
1305
|
+
These don't appear in `links` (they're rendered separately), so we
|
|
1306
|
+
inject a legend entry when any are present. */
|
|
1307
|
+
const partitiveCount = props.partitiveRelations?.length ?? 0;
|
|
1308
|
+
if (partitiveCount > 0) {
|
|
1309
|
+
typeMap.set('__partitive__', {
|
|
1310
|
+
type: '__partitive__',
|
|
1311
|
+
label: 'partitive relation',
|
|
1312
|
+
color: uiStore.isDark ? '#2dd4bf' : '#0d9488',
|
|
1313
|
+
count: partitiveCount,
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1264
1316
|
return Array.from(typeMap.values()).sort((a, b) => b.count - a.count);
|
|
1265
1317
|
});
|
|
1266
1318
|
|
|
@@ -1285,6 +1337,9 @@ function toggleRegister(reg: string) {
|
|
|
1285
1337
|
renderDOM();
|
|
1286
1338
|
const c = canvasRef.value;
|
|
1287
1339
|
if (c) drawEdges(c.clientWidth / 2, c.clientHeight * 0.46);
|
|
1340
|
+
/* Re-heat the simulation so nodes "feel" the freed/hidden edges and
|
|
1341
|
+
drift to a new equilibrium instead of staying frozen. */
|
|
1342
|
+
if (sim) sim.alpha(0.5).restart();
|
|
1288
1343
|
}
|
|
1289
1344
|
|
|
1290
1345
|
async function changeDegree(d: 1 | 2 | 3) {
|
|
@@ -1402,6 +1457,10 @@ function toggleType(type: string) {
|
|
|
1402
1457
|
mutedTypes.value = n;
|
|
1403
1458
|
const c = canvasRef.value;
|
|
1404
1459
|
if (c) drawEdges(c.clientWidth / 2, c.clientHeight * 0.46);
|
|
1460
|
+
/* Re-heat the simulation so nodes drift to a new equilibrium with
|
|
1461
|
+
the freed/hidden edges. The link force reads mutedTypes each tick,
|
|
1462
|
+
so toggling effectively "removes" the edge from the force calc. */
|
|
1463
|
+
if (sim) sim.alpha(0.5).restart();
|
|
1405
1464
|
}
|
|
1406
1465
|
|
|
1407
1466
|
function isTypeMuted(type: string): boolean {
|
|
@@ -1574,9 +1633,12 @@ defineExpose({ navigate });
|
|
|
1574
1633
|
<div class="sp-section-label">{{ t('sphere.relationTypes') }}</div>
|
|
1575
1634
|
<div class="sp-legend-grid">
|
|
1576
1635
|
<button v-for="item in legendItems" :key="item.type" class="sp-legend-item"
|
|
1577
|
-
:class="{ muted: isTypeMuted(item.type)
|
|
1636
|
+
:class="{ muted: isTypeMuted(item.type), 'is-partitive': item.type === '__partitive__' }"
|
|
1637
|
+
@click="toggleType(item.type)"
|
|
1578
1638
|
:title="item.type">
|
|
1579
|
-
<span
|
|
1639
|
+
<span v-if="item.type === '__partitive__'" class="sp-swatch sp-swatch-rake"
|
|
1640
|
+
:style="{ '--rake-color': isTypeMuted(item.type) ? '#b8b9cc' : item.color }"></span>
|
|
1641
|
+
<span v-else class="sp-swatch" :style="{ background: isTypeMuted(item.type) ? '#b8b9cc' : item.color }"></span>
|
|
1580
1642
|
<span>{{ item.label }}</span>
|
|
1581
1643
|
<span class="sp-count">{{ item.count }}</span>
|
|
1582
1644
|
</button>
|
|
@@ -1825,6 +1887,26 @@ defineExpose({ navigate });
|
|
|
1825
1887
|
.sp-legend-item:hover { background: var(--rule); }
|
|
1826
1888
|
.sp-legend-item.muted { opacity: 0.35; }
|
|
1827
1889
|
.sp-swatch { width: 18px; height: 2px; flex-shrink: 0; border-radius: 1px; }
|
|
1890
|
+
/* Rake swatch — small bracket icon signaling a PartitiveRelation hyperedge
|
|
1891
|
+
(not a single binary edge). Two parallel solid lines (close-set double)
|
|
1892
|
+
with a perpendicular stem, evoking the ISO 704 rake convention. */
|
|
1893
|
+
.sp-swatch-rake {
|
|
1894
|
+
width: 18px; height: 10px; position: relative; background: transparent !important;
|
|
1895
|
+
border-radius: 0;
|
|
1896
|
+
}
|
|
1897
|
+
.sp-swatch-rake::before,
|
|
1898
|
+
.sp-swatch-rake::after {
|
|
1899
|
+
content: ''; position: absolute; background: var(--rake-color, #0d9488);
|
|
1900
|
+
}
|
|
1901
|
+
.sp-swatch-rake::before {
|
|
1902
|
+
/* horizontal spine (close-set double line) */
|
|
1903
|
+
left: 4px; right: 0; top: 3px; height: 1.5px;
|
|
1904
|
+
box-shadow: 0 3px 0 var(--rake-color, #0d9488);
|
|
1905
|
+
}
|
|
1906
|
+
.sp-swatch-rake::after {
|
|
1907
|
+
/* vertical stem from top of swatch to spine */
|
|
1908
|
+
left: 4px; width: 1.5px; top: 0; height: 4px;
|
|
1909
|
+
}
|
|
1828
1910
|
.sp-count { margin-left: auto; font-family: 'JetBrains Mono', monospace; font-size: 10px; }
|
|
1829
1911
|
|
|
1830
1912
|
.sp-preview {
|