@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.84",
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/bracket bundles in the sphere.
763
+ * Render PartitiveRelations as ISO 704 right-angle rake brackets.
756
764
  *
757
- * Each relation renders as a real bracket:
765
+ * Each relation renders as a true bracket with 90° angles:
758
766
  *
759
- * [comprehensive]
760
- * |
761
- * | ← single stem (vertical/radial)
762
- * |
763
- * +---•---+ ← spine midpoint (junction)
764
- * | | |
765
- * | | | ← N branches from spine midpoint
766
- * [p1] [p2] [p3] (one per partitive)
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
- * In 3D the bracket's "vertical" direction is along the line from
769
- * comprehensive to the centroid of partitives. The branches fan out
770
- * from the spine midpoint to each partitive. Right angles are not
771
- * enforceable in a force-directed layout; instead we use a single
772
- * shared junction point so the eye reads "ONE relation, N branches".
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
- * ISO 704 line conventions applied (each segment: stem + spine + branches):
775
- * - default : single solid line
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
- * Per-member certainty:
781
- * - confirmed : branch is solid (with the plurality style above)
782
- * - possible : branch is dashed + reduced opacity (overrides plurality
783
- * on that branch only; the stem + spine keep the relation
784
- * style)
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
- * A diamond marker at the comprehensive end signals the hyperedge origin.
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
- // Spine midpoint = centroid of comprehensive + all partitives.
818
- // This is the junction where the stem meets the branches.
819
- const cx_part = (compPos.x + memberPositions.reduce((s, m) => s + m.pos.x, 0)) / (memberPositions.length + 1);
820
- const cy_part = (compPos.y + memberPositions.reduce((s, m) => s + m.pos.y, 0)) / (memberPositions.length + 1);
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 line segment with optional parallel offset
827
- * and dash style. Used for both stem and branches.
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
- if (offset === 0) {
837
- const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
838
- line.setAttribute('class', 'rake-seg');
839
- line.setAttribute('x1', String(ax));
840
- line.setAttribute('y1', String(ay));
841
- line.setAttribute('x2', String(bx));
842
- line.setAttribute('y2', String(by));
843
- line.setAttribute('stroke', color);
844
- line.setAttribute('stroke-width', '1.6');
845
- line.setAttribute('opacity', String(opacity));
846
- line.setAttribute('stroke-linecap', 'round');
847
- if (dashed) line.setAttribute('stroke-dasharray', DASH);
848
- svg.appendChild(line);
849
- } else {
850
- // Compute perpendicular unit vector for parallel offset
851
- const dx = bx - ax, dy = by - ay;
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
- * single solid (default), double solid (isShared),
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
- // 1. Stem: comprehensive → spine midpoint
893
- drawSegmentWithPlurality(compPos.x, compPos.y, cx_part, cy_part);
924
+ // 5. Stem: comp → spine center (along stem-axis)
925
+ drawSegmentWithPlurality(compPos.x, compPos.y, spineCx, spineCy);
894
926
 
895
- // 2. Branches: spine midpoint → each partitive
896
- for (const { member, pos: pPos } of memberPositions) {
897
- drawSegmentWithPlurality(cx_part, cy_part, pPos.x, pPos.y, {
898
- opacity: member.certainty === 'possible' ? 0.45 : 0.85,
899
- perMemberPossible: member.certainty === 'possible',
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
- // 3. Junction marker at spine midpoint (small filled circle)
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(cx_part));
907
- junction.setAttribute('cy', String(cy_part));
908
- junction.setAttribute('r', '3');
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.85');
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
- // 4. Diamond marker at comprehensive end (hyperedge origin signal)
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) }" @click="toggleType(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 class="sp-swatch" :style="{ background: isTypeMuted(item.type) ? '#b8b9cc' : item.color }"></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 {