@glossarist/concept-browser 0.7.84 → 0.7.86
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 +2 -2
- package/scripts/build-edges.js +42 -26
- package/scripts/generate-data.mjs +45 -5
- package/scripts/migrate-v1-to-v3.d.mts +6 -0
- package/scripts/migrate-v1-to-v3.mjs +176 -0
- package/src/__tests__/components/PartitiveRelationDiagram.test.ts +92 -136
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +115 -87
- package/src/__tests__/partitive-relation-styling.test.ts +12 -54
- package/src/__tests__/partitive-relation.test.ts +42 -90
- package/src/__tests__/scripts/migrate-v1-to-v3.d.mts +8 -0
- package/src/__tests__/scripts/migrate-v1-to-v3.test.ts +115 -0
- package/src/__tests__/utils/partitive-multiplicity.test.ts +113 -0
- package/src/adapters/model-bridge.ts +15 -19
- package/src/adapters/non-verbal/glossarist-augment.d.ts +34 -18
- package/src/adapters/types.ts +2 -8
- package/src/components/PartitiveRelationDiagram.vue +88 -76
- package/src/components/PartitiveRelationList.vue +26 -34
- package/src/components/RelationSphere.vue +173 -106
- package/src/composables/use-concept-edges.ts +21 -21
- package/src/utils/partitive-multiplicity.ts +106 -0
- package/src/utils/partitive-relation-styling.ts +29 -38
- package/src/views/ConceptView.vue +7 -13
- package/src/__tests__/model-bridge-partitive.test.ts +0 -146
|
@@ -26,6 +26,7 @@ import { useI18n } from '../i18n';
|
|
|
26
26
|
|
|
27
27
|
const { t, locale } = useI18n();
|
|
28
28
|
import type { Concept, Manifest, GraphEdge, PartitiveRelationWire } from '../adapters/types';
|
|
29
|
+
import { rakeStrokeStyle, type PartitiveMultiplicity } from '../utils/partitive-multiplicity';
|
|
29
30
|
import { conceptUri } from '../adapters/model-bridge';
|
|
30
31
|
|
|
31
32
|
const props = defineProps<{
|
|
@@ -537,11 +538,19 @@ function linkForce() {
|
|
|
537
538
|
if (links.length === 0) return;
|
|
538
539
|
const target = linkDistance;
|
|
539
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;
|
|
540
545
|
for (const link of links) {
|
|
546
|
+
if (mutedT.has(link.type)) continue;
|
|
541
547
|
const ai = idToIdx.get(link.source);
|
|
542
548
|
const bi = idToIdx.get(link.target);
|
|
543
549
|
if (ai === undefined || bi === undefined) continue;
|
|
544
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;
|
|
545
554
|
const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
|
|
546
555
|
const d2 = dx*dx + dy*dy + dz*dz;
|
|
547
556
|
if (d2 < 1e-6) continue;
|
|
@@ -752,38 +761,33 @@ function drawEdges(cx: number, cy: number) {
|
|
|
752
761
|
}
|
|
753
762
|
|
|
754
763
|
/**
|
|
755
|
-
* Render PartitiveRelations as ISO 704 rake
|
|
764
|
+
* Render PartitiveRelations as ISO 704:2022 rake brackets in the sphere.
|
|
756
765
|
*
|
|
757
|
-
* Each relation renders as a
|
|
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.
|
|
758
769
|
*
|
|
759
|
-
*
|
|
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)
|
|
770
|
+
* Per-member multiplicity + is_delimiting is encoded on each DROP via
|
|
771
|
+
* rakeStrokeStyle() (see utils/partitive-multiplicity.ts):
|
|
767
772
|
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
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
|
|
773
785
|
*
|
|
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
|
|
786
|
+
* completeness === 'partial' : spine extends past last tooth (continued
|
|
787
|
+
* backline — "more exist but not shown")
|
|
779
788
|
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
* - possible : branch is dashed + reduced opacity (overrides plurality
|
|
783
|
-
* on that branch only; the stem + spine keep the relation
|
|
784
|
-
* style)
|
|
785
|
-
*
|
|
786
|
-
* A diamond marker at the comprehensive end signals the hyperedge origin.
|
|
789
|
+
* Diamond marker at the comprehensive end signals the hyperedge origin.
|
|
790
|
+
* Junction marker at the spine midpoint signals the right-angle corner.
|
|
787
791
|
*/
|
|
788
792
|
function drawRakeBundles(
|
|
789
793
|
svg: SVGSVGElement,
|
|
@@ -791,9 +795,13 @@ function drawRakeBundles(
|
|
|
791
795
|
_cx: number,
|
|
792
796
|
_cy: number,
|
|
793
797
|
) {
|
|
798
|
+
if (mutedTypes.value.has('__partitive__')) return;
|
|
794
799
|
const color = uiStore.isDark ? '#2dd4bf' : '#0d9488';
|
|
795
800
|
const DOUBLE_GAP = 4;
|
|
796
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'
|
|
797
805
|
|
|
798
806
|
for (const rel of props.partitiveRelations ?? []) {
|
|
799
807
|
const compParsed = UriRouter.parseUri(rel.comprehensive);
|
|
@@ -810,109 +818,124 @@ function drawRakeBundles(
|
|
|
810
818
|
const p = pos.get(nid);
|
|
811
819
|
return p ? { member, pos: p } : null;
|
|
812
820
|
})
|
|
813
|
-
.filter((x): x is { member: { uri: string;
|
|
821
|
+
.filter((x): x is { member: { uri: string; multiplicity: PartitiveMultiplicity; isDelimiting: boolean }; pos: {x: number; y: number} } => x !== null);
|
|
814
822
|
|
|
815
823
|
if (memberPositions.length === 0) continue;
|
|
816
824
|
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
const
|
|
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.multiplicity, member.isDelimiting),
|
|
854
|
+
};
|
|
855
|
+
});
|
|
821
856
|
|
|
822
|
-
|
|
823
|
-
|
|
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;
|
|
824
870
|
|
|
825
871
|
/**
|
|
826
|
-
* Draw a single straight
|
|
827
|
-
*
|
|
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().
|
|
828
877
|
*/
|
|
829
878
|
const drawSegment = (
|
|
830
879
|
ax: number, ay: number, bx: number, by: number,
|
|
831
|
-
opts: {
|
|
880
|
+
opts: { width: number; dashed?: boolean; offset?: number; opacity?: number },
|
|
832
881
|
) => {
|
|
882
|
+
const opacity = opts.opacity ?? 0.85;
|
|
833
883
|
const offset = opts.offset ?? 0;
|
|
834
884
|
const dashed = opts.dashed ?? false;
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
-
}
|
|
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);
|
|
867
900
|
};
|
|
868
901
|
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
});
|
|
889
|
-
}
|
|
890
|
-
};
|
|
902
|
+
// 5. Stem: comp → spine center (frame — single solid)
|
|
903
|
+
drawSegment(compPos.x, compPos.y, spineCx, spineCy, { width: FRAME_STROKE_WIDTH });
|
|
891
904
|
|
|
892
|
-
//
|
|
893
|
-
|
|
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
|
+
}
|
|
894
909
|
|
|
895
|
-
//
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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,
|
|
900
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
|
+
}
|
|
901
924
|
}
|
|
902
925
|
|
|
903
|
-
//
|
|
926
|
+
// 8. Junction marker at spine center (the right-angle corner)
|
|
904
927
|
const junction = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
905
928
|
junction.setAttribute('class', 'rake-junction');
|
|
906
|
-
junction.setAttribute('cx', String(
|
|
907
|
-
junction.setAttribute('cy', String(
|
|
908
|
-
junction.setAttribute('r', '
|
|
929
|
+
junction.setAttribute('cx', String(spineCx));
|
|
930
|
+
junction.setAttribute('cy', String(spineCy));
|
|
931
|
+
junction.setAttribute('r', '2.5');
|
|
909
932
|
junction.setAttribute('fill', color);
|
|
910
|
-
junction.setAttribute('opacity', '0.
|
|
933
|
+
junction.setAttribute('opacity', '0.9');
|
|
911
934
|
junction.setAttribute('stroke', uiStore.isDark ? '#0a1f1c' : '#ffffff');
|
|
912
935
|
junction.setAttribute('stroke-width', '1');
|
|
913
936
|
svg.appendChild(junction);
|
|
914
937
|
|
|
915
|
-
//
|
|
938
|
+
// 9. Diamond marker at comprehensive end (hyperedge origin)
|
|
916
939
|
const diamond = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
917
940
|
const dsize = 5;
|
|
918
941
|
diamond.setAttribute('d',
|
|
@@ -1249,6 +1272,8 @@ watch(() => [props.concept?.id, props.registerId] as const, ([newId, newReg]) =>
|
|
|
1249
1272
|
/* ── Legend — grouped by relation TYPE (not category) ───── */
|
|
1250
1273
|
const legendItems = computed(() => {
|
|
1251
1274
|
graphVersion.value; /* reactive dependency */
|
|
1275
|
+
uiStore.isDark; /* re-evaluate when dark mode toggles (color depends on it) */
|
|
1276
|
+
mutedTypes.value; /* re-evaluate when mute set changes */
|
|
1252
1277
|
const typeMap = new Map<string, { type: string; label: string; color: string; count: number }>();
|
|
1253
1278
|
for (const link of links) {
|
|
1254
1279
|
/* Per-type color (with override) so e.g. 'see' and 'references' differ */
|
|
@@ -1261,6 +1286,18 @@ const legendItems = computed(() => {
|
|
|
1261
1286
|
}
|
|
1262
1287
|
typeMap.get(key)!.count++;
|
|
1263
1288
|
}
|
|
1289
|
+
/* Synthetic entry for PartitiveRelations (hyperedge rake bundles).
|
|
1290
|
+
These don't appear in `links` (they're rendered separately), so we
|
|
1291
|
+
inject a legend entry when any are present. */
|
|
1292
|
+
const partitiveCount = props.partitiveRelations?.length ?? 0;
|
|
1293
|
+
if (partitiveCount > 0) {
|
|
1294
|
+
typeMap.set('__partitive__', {
|
|
1295
|
+
type: '__partitive__',
|
|
1296
|
+
label: 'partitive relation',
|
|
1297
|
+
color: uiStore.isDark ? '#2dd4bf' : '#0d9488',
|
|
1298
|
+
count: partitiveCount,
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1264
1301
|
return Array.from(typeMap.values()).sort((a, b) => b.count - a.count);
|
|
1265
1302
|
});
|
|
1266
1303
|
|
|
@@ -1285,6 +1322,9 @@ function toggleRegister(reg: string) {
|
|
|
1285
1322
|
renderDOM();
|
|
1286
1323
|
const c = canvasRef.value;
|
|
1287
1324
|
if (c) drawEdges(c.clientWidth / 2, c.clientHeight * 0.46);
|
|
1325
|
+
/* Re-heat the simulation so nodes "feel" the freed/hidden edges and
|
|
1326
|
+
drift to a new equilibrium instead of staying frozen. */
|
|
1327
|
+
if (sim) sim.alpha(0.5).restart();
|
|
1288
1328
|
}
|
|
1289
1329
|
|
|
1290
1330
|
async function changeDegree(d: 1 | 2 | 3) {
|
|
@@ -1402,6 +1442,10 @@ function toggleType(type: string) {
|
|
|
1402
1442
|
mutedTypes.value = n;
|
|
1403
1443
|
const c = canvasRef.value;
|
|
1404
1444
|
if (c) drawEdges(c.clientWidth / 2, c.clientHeight * 0.46);
|
|
1445
|
+
/* Re-heat the simulation so nodes drift to a new equilibrium with
|
|
1446
|
+
the freed/hidden edges. The link force reads mutedTypes each tick,
|
|
1447
|
+
so toggling effectively "removes" the edge from the force calc. */
|
|
1448
|
+
if (sim) sim.alpha(0.5).restart();
|
|
1405
1449
|
}
|
|
1406
1450
|
|
|
1407
1451
|
function isTypeMuted(type: string): boolean {
|
|
@@ -1574,9 +1618,12 @@ defineExpose({ navigate });
|
|
|
1574
1618
|
<div class="sp-section-label">{{ t('sphere.relationTypes') }}</div>
|
|
1575
1619
|
<div class="sp-legend-grid">
|
|
1576
1620
|
<button v-for="item in legendItems" :key="item.type" class="sp-legend-item"
|
|
1577
|
-
:class="{ muted: isTypeMuted(item.type)
|
|
1621
|
+
:class="{ muted: isTypeMuted(item.type), 'is-partitive': item.type === '__partitive__' }"
|
|
1622
|
+
@click="toggleType(item.type)"
|
|
1578
1623
|
:title="item.type">
|
|
1579
|
-
<span
|
|
1624
|
+
<span v-if="item.type === '__partitive__'" class="sp-swatch sp-swatch-rake"
|
|
1625
|
+
:style="{ '--rake-color': isTypeMuted(item.type) ? '#b8b9cc' : item.color }"></span>
|
|
1626
|
+
<span v-else class="sp-swatch" :style="{ background: isTypeMuted(item.type) ? '#b8b9cc' : item.color }"></span>
|
|
1580
1627
|
<span>{{ item.label }}</span>
|
|
1581
1628
|
<span class="sp-count">{{ item.count }}</span>
|
|
1582
1629
|
</button>
|
|
@@ -1825,6 +1872,26 @@ defineExpose({ navigate });
|
|
|
1825
1872
|
.sp-legend-item:hover { background: var(--rule); }
|
|
1826
1873
|
.sp-legend-item.muted { opacity: 0.35; }
|
|
1827
1874
|
.sp-swatch { width: 18px; height: 2px; flex-shrink: 0; border-radius: 1px; }
|
|
1875
|
+
/* Rake swatch — small bracket icon signaling a PartitiveRelation hyperedge
|
|
1876
|
+
(not a single binary edge). Two parallel solid lines (close-set double)
|
|
1877
|
+
with a perpendicular stem, evoking the ISO 704 rake convention. */
|
|
1878
|
+
.sp-swatch-rake {
|
|
1879
|
+
width: 18px; height: 10px; position: relative; background: transparent !important;
|
|
1880
|
+
border-radius: 0;
|
|
1881
|
+
}
|
|
1882
|
+
.sp-swatch-rake::before,
|
|
1883
|
+
.sp-swatch-rake::after {
|
|
1884
|
+
content: ''; position: absolute; background: var(--rake-color, #0d9488);
|
|
1885
|
+
}
|
|
1886
|
+
.sp-swatch-rake::before {
|
|
1887
|
+
/* horizontal spine (close-set double line) */
|
|
1888
|
+
left: 4px; right: 0; top: 3px; height: 1.5px;
|
|
1889
|
+
box-shadow: 0 3px 0 var(--rake-color, #0d9488);
|
|
1890
|
+
}
|
|
1891
|
+
.sp-swatch-rake::after {
|
|
1892
|
+
/* vertical stem from top of swatch to spine */
|
|
1893
|
+
left: 4px; width: 1.5px; top: 0; height: 4px;
|
|
1894
|
+
}
|
|
1828
1895
|
.sp-count { margin-left: auto; font-family: 'JetBrains Mono', monospace; font-size: 10px; }
|
|
1829
1896
|
|
|
1830
1897
|
.sp-preview {
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { computed, type ComputedRef } from 'vue';
|
|
2
2
|
import type { Router } from 'vue-router';
|
|
3
|
+
// glossarist 0.4.24's top-level d.ts is incomplete for v3 model fields
|
|
4
|
+
// (multiplicity + is_delimiting). Import the v3-augmented types from
|
|
5
|
+
// glossarist/models (extended by src/adapters/non-verbal/glossarist-augment.d.ts).
|
|
6
|
+
import type { Concept, RelatedConcept, ConceptRef } from 'glossarist';
|
|
3
7
|
import type {
|
|
4
|
-
Concept,
|
|
5
|
-
RelatedConcept,
|
|
6
8
|
PartitiveRelation as GlsPartitiveRelation,
|
|
7
9
|
PartitiveMember as GlsPartitiveMember,
|
|
8
|
-
|
|
9
|
-
ConceptRef,
|
|
10
|
-
} from 'glossarist';
|
|
10
|
+
} from 'glossarist/models';
|
|
11
11
|
import type {
|
|
12
12
|
Manifest,
|
|
13
13
|
GraphEdge,
|
|
14
14
|
PartitiveRelationWire,
|
|
15
15
|
PartitiveMemberWire,
|
|
16
|
-
TypeSharedPluralityWire,
|
|
17
16
|
} from '../adapters/types';
|
|
18
17
|
import { getFactory } from '../adapters/factory';
|
|
19
18
|
import { conceptUri } from '../adapters/model-bridge';
|
|
@@ -24,6 +23,7 @@ import { categorizeRelationship, relationshipLabel, INVERSE_RELATIONSHIPS } from
|
|
|
24
23
|
import { langLabel } from '../utils/lang';
|
|
25
24
|
import { escapeAttr } from '../utils/escape';
|
|
26
25
|
import { useI18n } from '../i18n';
|
|
26
|
+
import type { PartitiveMultiplicity } from '../utils/partitive-multiplicity';
|
|
27
27
|
|
|
28
28
|
export interface EdgeDisplay {
|
|
29
29
|
uri: string;
|
|
@@ -190,7 +190,11 @@ export function useConceptEdges(
|
|
|
190
190
|
.map((m: GlsPartitiveMember): PartitiveMemberWire | null => {
|
|
191
191
|
const uri = resolveConceptRefUri(m.ref);
|
|
192
192
|
if (!uri || uri === source) return null;
|
|
193
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
uri,
|
|
195
|
+
multiplicity: readMultiplicity(m),
|
|
196
|
+
isDelimiting: readIsDelimiting(m),
|
|
197
|
+
};
|
|
194
198
|
})
|
|
195
199
|
.filter((m: PartitiveMemberWire | null): m is PartitiveMemberWire => m !== null);
|
|
196
200
|
if (partitives.length === 0) return null;
|
|
@@ -200,7 +204,6 @@ export function useConceptEdges(
|
|
|
200
204
|
comprehensive,
|
|
201
205
|
partitives,
|
|
202
206
|
completeness: rel.completeness,
|
|
203
|
-
plurality: projectPlurality(rel.plurality),
|
|
204
207
|
criterion: rel.criterion ?? undefined,
|
|
205
208
|
register: registerId.value,
|
|
206
209
|
};
|
|
@@ -208,24 +211,21 @@ export function useConceptEdges(
|
|
|
208
211
|
.filter((r: PartitiveRelationWire | null): r is PartitiveRelationWire => r !== null);
|
|
209
212
|
});
|
|
210
213
|
|
|
211
|
-
function projectPlurality(p: GlsTypeSharedPlurality | null): TypeSharedPluralityWire | null {
|
|
212
|
-
if (!p) return null;
|
|
213
|
-
const sharedTypeRef: ConceptRef | null = p.sharedType;
|
|
214
|
-
const sharedType = sharedTypeRef
|
|
215
|
-
? [sharedTypeRef.source, sharedTypeRef.id].filter(Boolean).join(':') || null
|
|
216
|
-
: null;
|
|
217
|
-
return {
|
|
218
|
-
isShared: p.isShared,
|
|
219
|
-
isUncertain: p.isUncertain,
|
|
220
|
-
sharedType,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
214
|
function resolveConceptRefUri(ref: ConceptRef | null): string | null {
|
|
225
215
|
if (!ref) return null;
|
|
226
216
|
return resolveRefUri({ source: ref.source ?? null, id: ref.id ?? null });
|
|
227
217
|
}
|
|
228
218
|
|
|
219
|
+
// glossarist 0.4.24 carries multiplicity + is_delimiting natively on
|
|
220
|
+
// PartitiveMember. Read them directly — no migration adapter needed.
|
|
221
|
+
function readMultiplicity(m: GlsPartitiveMember): PartitiveMultiplicity {
|
|
222
|
+
return m.multiplicity;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function readIsDelimiting(m: GlsPartitiveMember): boolean {
|
|
226
|
+
return m.is_delimiting;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
229
|
function resolveRefUri(ref: { source: string | null; id: string | null } | null): string | null {
|
|
230
230
|
const target = resolveRelatedRef(ref);
|
|
231
231
|
if (!target) return null;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISO 704:2022 §5.5.4.2 — partitive multiplicity + delimiting model.
|
|
3
|
+
*
|
|
4
|
+
* Multiplicity (5 values) encodes part-count semantics:
|
|
5
|
+
* - compulsory : exactly one, required
|
|
6
|
+
* - optional : zero or one
|
|
7
|
+
* - compulsory_multiple : two or more, required
|
|
8
|
+
* - optional_multiple : zero or more
|
|
9
|
+
* - compulsory_at_least_one : one or more (required, count unknown)
|
|
10
|
+
*
|
|
11
|
+
* Delimiting (boolean) is orthogonal — a delimiting part behaves like a
|
|
12
|
+
* delimiting characteristic in generic relations: it distinguishes the
|
|
13
|
+
* comprehensive concept from coordinate concepts. Visual encoding:
|
|
14
|
+
* 3× stroke width (4.5 px vs 1.5 px).
|
|
15
|
+
*
|
|
16
|
+
* Adding a new multiplicity value = add one entry to PARTITIVE_MULTIPLICITY.
|
|
17
|
+
* Renderers consume RakeStrokeStyle via rakeStrokeStyle(); never inspect
|
|
18
|
+
* the raw multiplicity string. OCP + MECE + DRY.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const PARTITIVE_MULTIPLICITY = {
|
|
22
|
+
compulsory: { lines: 1, pattern: 'solid', label: 'Compulsory' },
|
|
23
|
+
optional: { lines: 1, pattern: 'dashed', label: 'Optional' },
|
|
24
|
+
compulsory_multiple: { lines: 2, pattern: 'solid', label: 'Compulsory multiple' },
|
|
25
|
+
optional_multiple: { lines: 2, pattern: 'dashed', label: 'Optional multiple' },
|
|
26
|
+
compulsory_at_least_one: { lines: 2, pattern: 'mixed', label: 'At least one' },
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export type PartitiveMultiplicity = keyof typeof PARTITIVE_MULTIPLICITY;
|
|
30
|
+
|
|
31
|
+
export const PARTITIVE_MULTIPLICITY_VALUES: readonly PartitiveMultiplicity[] =
|
|
32
|
+
Object.keys(PARTITIVE_MULTIPLICITY) as PartitiveMultiplicity[];
|
|
33
|
+
|
|
34
|
+
export function isPartitiveMultiplicity(value: unknown): value is PartitiveMultiplicity {
|
|
35
|
+
return typeof value === 'string' && value in PARTITIVE_MULTIPLICITY;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type LinePattern = 'solid' | 'dashed' | 'mixed';
|
|
39
|
+
|
|
40
|
+
export interface MultiplicityDefinition {
|
|
41
|
+
/** Number of parallel lines used to render the multiplicity. */
|
|
42
|
+
readonly lines: 1 | 2;
|
|
43
|
+
/** Dash pattern: solid, dashed, or mixed (one solid + one dashed). */
|
|
44
|
+
readonly pattern: LinePattern;
|
|
45
|
+
/** Human-readable label for legends and badges. */
|
|
46
|
+
readonly label: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function multiplicityDefinition(m: PartitiveMultiplicity): MultiplicityDefinition {
|
|
50
|
+
return PARTITIVE_MULTIPLICITY[m];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── RakeStrokeStyle — the render-ready form consumed by all renderers ────
|
|
54
|
+
|
|
55
|
+
export interface RakeStrokeStyle {
|
|
56
|
+
/** 1 or 2 parallel lines. */
|
|
57
|
+
readonly lineCount: 1 | 2;
|
|
58
|
+
/** Primary (always-present) line dash. */
|
|
59
|
+
readonly primaryDashed: boolean;
|
|
60
|
+
/** Secondary line dash (only meaningful when lineCount === 2). */
|
|
61
|
+
readonly secondaryDashed: boolean;
|
|
62
|
+
/** Stroke width in px. 1.5 normal, 4.5 (3×) when is_delimiting. */
|
|
63
|
+
readonly strokeWidth: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const NORMAL_STROKE_WIDTH = 1.5;
|
|
67
|
+
export const DELIMITING_STROKE_WIDTH = 4.5; // 3× normal per ISO 704
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Compute the render-ready stroke style for a partitive member.
|
|
71
|
+
*
|
|
72
|
+
* Pure function — same inputs always yield the same RakeStrokeStyle.
|
|
73
|
+
* Consumed by both the sidebar rake diagram (PartitiveRelationDiagram)
|
|
74
|
+
* and the sphere rake bundles (RelationSphere.drawRakeBundles).
|
|
75
|
+
*/
|
|
76
|
+
export function rakeStrokeStyle(
|
|
77
|
+
multiplicity: PartitiveMultiplicity,
|
|
78
|
+
isDelimiting: boolean,
|
|
79
|
+
): RakeStrokeStyle {
|
|
80
|
+
const def = PARTITIVE_MULTIPLICITY[multiplicity];
|
|
81
|
+
return {
|
|
82
|
+
lineCount: def.lines,
|
|
83
|
+
primaryDashed: def.pattern === 'dashed',
|
|
84
|
+
secondaryDashed: def.pattern === 'dashed' || def.pattern === 'mixed',
|
|
85
|
+
strokeWidth: isDelimiting ? DELIMITING_STROKE_WIDTH : NORMAL_STROKE_WIDTH,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Migration from glossarist 0.4.20 certainty → ISO 704 multiplicity ────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* glossarist 0.4.20 carries per-member `certainty: 'confirmed' | 'possible'`
|
|
93
|
+
* (a v0.4 abstraction that conflated "this member exists" with "we're sure
|
|
94
|
+
* about it"). ISO 704:2022 replaces that with the multiplicity enum.
|
|
95
|
+
*
|
|
96
|
+
* Until glossarist publishes native multiplicity support, this helper maps
|
|
97
|
+
* the legacy field to the closest ISO 704 multiplicity:
|
|
98
|
+
* - confirmed → compulsory
|
|
99
|
+
* - possible → optional
|
|
100
|
+
*
|
|
101
|
+
* Once glossarist publishes multiplicity natively, the bridge can read
|
|
102
|
+
* it directly and this helper can be deleted.
|
|
103
|
+
*/
|
|
104
|
+
export function multiplicityFromCertainty(certainty: 'confirmed' | 'possible' | null | undefined): PartitiveMultiplicity {
|
|
105
|
+
return certainty === 'possible' ? 'optional' : 'compulsory';
|
|
106
|
+
}
|