@glossarist/concept-browser 0.7.93 → 0.7.95
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/scripts/generate-data.mjs +49 -0
- package/src/__tests__/__fixtures__/concepts.ts +61 -0
- package/src/__tests__/composables/use-concept-edges-hyperedge.test.ts +143 -0
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +2 -2
- package/src/__tests__/sphere-math.test.ts +108 -1
- package/src/adapters/non-verbal/glossarist-augment.d.ts +0 -2
- package/src/components/ConceptRdfView.vue +6 -4
- package/src/components/RelationSphere.vue +57 -9
- package/src/composables/use-concept-edges.ts +26 -16
- package/src/utils/sphere-math.ts +54 -0
- package/src/views/ConceptView.vue +10 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.95",
|
|
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": {
|
|
@@ -91,6 +91,7 @@ function loadConceptFile(filePath) {
|
|
|
91
91
|
// Managed concept-level fields
|
|
92
92
|
if (mc.related) result._related = mc.related;
|
|
93
93
|
if (mc.partitive_relations) result._partitiveRelations = mc.partitive_relations;
|
|
94
|
+
if (mc.generic_relations) result._genericRelations = mc.generic_relations;
|
|
94
95
|
if (mc.data.domains) result._domains = mc.data.domains;
|
|
95
96
|
if (mc.dates) result._dates = mc.dates;
|
|
96
97
|
if (mc.sources) result._sources = mc.sources;
|
|
@@ -769,6 +770,54 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
|
|
|
769
770
|
});
|
|
770
771
|
}
|
|
771
772
|
|
|
773
|
+
if (conceptYaml._genericRelations?.length > 0) {
|
|
774
|
+
doc['gl:genericRelations'] = conceptYaml._genericRelations.map(rel => {
|
|
775
|
+
const out = { '@type': 'gl:GenericRelation' };
|
|
776
|
+
if (rel.comprehensive) {
|
|
777
|
+
out['gl:comprehensive'] = refToJsonLd(rel.comprehensive, 'gl:ConceptRef');
|
|
778
|
+
}
|
|
779
|
+
if (Array.isArray(rel.members) && rel.members.length > 0) {
|
|
780
|
+
out['gl:hasGeneric'] = rel.members.map(member => {
|
|
781
|
+
const m = { '@type': 'gl:GenericMember' };
|
|
782
|
+
if (member.ref) {
|
|
783
|
+
m['gl:ref'] = refToJsonLd(member.ref, 'gl:ConceptRef');
|
|
784
|
+
} else {
|
|
785
|
+
m['gl:ref'] = refToJsonLd(member, 'gl:ConceptRef');
|
|
786
|
+
}
|
|
787
|
+
const presence = member.presence
|
|
788
|
+
?? splitMultiplicity(member.multiplicity ?? '').presence;
|
|
789
|
+
const count = member.count
|
|
790
|
+
?? splitMultiplicity(member.multiplicity ?? '').count;
|
|
791
|
+
m['gl:presence'] = presence;
|
|
792
|
+
m['gl:count'] = count;
|
|
793
|
+
/* ISO 704:2022 §5.5.4.2.1 — every generic member carries a
|
|
794
|
+
delimiting characteristic that distinguishes it from its
|
|
795
|
+
coordinate concepts. Required field. */
|
|
796
|
+
if (member.delimiting_characteristic || member.delimitingCharacteristic) {
|
|
797
|
+
const dc = member.delimiting_characteristic ?? member.delimitingCharacteristic;
|
|
798
|
+
m['gl:delimitingCharacteristic'] = typeof dc === 'string'
|
|
799
|
+
? { default: dc }
|
|
800
|
+
: dc;
|
|
801
|
+
}
|
|
802
|
+
if (member.multiplicity) m['gl:multiplicity'] = member.multiplicity;
|
|
803
|
+
const legacyCertainty = member.certainty
|
|
804
|
+
?? LEGACY_CERTAINTY_FROM_AXIS[presence];
|
|
805
|
+
if (legacyCertainty) m['gl:certainty'] = legacyCertainty;
|
|
806
|
+
return m;
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
if (rel.completeness) {
|
|
810
|
+
out['gl:completeness'] = rel.completeness;
|
|
811
|
+
}
|
|
812
|
+
if (rel.criterion) {
|
|
813
|
+
out['gl:criterion'] = typeof rel.criterion === 'string'
|
|
814
|
+
? { default: rel.criterion }
|
|
815
|
+
: rel.criterion;
|
|
816
|
+
}
|
|
817
|
+
return out;
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
772
821
|
return doc;
|
|
773
822
|
}
|
|
774
823
|
|
|
@@ -204,6 +204,65 @@ function withdrawnConcept(): Concept {
|
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
function withPartitiveHyperedge(): Concept {
|
|
208
|
+
const source = 'urn:glossarist:fixtures';
|
|
209
|
+
return Concept.fromJSON({
|
|
210
|
+
id: '1.3',
|
|
211
|
+
uri: `${BASE}/with-partitive-hyperedge`,
|
|
212
|
+
status: 'valid',
|
|
213
|
+
localizations: {
|
|
214
|
+
eng: {
|
|
215
|
+
language_code: 'eng',
|
|
216
|
+
entry_status: 'valid',
|
|
217
|
+
terms: [{ type: 'expression', designation: 'system of quantities', normative_status: 'preferred' }],
|
|
218
|
+
definition: [{ content: 'A concept that has a partitive decomposition.' }],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
relations: [
|
|
222
|
+
{
|
|
223
|
+
type: 'partitive_relation',
|
|
224
|
+
comprehensive: { source, id: '1.3' },
|
|
225
|
+
members: [
|
|
226
|
+
{ ref: { source, id: '1.22' }, presence: 'required', count: 'at_least_one' },
|
|
227
|
+
{ ref: { source, id: '1.4' }, presence: 'required', count: 'multiple' },
|
|
228
|
+
{ ref: { source, id: '1.5' }, presence: 'required', count: 'at_least_one' },
|
|
229
|
+
],
|
|
230
|
+
completeness: 'complete',
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function withGenericHyperedge(): Concept {
|
|
237
|
+
const source = 'urn:glossarist:fixtures';
|
|
238
|
+
return Concept.fromJSON({
|
|
239
|
+
id: '1.9',
|
|
240
|
+
uri: `${BASE}/with-generic-hyperedge`,
|
|
241
|
+
status: 'valid',
|
|
242
|
+
localizations: {
|
|
243
|
+
eng: {
|
|
244
|
+
language_code: 'eng',
|
|
245
|
+
entry_status: 'valid',
|
|
246
|
+
terms: [{ type: 'expression', designation: 'measurement unit', normative_status: 'preferred' }],
|
|
247
|
+
definition: [{ content: 'A concept that has a generic (genus/species) decomposition.' }],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
relations: [
|
|
251
|
+
{
|
|
252
|
+
type: 'generic_relation',
|
|
253
|
+
comprehensive: { source, id: '1.9' },
|
|
254
|
+
criterion: { eng: 'by magnitude relationship to a reference unit' },
|
|
255
|
+
members: [
|
|
256
|
+
{ ref: { source, id: '1.17' }, presence: 'required', count: 'at_least_one', delimitingCharacteristic: { eng: 'multiple of a unit' } },
|
|
257
|
+
{ ref: { source, id: '1.18' }, presence: 'required', count: 'at_least_one', delimitingCharacteristic: { eng: 'submultiple of a unit' } },
|
|
258
|
+
{ ref: { source, id: '1.12' }, presence: 'required', count: 'at_least_one', delimitingCharacteristic: { eng: 'coherent derived unit' } },
|
|
259
|
+
],
|
|
260
|
+
completeness: 'partial',
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
207
266
|
export const CONCEPT_FIXTURES: readonly ConceptFixture[] = [
|
|
208
267
|
{ name: 'minimal', description: 'one localization, one designation', uri: `${BASE}/minimal`, concept: minimal() },
|
|
209
268
|
{ name: 'multilingual', description: 'three languages across Latn and CJK scripts', uri: `${BASE}/multilingual`, concept: multilingual() },
|
|
@@ -212,6 +271,8 @@ export const CONCEPT_FIXTURES: readonly ConceptFixture[] = [
|
|
|
212
271
|
{ name: 'with-non-verbal', description: 'figure, formula, and table non-verbal reps', uri: `${BASE}/with-non-verbal`, concept: withNonVerbal() },
|
|
213
272
|
{ name: 'with-dates', description: 'accepted/amended/retired lifecycle', uri: `${BASE}/with-dates`, concept: withDates() },
|
|
214
273
|
{ name: 'withdrawn', description: 'withdrawn status with retirement date (J3)', uri: `${BASE}/withdrawn`, concept: withdrawnConcept() },
|
|
274
|
+
{ name: 'with-partitive-hyperedge', description: 'partitive decomposition in concept.relations', uri: `${BASE}/with-partitive-hyperedge`, concept: withPartitiveHyperedge() },
|
|
275
|
+
{ name: 'with-generic-hyperedge', description: 'generic (genus/species) decomposition in concept.relations', uri: `${BASE}/with-generic-hyperedge`, concept: withGenericHyperedge() },
|
|
215
276
|
];
|
|
216
277
|
|
|
217
278
|
export function fixtureByName(name: string): ConceptFixture {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
|
+
import type { Router } from 'vue-router';
|
|
4
|
+
import { useConceptEdges } from '../../composables/use-concept-edges';
|
|
5
|
+
import { useVocabularyStore } from '../../stores/vocabulary';
|
|
6
|
+
import type { Manifest, GraphEdge } from '../../adapters/types';
|
|
7
|
+
import { makeManifest, setupPinia } from '../test-helpers';
|
|
8
|
+
import { fixtureByName } from '../__fixtures__/concepts';
|
|
9
|
+
import { getFactory, resetFactory } from '../../adapters/factory';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Regression test for the sphere-view hyperedge projection.
|
|
13
|
+
*
|
|
14
|
+
* Bug (2026-07-30): ConceptView.vue read `concept.partitiveRelations`
|
|
15
|
+
* — a field glossarist 0.4.34 REMOVED. The field was silently
|
|
16
|
+
* `undefined`, the sphere rendered no rake, and no test caught it.
|
|
17
|
+
*
|
|
18
|
+
* Fix: ConceptView consumes `useConceptEdges.conceptPartitiveRelations`,
|
|
19
|
+
* which reads the unified `concept.relations` array and filters by
|
|
20
|
+
* `instanceof PartitiveHyperedge`. This spec pins that contract —
|
|
21
|
+
* if glossarist ever renames or removes `.relations`, this fails.
|
|
22
|
+
*/
|
|
23
|
+
describe('useConceptEdges — hyperedge projection (regression)', () => {
|
|
24
|
+
const URN_SOURCE = 'urn:glossarist:fixtures';
|
|
25
|
+
const manifest: Manifest = makeManifest({
|
|
26
|
+
id: 'fixtures',
|
|
27
|
+
datasetUri: URN_SOURCE,
|
|
28
|
+
uriBase: 'https://glossarist.org',
|
|
29
|
+
});
|
|
30
|
+
const noEdges: GraphEdge[] = [];
|
|
31
|
+
const noopRouter = { push() {} } as unknown as Router;
|
|
32
|
+
|
|
33
|
+
let store: ReturnType<typeof useVocabularyStore>;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
setupPinia();
|
|
37
|
+
resetFactory();
|
|
38
|
+
store = useVocabularyStore();
|
|
39
|
+
store.manifests.set(manifest.id, manifest);
|
|
40
|
+
|
|
41
|
+
const factory = getFactory();
|
|
42
|
+
factory.resolveRelatedRef = vi.fn((ref: { source: string | null; id: string | null } | null) =>
|
|
43
|
+
ref?.source === URN_SOURCE && ref.id
|
|
44
|
+
? { registerId: manifest.id, conceptId: ref.id }
|
|
45
|
+
: null,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('projects a PartitiveHyperedge from concept.relations', () => {
|
|
50
|
+
const { concept } = fixtureByName('with-partitive-hyperedge');
|
|
51
|
+
const { conceptPartitiveRelations } = useConceptEdges(
|
|
52
|
+
computed(() => concept),
|
|
53
|
+
computed(() => 'fixtures'),
|
|
54
|
+
computed(() => manifest),
|
|
55
|
+
computed(() => noEdges),
|
|
56
|
+
noopRouter,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const rels = conceptPartitiveRelations.value;
|
|
60
|
+
expect(rels).toHaveLength(1);
|
|
61
|
+
|
|
62
|
+
const [rel] = rels;
|
|
63
|
+
expect(rel.comprehensive).toBe('https://glossarist.org/fixtures/concept/1.3');
|
|
64
|
+
expect(rel.partitives).toHaveLength(3);
|
|
65
|
+
expect(rel.partitives.map(m => m.uri)).toEqual([
|
|
66
|
+
'https://glossarist.org/fixtures/concept/1.22',
|
|
67
|
+
'https://glossarist.org/fixtures/concept/1.4',
|
|
68
|
+
'https://glossarist.org/fixtures/concept/1.5',
|
|
69
|
+
]);
|
|
70
|
+
expect(rel.partitives[1].presence).toBe('required');
|
|
71
|
+
expect(rel.partitives[1].count).toBe('multiple');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns [] when concept is null (loading state)', () => {
|
|
75
|
+
const { conceptPartitiveRelations, conceptGenericRelations } = useConceptEdges(
|
|
76
|
+
computed(() => null),
|
|
77
|
+
computed(() => 'fixtures'),
|
|
78
|
+
computed(() => manifest),
|
|
79
|
+
computed(() => noEdges),
|
|
80
|
+
noopRouter,
|
|
81
|
+
);
|
|
82
|
+
expect(conceptPartitiveRelations.value).toEqual([]);
|
|
83
|
+
expect(conceptGenericRelations.value).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns [] when manifest is null', () => {
|
|
87
|
+
const { concept } = fixtureByName('with-partitive-hyperedge');
|
|
88
|
+
const { conceptPartitiveRelations } = useConceptEdges(
|
|
89
|
+
computed(() => concept),
|
|
90
|
+
computed(() => 'fixtures'),
|
|
91
|
+
computed(() => null),
|
|
92
|
+
computed(() => noEdges),
|
|
93
|
+
noopRouter,
|
|
94
|
+
);
|
|
95
|
+
expect(conceptPartitiveRelations.value).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('would have caught the .partitiveRelations regression', () => {
|
|
99
|
+
// Simulate the pre-fix behavior: the old code read concept.partitiveRelations
|
|
100
|
+
// (undefined in glossarist 0.4.34+) and produced an empty array.
|
|
101
|
+
// This spec proves the projection now reads concept.relations instead.
|
|
102
|
+
const { concept } = fixtureByName('with-partitive-hyperedge');
|
|
103
|
+
expect((concept as any).partitiveRelations).toBeUndefined();
|
|
104
|
+
expect((concept as any).relations).toHaveLength(1);
|
|
105
|
+
|
|
106
|
+
const { conceptPartitiveRelations } = useConceptEdges(
|
|
107
|
+
computed(() => concept),
|
|
108
|
+
computed(() => 'fixtures'),
|
|
109
|
+
computed(() => manifest),
|
|
110
|
+
computed(() => noEdges),
|
|
111
|
+
noopRouter,
|
|
112
|
+
);
|
|
113
|
+
expect(conceptPartitiveRelations.value.length).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('projects a GenericHyperedge from concept.relations', () => {
|
|
117
|
+
const { concept } = fixtureByName('with-generic-hyperedge');
|
|
118
|
+
const { conceptGenericRelations } = useConceptEdges(
|
|
119
|
+
computed(() => concept),
|
|
120
|
+
computed(() => 'fixtures'),
|
|
121
|
+
computed(() => manifest),
|
|
122
|
+
computed(() => noEdges),
|
|
123
|
+
noopRouter,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const rels = conceptGenericRelations.value;
|
|
127
|
+
expect(rels).toHaveLength(1);
|
|
128
|
+
|
|
129
|
+
const [rel] = rels;
|
|
130
|
+
expect(rel.comprehensive).toBe('https://glossarist.org/fixtures/concept/1.9');
|
|
131
|
+
expect(rel.members).toHaveLength(3);
|
|
132
|
+
expect(rel.members.map(m => m.uri)).toEqual([
|
|
133
|
+
'https://glossarist.org/fixtures/concept/1.17',
|
|
134
|
+
'https://glossarist.org/fixtures/concept/1.18',
|
|
135
|
+
'https://glossarist.org/fixtures/concept/1.12',
|
|
136
|
+
]);
|
|
137
|
+
/* ISO 704:2022 §5.5.4.2.1 — every generic member carries a
|
|
138
|
+
delimiting characteristic. Pin the field is projected. */
|
|
139
|
+
expect(rel.members[0].delimitingCharacteristic).toEqual({ eng: 'multiple of a unit' });
|
|
140
|
+
expect(rel.members[1].delimitingCharacteristic).toEqual({ eng: 'submultiple of a unit' });
|
|
141
|
+
expect(rel.criterion).toEqual({ eng: 'by magnitude relationship to a reference unit' });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -39,7 +39,7 @@ function makePartitive(
|
|
|
39
39
|
if (fields.presence) data.presence = fields.presence;
|
|
40
40
|
if (fields.count) data.count = fields.count;
|
|
41
41
|
if (fields.isDelimiting) data.is_delimiting = fields.isDelimiting;
|
|
42
|
-
return new PartitiveMemberModel(data
|
|
42
|
+
return new (PartitiveMemberModel as unknown as new (data: unknown) => PartitiveMember)(data) as PartitiveMember;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function makeRelation(opts: {
|
|
@@ -48,7 +48,7 @@ function makeRelation(opts: {
|
|
|
48
48
|
completeness?: 'complete' | 'partial';
|
|
49
49
|
criterion?: Record<string, string>;
|
|
50
50
|
}): PartitiveHyperedge {
|
|
51
|
-
return new PartitiveHyperedgeModel
|
|
51
|
+
return new (PartitiveHyperedgeModel as unknown as new (data: unknown) => PartitiveHyperedge)(opts) as PartitiveHyperedge;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { hashSeed, expandParams, portSide, portPoint, idToUriGet } from '../utils/sphere-math';
|
|
2
|
+
import { hashSeed, expandParams, portSide, portPoint, idToUriGet, visibleNodeIds } from '../utils/sphere-math';
|
|
3
3
|
|
|
4
4
|
describe('sphere-math', () => {
|
|
5
5
|
describe('hashSeed', () => {
|
|
@@ -53,4 +53,111 @@ describe('sphere-math', () => {
|
|
|
53
53
|
expect(idToUriGet(map, '/uri/unknown')).toBeUndefined();
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
describe('visibleNodeIds', () => {
|
|
58
|
+
const focus = { id: 'focus', register: 'main', depth: 0 };
|
|
59
|
+
const a = { id: 'a', register: 'main', depth: 1 };
|
|
60
|
+
const b = { id: 'b', register: 'main', depth: 1 };
|
|
61
|
+
const c = { id: 'c', register: 'main', depth: 1 };
|
|
62
|
+
const d = { id: 'd', register: 'other', depth: 1 };
|
|
63
|
+
|
|
64
|
+
const buildLinks = (links: Array<[string, string, string]>) =>
|
|
65
|
+
links.map(([source, target, type]) => ({ source, target, type }));
|
|
66
|
+
|
|
67
|
+
it('always includes the focus node (depth 0)', () => {
|
|
68
|
+
const nodes = [focus, a];
|
|
69
|
+
const links = buildLinks([['focus', 'a', 'related']]);
|
|
70
|
+
const visible = visibleNodeIds(nodes, links, new Set(), new Set());
|
|
71
|
+
expect(visible.has('focus')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('includes a node that has at least one non-muted edge', () => {
|
|
75
|
+
const nodes = [focus, a, b];
|
|
76
|
+
const links = buildLinks([
|
|
77
|
+
['focus', 'a', 'related'],
|
|
78
|
+
['focus', 'b', 'see'],
|
|
79
|
+
]);
|
|
80
|
+
const visible = visibleNodeIds(nodes, links, new Set(), new Set());
|
|
81
|
+
expect(visible.has('a')).toBe(true);
|
|
82
|
+
expect(visible.has('b')).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('hides a node whose only edge type is muted', () => {
|
|
86
|
+
const nodes = [focus, a, b];
|
|
87
|
+
const links = buildLinks([
|
|
88
|
+
['focus', 'a', 'related'],
|
|
89
|
+
['focus', 'b', 'see'],
|
|
90
|
+
]);
|
|
91
|
+
const visible = visibleNodeIds(nodes, links, new Set(['see']), new Set());
|
|
92
|
+
expect(visible.has('a')).toBe(true);
|
|
93
|
+
expect(visible.has('b')).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('vice versa: hidden node reappears when type is unmuted', () => {
|
|
97
|
+
const nodes = [focus, a, b];
|
|
98
|
+
const links = buildLinks([
|
|
99
|
+
['focus', 'a', 'related'],
|
|
100
|
+
['focus', 'b', 'see'],
|
|
101
|
+
]);
|
|
102
|
+
const muted = new Set(['see']);
|
|
103
|
+
expect(visibleNodeIds(nodes, links, muted, new Set()).has('b')).toBe(false);
|
|
104
|
+
muted.delete('see');
|
|
105
|
+
expect(visibleNodeIds(nodes, links, muted, new Set()).has('b')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('keeps a node visible if it has at least one unmuted edge among several', () => {
|
|
109
|
+
const nodes = [focus, a, b, c];
|
|
110
|
+
const links = buildLinks([
|
|
111
|
+
['focus', 'a', 'related'],
|
|
112
|
+
['focus', 'a', 'see'],
|
|
113
|
+
['focus', 'b', 'see'],
|
|
114
|
+
['focus', 'c', 'related'],
|
|
115
|
+
]);
|
|
116
|
+
const visible = visibleNodeIds(nodes, links, new Set(['see']), new Set());
|
|
117
|
+
expect(visible.has('a')).toBe(true);
|
|
118
|
+
expect(visible.has('b')).toBe(false);
|
|
119
|
+
expect(visible.has('c')).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('hides a node in a muted register (unless it is the focus)', () => {
|
|
123
|
+
const nodes = [focus, a, d];
|
|
124
|
+
const links = buildLinks([
|
|
125
|
+
['focus', 'a', 'related'],
|
|
126
|
+
['focus', 'd', 'related'],
|
|
127
|
+
]);
|
|
128
|
+
const visible = visibleNodeIds(nodes, links, new Set(), new Set(['other']));
|
|
129
|
+
expect(visible.has('a')).toBe(true);
|
|
130
|
+
expect(visible.has('d')).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('hides a node whose only edge endpoint is in a muted register', () => {
|
|
134
|
+
const nodes = [focus, a, d];
|
|
135
|
+
const links = buildLinks([
|
|
136
|
+
['a', 'd', 'related'],
|
|
137
|
+
]);
|
|
138
|
+
const visible = visibleNodeIds(nodes, links, new Set(), new Set(['other']));
|
|
139
|
+
expect(visible.has('a')).toBe(false);
|
|
140
|
+
expect(visible.has('d')).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('isolated non-focus node (no edges at all) is hidden', () => {
|
|
144
|
+
const nodes = [focus, a];
|
|
145
|
+
const links: Array<{ source: string; target: string; type: string }> = [];
|
|
146
|
+
const visible = visibleNodeIds(nodes, links, new Set(), new Set());
|
|
147
|
+
expect(visible.has('focus')).toBe(true);
|
|
148
|
+
expect(visible.has('a')).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('all-visible when no mutes are set', () => {
|
|
152
|
+
const nodes = [focus, a, b, c, d];
|
|
153
|
+
const links = buildLinks([
|
|
154
|
+
['focus', 'a', 'related'],
|
|
155
|
+
['focus', 'b', 'see'],
|
|
156
|
+
['focus', 'c', 'related'],
|
|
157
|
+
['focus', 'd', 'references'],
|
|
158
|
+
]);
|
|
159
|
+
const visible = visibleNodeIds(nodes, links, new Set(), new Set());
|
|
160
|
+
expect([...visible].sort()).toEqual(['a', 'b', 'c', 'd', 'focus']);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
56
163
|
});
|
|
@@ -171,8 +171,6 @@ declare module 'glossarist' {
|
|
|
171
171
|
|
|
172
172
|
interface Concept {
|
|
173
173
|
readonly relations: AbstractHyperedge[];
|
|
174
|
-
readonly partitiveRelations: PartitiveHyperedge[];
|
|
175
|
-
readonly genericRelations: GenericHyperedge[];
|
|
176
174
|
}
|
|
177
175
|
|
|
178
176
|
// Re-exposed here so callers importing from the top-level `glossarist`
|
|
@@ -10,12 +10,14 @@ import { useRdfDocument } from './use-rdf-document';
|
|
|
10
10
|
const props = defineProps<{
|
|
11
11
|
concept: Concept;
|
|
12
12
|
registerId: string;
|
|
13
|
-
conceptUriValue: string;
|
|
13
|
+
conceptUriValue: string | null;
|
|
14
14
|
}>();
|
|
15
15
|
|
|
16
|
+
const uri = computed(() => props.conceptUriValue ?? '');
|
|
17
|
+
|
|
16
18
|
const { sections, turtle, jsonld, typeChain } = useRdfDocument(
|
|
17
19
|
() => props.concept,
|
|
18
|
-
() =>
|
|
20
|
+
() => uri.value,
|
|
19
21
|
);
|
|
20
22
|
|
|
21
23
|
const resourceCount = computed(() => sections.value.length);
|
|
@@ -23,11 +25,11 @@ const conceptId = computed(() => props.concept.id);
|
|
|
23
25
|
</script>
|
|
24
26
|
|
|
25
27
|
<template>
|
|
26
|
-
<ErrorBoundary title="RDF view failed" :retry-key="
|
|
28
|
+
<ErrorBoundary title="RDF view failed" :retry-key="uri">
|
|
27
29
|
<div class="space-y-6">
|
|
28
30
|
<div class="card p-5">
|
|
29
31
|
<div class="flex items-start justify-between gap-3">
|
|
30
|
-
<RdfInstanceHeader :uri="
|
|
32
|
+
<RdfInstanceHeader :uri="uri" :concept-id="conceptId" />
|
|
31
33
|
</div>
|
|
32
34
|
<div class="mt-4 pt-3 border-t border-ink-100/60">
|
|
33
35
|
<div class="flex items-center gap-1.5 flex-wrap text-xs text-ink-400">
|
|
@@ -21,11 +21,11 @@ import { easeInOutCubic, slerp, fibonacciSpherePosition, project, cardEdge, type
|
|
|
21
21
|
import { UriRouter } from '../adapters/UriRouter';
|
|
22
22
|
import { getPreferredTerm } from '../utils/concept-helpers';
|
|
23
23
|
import { renderContent } from '../utils/content-renderer';
|
|
24
|
-
import { hashSeed, expandParams, portSide, portPoint, idToUriGet } from '../utils/sphere-math';
|
|
24
|
+
import { hashSeed, expandParams, portSide, portPoint, idToUriGet, visibleNodeIds } from '../utils/sphere-math';
|
|
25
25
|
import { useI18n } from '../i18n';
|
|
26
26
|
|
|
27
27
|
const { t, locale } = useI18n();
|
|
28
|
-
import type { Concept, Manifest, GraphEdge, PartitiveRelationWire, PartitiveMemberWire } from '../adapters/types';
|
|
28
|
+
import type { Concept, Manifest, GraphEdge, PartitiveRelationWire, PartitiveMemberWire, GenericRelationWire } from '../adapters/types';
|
|
29
29
|
import { rakeStrokeStyle } from '../utils/partitive-multiplicity';
|
|
30
30
|
import { drawRakeBundles } from './relation-sphere/rake-bundles';
|
|
31
31
|
import { ensureMarkers, ensureTypeMarker } from './relation-sphere/svg-markers';
|
|
@@ -45,6 +45,7 @@ const props = defineProps<{
|
|
|
45
45
|
registerId: string;
|
|
46
46
|
edges: GraphEdge[];
|
|
47
47
|
partitiveRelations?: PartitiveRelationWire[];
|
|
48
|
+
genericRelations?: GenericRelationWire[];
|
|
48
49
|
}>();
|
|
49
50
|
|
|
50
51
|
const emit = defineEmits<{
|
|
@@ -403,16 +404,22 @@ function buildGraph() {
|
|
|
403
404
|
nodes = Array.from(nodeMap.values());
|
|
404
405
|
links = resultLinks;
|
|
405
406
|
|
|
406
|
-
/* Ensure every PartitiveRelation member is present
|
|
407
|
-
|
|
408
|
-
focus;
|
|
409
|
-
|
|
407
|
+
/* Ensure every PartitiveRelation + GenericRelation member is present
|
|
408
|
+
in the graph so rake bundles can render. The comprehensive is
|
|
409
|
+
normally the focus; members may need to be added as depth-1
|
|
410
|
+
neighbors. */
|
|
411
|
+
const hyperedgeRelations: ReadonlyArray<PartitiveRelationWire | GenericRelationWire> = [
|
|
412
|
+
...(props.partitiveRelations ?? []),
|
|
413
|
+
...(props.genericRelations ?? []),
|
|
414
|
+
];
|
|
415
|
+
for (const rel of hyperedgeRelations) {
|
|
410
416
|
const compParsed = UriRouter.parseUri(rel.comprehensive);
|
|
411
417
|
if (!compParsed) continue;
|
|
412
418
|
const compNodeId = `${compParsed.registerId}/${compParsed.conceptId}`;
|
|
413
419
|
if (!nodeMap.has(compNodeId)) continue;
|
|
414
420
|
|
|
415
|
-
|
|
421
|
+
const members = 'partitives' in rel ? rel.partitives : rel.members;
|
|
422
|
+
for (const member of members) {
|
|
416
423
|
const parsed = UriRouter.parseUri(member.uri);
|
|
417
424
|
if (!parsed) continue;
|
|
418
425
|
const nid = `${parsed.registerId}/${parsed.conceptId}`;
|
|
@@ -691,6 +698,30 @@ function drawEdges(cx: number, cy: number) {
|
|
|
691
698
|
isMuted: mutedTypes.value.has('__partitive__'),
|
|
692
699
|
relations: props.partitiveRelations ?? [],
|
|
693
700
|
});
|
|
701
|
+
|
|
702
|
+
/* Generic relations render as rake bundles in a distinct hue (amber).
|
|
703
|
+
Member shape differs (delimitingCharacteristic instead of isDelimiting),
|
|
704
|
+
so normalize to the partitive-shaped input the renderer expects. */
|
|
705
|
+
const genericNormalized: PartitiveRelationWire[] = (props.genericRelations ?? []).map(rel => ({
|
|
706
|
+
source: rel.source,
|
|
707
|
+
comprehensive: rel.comprehensive,
|
|
708
|
+
completeness: rel.completeness,
|
|
709
|
+
register: rel.register,
|
|
710
|
+
partitives: rel.members.map(m => ({
|
|
711
|
+
uri: m.uri,
|
|
712
|
+
presence: m.presence,
|
|
713
|
+
count: m.count,
|
|
714
|
+
/* Every generic member is delimiting by definition (each carries
|
|
715
|
+
its own delimiting characteristic per ISO 704 §5.5.4.2.1). */
|
|
716
|
+
isDelimiting: true,
|
|
717
|
+
})),
|
|
718
|
+
}));
|
|
719
|
+
drawRakeBundles(svg, pos, {
|
|
720
|
+
isDark: uiStore.isDark,
|
|
721
|
+
color: uiStore.isDark ? '#fbbf24' : '#b45309',
|
|
722
|
+
isMuted: mutedTypes.value.has('__generic__'),
|
|
723
|
+
relations: genericNormalized,
|
|
724
|
+
});
|
|
694
725
|
}
|
|
695
726
|
|
|
696
727
|
/* ── Render DOM nodes ───────────────────────────────────── */
|
|
@@ -702,9 +733,13 @@ function renderDOM() {
|
|
|
702
733
|
const cx = c ? c.clientWidth / 2 : 0;
|
|
703
734
|
const cy = c ? c.clientHeight * 0.46 : 0;
|
|
704
735
|
|
|
736
|
+
/* Cards with no visible edges (after applying type + register mutes)
|
|
737
|
+
float with no connections — hide them so toggling a relation type
|
|
738
|
+
cleans up orphaned cards. Focus node (depth 0) always renders. */
|
|
739
|
+
const visibleIds = visibleNodeIds(nodes, links, mutedTypes.value, mutedRegisters.value);
|
|
740
|
+
|
|
705
741
|
for (const n of nodes) {
|
|
706
|
-
|
|
707
|
-
if (n.depth !== 0 && mutedRegisters.value.has(n.register)) continue;
|
|
742
|
+
if (!visibleIds.has(n.id)) continue;
|
|
708
743
|
|
|
709
744
|
const dsColor = getColor(n.register) || '#888';
|
|
710
745
|
const el = document.createElement('div');
|
|
@@ -1020,6 +1055,16 @@ const legendItems = computed(() => {
|
|
|
1020
1055
|
count: partitiveCount,
|
|
1021
1056
|
});
|
|
1022
1057
|
}
|
|
1058
|
+
/* Synthetic entry for GenericRelations — same idea, distinct hue. */
|
|
1059
|
+
const genericCount = props.genericRelations?.length ?? 0;
|
|
1060
|
+
if (genericCount > 0) {
|
|
1061
|
+
typeMap.set('__generic__', {
|
|
1062
|
+
type: '__generic__',
|
|
1063
|
+
label: 'generic relation',
|
|
1064
|
+
color: uiStore.isDark ? '#fbbf24' : '#b45309',
|
|
1065
|
+
count: genericCount,
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1023
1068
|
return Array.from(typeMap.values()).sort((a, b) => b.count - a.count);
|
|
1024
1069
|
});
|
|
1025
1070
|
|
|
@@ -1164,6 +1209,9 @@ function toggleType(type: string) {
|
|
|
1164
1209
|
mutedTypes.value = n;
|
|
1165
1210
|
const c = canvasRef.value;
|
|
1166
1211
|
if (c) drawEdges(c.clientWidth / 2, c.clientHeight * 0.46);
|
|
1212
|
+
/* Re-render cards so nodes whose only edges were the toggled type
|
|
1213
|
+
disappear (and reappear when the type is unmuted). */
|
|
1214
|
+
renderDOM();
|
|
1167
1215
|
/* Re-heat the simulation so nodes drift to a new equilibrium with
|
|
1168
1216
|
the freed/hidden edges. The link force reads mutedTypes each tick,
|
|
1169
1217
|
so toggling effectively "removes" the edge from the force calc. */
|
|
@@ -39,9 +39,9 @@ export interface EdgeDisplay {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function useConceptEdges(
|
|
42
|
-
concept: ComputedRef<Concept>,
|
|
42
|
+
concept: ComputedRef<Concept | null | undefined>,
|
|
43
43
|
registerId: ComputedRef<string>,
|
|
44
|
-
manifest: ComputedRef<Manifest>,
|
|
44
|
+
manifest: ComputedRef<Manifest | null | undefined>,
|
|
45
45
|
edges: ComputedRef<GraphEdge[]>,
|
|
46
46
|
router: Router,
|
|
47
47
|
) {
|
|
@@ -50,24 +50,30 @@ export function useConceptEdges(
|
|
|
50
50
|
const { getColor } = useDsStyle();
|
|
51
51
|
const { locale } = useI18n();
|
|
52
52
|
|
|
53
|
-
const conceptUriValue = computed(() =>
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
const conceptUriValue = computed(() => {
|
|
54
|
+
if (!concept.value || !manifest.value) return null;
|
|
55
|
+
return conceptUri(concept.value, registerId.value, manifest.value.uriBase);
|
|
56
|
+
});
|
|
56
57
|
|
|
57
|
-
const outgoingEdges = computed(() =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
const outgoingEdges = computed(() => {
|
|
59
|
+
const uri = conceptUriValue.value;
|
|
60
|
+
if (!uri) return [];
|
|
61
|
+
return store.graph.getUniqueEdges(uri, 'outgoing', 'target')
|
|
62
|
+
.filter(e => e.type !== 'domain' && e.type !== 'section');
|
|
63
|
+
});
|
|
61
64
|
|
|
62
|
-
const incomingEdges = computed(() =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const incomingEdges = computed(() => {
|
|
66
|
+
const uri = conceptUriValue.value;
|
|
67
|
+
if (!uri) return [];
|
|
68
|
+
return store.graph.getUniqueEdges(uri, 'incoming', 'source')
|
|
69
|
+
.filter(e => e.type !== 'domain' && e.type !== 'section');
|
|
70
|
+
});
|
|
66
71
|
|
|
67
72
|
const edgeDisplayCache = computed(() => {
|
|
68
73
|
const cache = new Map<string, EdgeDisplay>();
|
|
74
|
+
const focusUri = conceptUriValue.value;
|
|
69
75
|
for (const e of edges.value) {
|
|
70
|
-
const uri = e.source ===
|
|
76
|
+
const uri = e.source === focusUri ? e.target : e.source;
|
|
71
77
|
if (cache.has(uri)) continue;
|
|
72
78
|
const resolution = factory.resolve(uri, registerId.value);
|
|
73
79
|
const isLocal = resolution.type === 'internal' && resolution.registerId === registerId.value;
|
|
@@ -131,6 +137,7 @@ export function useConceptEdges(
|
|
|
131
137
|
|
|
132
138
|
// Concept-level related concepts (managed concept cross-references)
|
|
133
139
|
const conceptRelated = computed(() => {
|
|
140
|
+
if (!concept.value) return [];
|
|
134
141
|
const direct = concept.value.relatedConcepts?.filter(rc => !INVERSE_RELATIONSHIPS[rc.type]) ?? [];
|
|
135
142
|
const derived = incomingEdges.value
|
|
136
143
|
.filter(e => INVERSE_RELATIONSHIPS[e.type])
|
|
@@ -169,6 +176,7 @@ export function useConceptEdges(
|
|
|
169
176
|
// discrimination is via instanceof.
|
|
170
177
|
function projectHyperedge(rel: any): PartitiveRelationWire | GenericRelationWire | null {
|
|
171
178
|
const source = conceptUriValue.value;
|
|
179
|
+
if (!source) return null;
|
|
172
180
|
const comprehensive = resolveConceptRefUri(rel.comprehensive);
|
|
173
181
|
if (!comprehensive) return null;
|
|
174
182
|
const isGeneric = rel instanceof GenericHyperedge;
|
|
@@ -202,14 +210,16 @@ export function useConceptEdges(
|
|
|
202
210
|
}
|
|
203
211
|
|
|
204
212
|
const conceptPartitiveRelations = computed<PartitiveRelationWire[]>(() => {
|
|
205
|
-
|
|
213
|
+
if (!concept.value) return [];
|
|
214
|
+
return ((concept.value.relations as any[]) ?? [])
|
|
206
215
|
.filter(r => r instanceof PartitiveHyperedge)
|
|
207
216
|
.map(projectHyperedge)
|
|
208
217
|
.filter((r): r is PartitiveRelationWire => r !== null);
|
|
209
218
|
});
|
|
210
219
|
|
|
211
220
|
const conceptGenericRelations = computed<GenericRelationWire[]>(() => {
|
|
212
|
-
|
|
221
|
+
if (!concept.value) return [];
|
|
222
|
+
return ((concept.value.relations as any[]) ?? [])
|
|
213
223
|
.filter(r => r instanceof GenericHyperedge)
|
|
214
224
|
.map(projectHyperedge)
|
|
215
225
|
.filter((r): r is GenericRelationWire => r !== null);
|
package/src/utils/sphere-math.ts
CHANGED
|
@@ -52,3 +52,57 @@ export function idToUriGet(map: Map<string, string>, uri: string): string | unde
|
|
|
52
52
|
for (const [id, u] of map) if (u === uri) return id;
|
|
53
53
|
return undefined;
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
export interface SphereNodeLike {
|
|
57
|
+
id: string;
|
|
58
|
+
register: string;
|
|
59
|
+
depth: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SphereLinkLike {
|
|
63
|
+
source: string;
|
|
64
|
+
target: string;
|
|
65
|
+
type: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* IDs of nodes that should render given the current mute state.
|
|
70
|
+
*
|
|
71
|
+
* The focus node (depth 0) is always visible. Every other node must have
|
|
72
|
+
* at least one edge whose type is not muted AND whose other endpoint is
|
|
73
|
+
* not in a muted register — otherwise the card floats with no connections
|
|
74
|
+
* and should disappear when its relations are hidden.
|
|
75
|
+
*
|
|
76
|
+
* Extracted as a pure function so the visibility contract can be tested
|
|
77
|
+
* without mounting the sphere component.
|
|
78
|
+
*/
|
|
79
|
+
export function visibleNodeIds(
|
|
80
|
+
nodes: readonly SphereNodeLike[],
|
|
81
|
+
links: readonly SphereLinkLike[],
|
|
82
|
+
mutedTypes: ReadonlySet<string>,
|
|
83
|
+
mutedRegisters: ReadonlySet<string>,
|
|
84
|
+
): Set<string> {
|
|
85
|
+
const byId = new Map<string, SphereNodeLike>();
|
|
86
|
+
for (const n of nodes) byId.set(n.id, n);
|
|
87
|
+
|
|
88
|
+
const visible = new Set<string>();
|
|
89
|
+
for (const n of nodes) {
|
|
90
|
+
if (n.depth === 0) {
|
|
91
|
+
visible.add(n.id);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (mutedRegisters.has(n.register)) continue;
|
|
95
|
+
for (const l of links) {
|
|
96
|
+
if (mutedTypes.has(l.type)) continue;
|
|
97
|
+
const otherId = l.source === n.id ? l.target : l.target === n.id ? l.source : null;
|
|
98
|
+
if (!otherId) continue;
|
|
99
|
+
const other = byId.get(otherId);
|
|
100
|
+
if (!other) continue;
|
|
101
|
+
if (other.depth !== 0 && mutedRegisters.has(other.register)) continue;
|
|
102
|
+
visible.add(n.id);
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return visible;
|
|
107
|
+
}
|
|
108
|
+
|
|
@@ -2,14 +2,10 @@
|
|
|
2
2
|
import { computed, watch, ref, onMounted, onUnmounted } from 'vue';
|
|
3
3
|
import { useRouter } from 'vue-router';
|
|
4
4
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
5
|
-
import { conceptUri } from '../adapters/model-bridge';
|
|
6
|
-
import { UriRouter } from '../adapters/UriRouter';
|
|
7
|
-
import type { PartitiveRelationWire, PartitiveMemberWire } from '../adapters/types';
|
|
8
|
-
import type { PartitiveMember } from 'glossarist/models';
|
|
9
|
-
import type { PartitivePresence, PartitiveCount } from '../utils/partitive-multiplicity';
|
|
10
5
|
import ConceptDetail from '../components/ConceptDetail.vue';
|
|
11
6
|
import RelationSphere from '../components/RelationSphere.vue';
|
|
12
7
|
import ShortcutsModal from '../components/ShortcutsModal.vue';
|
|
8
|
+
import { useConceptEdges } from '../composables/use-concept-edges';
|
|
13
9
|
import { useI18n } from '../i18n';
|
|
14
10
|
|
|
15
11
|
const { t } = useI18n();
|
|
@@ -63,47 +59,13 @@ const edges = computed(() => store.conceptEdges);
|
|
|
63
59
|
const activeRegisterId = computed(() => sphereFocusPayload.value?.registerId ?? props.registerId);
|
|
64
60
|
const adjacent = ref({ prev: null as string | null, next: null as string | null });
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const c = concept.value;
|
|
74
|
-
const m = manifest.value;
|
|
75
|
-
if (!c || !m) return [];
|
|
76
|
-
const sourceUri = conceptUri(c, activeRegisterId.value, m.uriBase);
|
|
77
|
-
const out: PartitiveRelationWire[] = [];
|
|
78
|
-
for (const rel of c.partitiveRelations ?? []) {
|
|
79
|
-
const compRef = rel.comprehensive;
|
|
80
|
-
const compTarget = store.manifests.get(activeRegisterId.value)
|
|
81
|
-
? UriRouter.buildConceptUri(m.uriBase, activeRegisterId.value, compRef.id ?? '')
|
|
82
|
-
: null;
|
|
83
|
-
if (!compRef.id || !compTarget) continue;
|
|
84
|
-
const partitives: PartitiveMemberWire[] = rel.partitives
|
|
85
|
-
.map((member): PartitiveMemberWire | null => {
|
|
86
|
-
const ref = member.ref;
|
|
87
|
-
if (!ref?.id) return null;
|
|
88
|
-
const uri = UriRouter.buildConceptUri(m.uriBase, activeRegisterId.value, ref.id);
|
|
89
|
-
if (uri === sourceUri) return null;
|
|
90
|
-
const presence: PartitivePresence = member.presence;
|
|
91
|
-
const count: PartitiveCount = member.count;
|
|
92
|
-
const isDelimiting = (member as any).is_delimiting === true;
|
|
93
|
-
return { uri, presence, count, isDelimiting };
|
|
94
|
-
})
|
|
95
|
-
.filter((x): x is PartitiveMemberWire => x !== null);
|
|
96
|
-
if (partitives.length === 0) continue;
|
|
97
|
-
out.push({
|
|
98
|
-
source: sourceUri,
|
|
99
|
-
comprehensive: compTarget,
|
|
100
|
-
partitives,
|
|
101
|
-
completeness: rel.completeness,
|
|
102
|
-
register: activeRegisterId.value,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
106
|
-
});
|
|
62
|
+
const { conceptPartitiveRelations, conceptGenericRelations } = useConceptEdges(
|
|
63
|
+
concept,
|
|
64
|
+
activeRegisterId,
|
|
65
|
+
manifest,
|
|
66
|
+
edges,
|
|
67
|
+
router,
|
|
68
|
+
);
|
|
107
69
|
const viewMode = ref<'detail' | 'sphere'>(
|
|
108
70
|
router.currentRoute.value.query.view === 'sphere' ? 'sphere' : 'detail'
|
|
109
71
|
);
|
|
@@ -326,7 +288,8 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown));
|
|
|
326
288
|
:manifest="manifest"
|
|
327
289
|
:register-id="activeRegisterId"
|
|
328
290
|
:edges="edges"
|
|
329
|
-
:partitive-relations="
|
|
291
|
+
:partitive-relations="conceptPartitiveRelations"
|
|
292
|
+
:generic-relations="conceptGenericRelations"
|
|
330
293
|
@navigate="onSphereNavigate"
|
|
331
294
|
/>
|
|
332
295
|
<!-- Detail mode -->
|