@glossarist/concept-browser 0.7.83 → 0.7.84

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.83",
3
+ "version": "0.7.84",
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": {
@@ -630,8 +630,10 @@ function drawEdges(cx: number, cy: number) {
630
630
  svg.setAttribute('height', String(h));
631
631
  svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
632
632
  svg.setAttribute('preserveAspectRatio', 'none');
633
- /* Remove only path + text + rect (edges), keep defs (markers) */
634
- svg.querySelectorAll('path:not(defs path), text.edge-label, rect.edge-label-bg').forEach(el => el.remove());
633
+ /* Remove only path + text + rect (edges), keep defs (markers).
634
+ Also remove <line> elements (rake-bundle segments) and <circle>
635
+ (rake junction markers). */
636
+ svg.querySelectorAll('path:not(defs path), line.rake-seg, circle.rake-junction, text.edge-label, rect.edge-label-bg').forEach(el => el.remove());
635
637
  if (!svg.querySelector('defs')) ensureMarkers(svg);
636
638
 
637
639
  /* Compute visible node positions */
@@ -750,19 +752,36 @@ function drawEdges(cx: number, cy: number) {
750
752
  }
751
753
 
752
754
  /**
753
- * Render PartitiveRelations as ISO 704 rake bundles in the sphere.
755
+ * Render PartitiveRelations as ISO 704 rake/bracket bundles in the sphere.
754
756
  *
755
- * In 3D space the literal "right-angle bracket" rake doesn't apply,
756
- * so we use a bundle of N curves fanning out from the comprehensive,
757
- * all sharing visual styling. The bundle communicates "these N edges
758
- * are ONE relation", not N independent pairwise links.
757
+ * Each relation renders as a real bracket:
759
758
  *
760
- * ISO 704 line conventions applied:
761
- * - default : single solid curve per member
762
- * - plurality.isShared && !isUncertain : two parallel solid curves (close-set)
763
- * - plurality.isShared && isUncertain : one solid + one dashed curve
764
- * - per-member certainty: 'possible' : the member's curve is dashed
765
- * (regardless of plurality)
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
+ *
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".
773
+ *
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
779
+ *
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)
766
785
  *
767
786
  * A diamond marker at the comprehensive end signals the hyperedge origin.
768
787
  */
@@ -772,6 +791,10 @@ function drawRakeBundles(
772
791
  _cx: number,
773
792
  _cy: number,
774
793
  ) {
794
+ const color = uiStore.isDark ? '#2dd4bf' : '#0d9488';
795
+ const DOUBLE_GAP = 4;
796
+ const DASH = '4 3';
797
+
775
798
  for (const rel of props.partitiveRelations ?? []) {
776
799
  const compParsed = UriRouter.parseUri(rel.comprehensive);
777
800
  if (!compParsed) continue;
@@ -791,58 +814,111 @@ function drawRakeBundles(
791
814
 
792
815
  if (memberPositions.length === 0) continue;
793
816
 
794
- const color = uiStore.isDark ? '#2dd4bf' : '#0d9488'; // teal — matches sidebar rake
795
- const DOUBLE_GAP = 4;
796
- const DASH = '4 3';
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);
821
+
797
822
  const isShared = !!rel.plurality?.isShared;
798
823
  const isUncertain = !!rel.plurality?.isUncertain;
799
- const drawDouble = isShared;
800
- const secondDashed = isUncertain;
801
824
 
802
- for (const { member, pos: pPos } of memberPositions) {
803
- const dx = pPos.x - compPos.x;
804
- const dy = pPos.y - compPos.y;
805
- const len = Math.hypot(dx, dy) || 1;
806
- const px = -dy / len; // perpendicular unit
807
- const py = dx / len;
808
-
809
- const drawLine = (offset: number, dashed: boolean) => {
810
- const ax = compPos.x + px * offset;
811
- const ay = compPos.y + py * offset;
812
- const bx = pPos.x + px * offset;
813
- const by = pPos.y + py * offset;
814
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
815
- // Quadratic bezier toward a control point biased outward from the focus
816
- // to suggest the "rake handle" shape.
817
- const mx = (ax + bx) / 2;
818
- const my = (ay + by) / 2;
819
- const ctrlLen = len * 0.15;
820
- path.setAttribute('d', `M ${ax} ${ay} Q ${mx + px * ctrlLen} ${my + py * ctrlLen} ${bx} ${by}`);
821
- path.setAttribute('stroke', color);
822
- path.setAttribute('stroke-width', '1.5');
823
- path.setAttribute('opacity', member.certainty === 'possible' ? '0.45' : '0.8');
824
- path.setAttribute('fill', 'none');
825
- path.setAttribute('stroke-linecap', 'round');
826
- if (dashed || member.certainty === 'possible') {
827
- path.setAttribute('stroke-dasharray', DASH);
828
- }
829
- svg.appendChild(path);
830
- };
825
+ /**
826
+ * Draw a single straight line segment with optional parallel offset
827
+ * and dash style. Used for both stem and branches.
828
+ */
829
+ const drawSegment = (
830
+ ax: number, ay: number, bx: number, by: number,
831
+ opts: { offset?: number; dashed?: boolean; opacity?: number } = {},
832
+ ) => {
833
+ const offset = opts.offset ?? 0;
834
+ const dashed = opts.dashed ?? false;
835
+ 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
+ }
867
+ };
831
868
 
832
- drawLine(0, false);
833
- if (drawDouble) {
834
- drawLine(DOUBLE_GAP, secondDashed);
835
- drawLine(-DOUBLE_GAP, secondDashed);
869
+ /**
870
+ * Draw a segment with the relation's plurality style:
871
+ * single solid (default), double solid (isShared),
872
+ * or solid + dashed (isShared + isUncertain).
873
+ */
874
+ const drawSegmentWithPlurality = (
875
+ ax: number, ay: number, bx: number, by: number,
876
+ opts: { opacity?: number; perMemberPossible?: boolean } = {},
877
+ ) => {
878
+ const opacity = opts.opacity ?? 0.85;
879
+ if (!isShared) {
880
+ drawSegment(ax, ay, bx, by, { opacity, dashed: opts.perMemberPossible });
881
+ } else {
882
+ // Close-set double: two parallel lines offset ±DOUBLE_GAP/2
883
+ drawSegment(ax, ay, bx, by, { offset: -DOUBLE_GAP / 2, opacity });
884
+ drawSegment(ax, ay, bx, by, {
885
+ offset: DOUBLE_GAP / 2,
886
+ opacity,
887
+ dashed: isUncertain || opts.perMemberPossible,
888
+ });
836
889
  }
890
+ };
891
+
892
+ // 1. Stem: comprehensive → spine midpoint
893
+ drawSegmentWithPlurality(compPos.x, compPos.y, cx_part, cy_part);
894
+
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',
900
+ });
837
901
  }
838
902
 
839
- /* Diamond marker at comprehensive end — signals "rake origin" */
903
+ // 3. Junction marker at spine midpoint (small filled circle)
904
+ const junction = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
905
+ junction.setAttribute('class', 'rake-junction');
906
+ junction.setAttribute('cx', String(cx_part));
907
+ junction.setAttribute('cy', String(cy_part));
908
+ junction.setAttribute('r', '3');
909
+ junction.setAttribute('fill', color);
910
+ junction.setAttribute('opacity', '0.85');
911
+ junction.setAttribute('stroke', uiStore.isDark ? '#0a1f1c' : '#ffffff');
912
+ junction.setAttribute('stroke-width', '1');
913
+ svg.appendChild(junction);
914
+
915
+ // 4. Diamond marker at comprehensive end (hyperedge origin signal)
840
916
  const diamond = document.createElementNS('http://www.w3.org/2000/svg', 'path');
841
917
  const dsize = 5;
842
918
  diamond.setAttribute('d',
843
919
  `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`);
844
920
  diamond.setAttribute('fill', color);
845
- diamond.setAttribute('opacity', '0.85');
921
+ diamond.setAttribute('opacity', '0.9');
846
922
  diamond.setAttribute('stroke', uiStore.isDark ? '#0a1f1c' : '#ffffff');
847
923
  diamond.setAttribute('stroke-width', '1');
848
924
  svg.appendChild(diamond);