@glossarist/concept-browser 0.7.82 → 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 +1 -1
- package/src/__tests__/components/PartitiveRelationDiagram.test.ts +79 -0
- package/src/components/PartitiveRelationDiagram.vue +113 -51
- package/src/components/PartitiveRelationList.vue +23 -9
- package/src/components/RelationSphere.vue +214 -3
- package/src/composables/use-concept-edges.ts +36 -0
- package/src/views/ConceptView.vue +52 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
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": {
|
|
@@ -166,4 +166,83 @@ describe('PartitiveRelationDiagram — ISO 704 rake rendering', () => {
|
|
|
166
166
|
expect(w.emitted('navigate')).toBeTruthy();
|
|
167
167
|
});
|
|
168
168
|
});
|
|
169
|
+
|
|
170
|
+
describe('designation-aware sizing', () => {
|
|
171
|
+
it('uses MIN_NODE_W when labels are short', () => {
|
|
172
|
+
const w = mount(PartitiveRelationDiagram, {
|
|
173
|
+
props: {
|
|
174
|
+
comprehensiveLabel: '1.3',
|
|
175
|
+
partitives: members(2),
|
|
176
|
+
completeness: 'complete',
|
|
177
|
+
plurality: null,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const svg = w.find('svg');
|
|
181
|
+
// Short labels → MIN_NODE_W=110, total = 2*110 + 16 + 40 = 276
|
|
182
|
+
const viewBox = svg.attributes('viewBox') || '';
|
|
183
|
+
const width = Number(viewBox.split(' ')[2]);
|
|
184
|
+
expect(width).toBeLessThan(300);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('grows nodes for long designations', () => {
|
|
188
|
+
const longMembers: PartitiveMemberLabeled[] = [
|
|
189
|
+
{ uri: 'u1', label: 'system of quantities', certainty: 'confirmed' },
|
|
190
|
+
{ uri: 'u2', label: 'International System of Units', certainty: 'confirmed' },
|
|
191
|
+
{ uri: 'u3', label: 'base quantity dimension', certainty: 'confirmed' },
|
|
192
|
+
];
|
|
193
|
+
const w = mount(PartitiveRelationDiagram, {
|
|
194
|
+
props: {
|
|
195
|
+
comprehensiveLabel: 'coordinate system of quantities',
|
|
196
|
+
partitives: longMembers,
|
|
197
|
+
completeness: 'complete',
|
|
198
|
+
plurality: null,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
const svg = w.find('svg');
|
|
202
|
+
const viewBox = svg.attributes('viewBox') || '';
|
|
203
|
+
const width = Number(viewBox.split(' ')[2]);
|
|
204
|
+
// Each node wider than MIN_NODE_W → total > 3*110 = 330
|
|
205
|
+
expect(width).toBeGreaterThan(330);
|
|
206
|
+
// Capped: node width ≤ MAX_NODE_W=220 → total ≤ 3*220 + 2*16 + 40 = 732
|
|
207
|
+
expect(width).toBeLessThanOrEqual(732);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('wraps long labels to 2 lines via tspan when needed', () => {
|
|
211
|
+
const longLabel = 'International System of Quantities and Units';
|
|
212
|
+
const w = mount(PartitiveRelationDiagram, {
|
|
213
|
+
props: {
|
|
214
|
+
comprehensiveLabel: longLabel,
|
|
215
|
+
partitives: [
|
|
216
|
+
{ uri: 'u1', label: 'short', certainty: 'confirmed' },
|
|
217
|
+
{ uri: 'u2', label: 'short', certainty: 'confirmed' },
|
|
218
|
+
],
|
|
219
|
+
completeness: 'complete',
|
|
220
|
+
plurality: null,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
// The comprehensive text should have ≥ 2 tspans (wrapped)
|
|
224
|
+
const texts = w.findAll('text');
|
|
225
|
+
const compText = texts[0]; // first text is the comprehensive
|
|
226
|
+
const tspans = compText.findAll('tspan');
|
|
227
|
+
expect(tspans.length).toBeGreaterThanOrEqual(2);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('renders the designation (not the conceptId) as the label', () => {
|
|
231
|
+
const w = mount(PartitiveRelationDiagram, {
|
|
232
|
+
props: {
|
|
233
|
+
comprehensiveLabel: 'system of quantities',
|
|
234
|
+
partitives: [
|
|
235
|
+
{ uri: 'u1', label: 'base quantity', certainty: 'confirmed' },
|
|
236
|
+
{ uri: 'u2', label: 'derived quantity', certainty: 'confirmed' },
|
|
237
|
+
],
|
|
238
|
+
completeness: 'complete',
|
|
239
|
+
plurality: null,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
const allText = w.findAll('text').map(t => t.text());
|
|
243
|
+
expect(allText).toContain('system of quantities');
|
|
244
|
+
expect(allText).toContain('base quantity');
|
|
245
|
+
expect(allText).toContain('derived quantity');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
169
248
|
});
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
* on stem + spine (drops stay single)
|
|
20
20
|
* - plurality.isShared && isUncertain : one solid + one DASHED line
|
|
21
21
|
* - completeness === 'partial' : spine extends past the last
|
|
22
|
-
* tooth (continued backline
|
|
23
|
-
* "more exist but aren't shown")
|
|
22
|
+
* tooth (continued backline)
|
|
24
23
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Node sizing is designation-aware: the longest label drives the node
|
|
25
|
+
* width (clamped to [MIN_NODE_W, MAX_NODE_W]); labels that don't fit
|
|
26
|
+
* wrap to a second line via tspan. This lets the rake show actual
|
|
27
|
+
* designations ('system of quantities') instead of bare IDs ('1.3').
|
|
27
28
|
*/
|
|
28
29
|
import { computed } from 'vue';
|
|
29
30
|
import type { TypeSharedPluralityWire } from '../adapters/types';
|
|
@@ -48,17 +49,21 @@ const emit = defineEmits<{
|
|
|
48
49
|
(e: 'navigate', uri: string): void;
|
|
49
50
|
}>();
|
|
50
51
|
|
|
51
|
-
// Layout constants
|
|
52
|
-
const
|
|
53
|
-
const
|
|
52
|
+
// Layout constants
|
|
53
|
+
const MIN_NODE_W = 110;
|
|
54
|
+
const MAX_NODE_W = 220;
|
|
55
|
+
const SINGLE_LINE_H = 32;
|
|
56
|
+
const DOUBLE_LINE_H = 50;
|
|
54
57
|
const STEM_LEN = 28;
|
|
55
58
|
const SPINE_TO_NODE = 32;
|
|
56
59
|
const GAP_X = 16;
|
|
57
|
-
const PADDING_X =
|
|
58
|
-
const PADDING_Y =
|
|
59
|
-
const DOUBLE_GAP = 3;
|
|
60
|
-
const PARTIAL_TAIL = 32;
|
|
60
|
+
const PADDING_X = 20;
|
|
61
|
+
const PADDING_Y = 14;
|
|
62
|
+
const DOUBLE_GAP = 3;
|
|
63
|
+
const PARTIAL_TAIL = 32;
|
|
61
64
|
const DASH_PATTERN = '4 3';
|
|
65
|
+
const CHAR_W = 6.2; // approx for 11px sans-serif
|
|
66
|
+
const NODE_TEXT_PAD = 16; // horizontal padding inside node rect
|
|
62
67
|
|
|
63
68
|
type LineVariant = 'single' | 'double-solid' | 'solid-plus-dashed';
|
|
64
69
|
|
|
@@ -67,19 +72,67 @@ const lineVariant = computed<LineVariant>(() => {
|
|
|
67
72
|
return props.plurality.isUncertain ? 'solid-plus-dashed' : 'double-solid';
|
|
68
73
|
});
|
|
69
74
|
|
|
75
|
+
/** Estimated pixel width of a string at 11px sans-serif. */
|
|
76
|
+
function estimateTextWidth(text: string): number {
|
|
77
|
+
return text.length * CHAR_W;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Wrap a label to at most 2 lines, breaking on word boundaries.
|
|
82
|
+
* If a single word exceeds the limit, it stays on its own line and
|
|
83
|
+
* gets truncated with an ellipsis.
|
|
84
|
+
*/
|
|
85
|
+
function wrapLabel(text: string, maxCharsPerLine: number): string[] {
|
|
86
|
+
if (text.length <= maxCharsPerLine) return [text];
|
|
87
|
+
const words = text.split(/\s+/);
|
|
88
|
+
const lines: string[] = [];
|
|
89
|
+
let current = '';
|
|
90
|
+
for (const w of words) {
|
|
91
|
+
if (w.length > maxCharsPerLine) {
|
|
92
|
+
// word itself too long — flush current, push truncated word
|
|
93
|
+
if (current) { lines.push(current); current = ''; }
|
|
94
|
+
lines.push(w.slice(0, Math.max(1, maxCharsPerLine - 1)) + '…');
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!current) {
|
|
98
|
+
current = w;
|
|
99
|
+
} else if ((current + ' ' + w).length <= maxCharsPerLine) {
|
|
100
|
+
current += ' ' + w;
|
|
101
|
+
} else {
|
|
102
|
+
lines.push(current);
|
|
103
|
+
current = w;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (current) lines.push(current);
|
|
107
|
+
if (lines.length > 2) {
|
|
108
|
+
return [lines[0], lines.slice(1).join(' ').slice(0, maxCharsPerLine - 1) + '…'];
|
|
109
|
+
}
|
|
110
|
+
return lines;
|
|
111
|
+
}
|
|
112
|
+
|
|
70
113
|
const layout = computed(() => {
|
|
114
|
+
const allLabels = [props.comprehensiveLabel, ...props.partitives.map(p => p.label)];
|
|
115
|
+
const longestWidth = Math.max(...allLabels.map(estimateTextWidth));
|
|
116
|
+
const nodeW = Math.min(MAX_NODE_W, Math.max(MIN_NODE_W, Math.ceil(longestWidth) + NODE_TEXT_PAD));
|
|
117
|
+
const charsPerLine = Math.max(10, Math.floor((nodeW - NODE_TEXT_PAD) / CHAR_W));
|
|
118
|
+
|
|
119
|
+
const wrapped = allLabels.map(label => wrapLabel(label, charsPerLine));
|
|
120
|
+
const anyWrapped = wrapped.some(lines => lines.length > 1);
|
|
121
|
+
const nodeH = anyWrapped ? DOUBLE_LINE_H : SINGLE_LINE_H;
|
|
122
|
+
|
|
71
123
|
const n = props.partitives.length;
|
|
72
|
-
const totalWidth = PADDING_X * 2 + n *
|
|
73
|
-
const totalHeight = PADDING_Y * 2 +
|
|
124
|
+
const totalWidth = PADDING_X * 2 + n * nodeW + (n - 1) * GAP_X;
|
|
125
|
+
const totalHeight = PADDING_Y * 2 + nodeH + STEM_LEN + SPINE_TO_NODE + nodeH;
|
|
74
126
|
|
|
75
127
|
const centerX = totalWidth / 2;
|
|
76
|
-
const compY = PADDING_Y +
|
|
77
|
-
const spineY = compY +
|
|
128
|
+
const compY = PADDING_Y + nodeH / 2;
|
|
129
|
+
const spineY = compY + nodeH / 2 + STEM_LEN;
|
|
78
130
|
const partY = spineY + SPINE_TO_NODE;
|
|
79
131
|
|
|
80
132
|
const partitiveSlots = props.partitives.map((member, i) => {
|
|
81
|
-
const x = PADDING_X +
|
|
82
|
-
|
|
133
|
+
const x = PADDING_X + nodeW / 2 + i * (nodeW + GAP_X);
|
|
134
|
+
const labelLines = wrapped[i + 1]; // index 0 is comprehensive
|
|
135
|
+
return { member, x, labelLines };
|
|
83
136
|
});
|
|
84
137
|
|
|
85
138
|
const leftX = partitiveSlots[0]?.x ?? centerX;
|
|
@@ -89,6 +142,8 @@ const layout = computed(() => {
|
|
|
89
142
|
: lastX;
|
|
90
143
|
|
|
91
144
|
return {
|
|
145
|
+
nodeW,
|
|
146
|
+
nodeH,
|
|
92
147
|
totalWidth,
|
|
93
148
|
totalHeight,
|
|
94
149
|
centerX,
|
|
@@ -96,16 +151,20 @@ const layout = computed(() => {
|
|
|
96
151
|
spineY,
|
|
97
152
|
partY,
|
|
98
153
|
partitiveSlots,
|
|
154
|
+
comprehensiveLines: wrapped[0],
|
|
99
155
|
leftX,
|
|
100
156
|
spineEndX,
|
|
101
157
|
};
|
|
102
158
|
});
|
|
103
159
|
|
|
104
|
-
function strokeAttr(variant: LineVariant, opacity = 1) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
160
|
+
function strokeAttr(variant: LineVariant, opacity = 1, override?: 'dash') {
|
|
161
|
+
const useDash = override === 'dash' || variant === 'solid-plus-dashed';
|
|
162
|
+
return {
|
|
163
|
+
stroke: 'currentColor',
|
|
164
|
+
'stroke-width': 1.4,
|
|
165
|
+
opacity,
|
|
166
|
+
...(useDash ? { 'stroke-dasharray': DASH_PATTERN } : {}),
|
|
167
|
+
};
|
|
109
168
|
}
|
|
110
169
|
|
|
111
170
|
function emitNav(uri: string) {
|
|
@@ -126,28 +185,29 @@ function emitNav(uri: string) {
|
|
|
126
185
|
<!-- Comprehensive label -->
|
|
127
186
|
<g>
|
|
128
187
|
<rect
|
|
129
|
-
:x="layout.centerX -
|
|
130
|
-
:y="layout.compY -
|
|
131
|
-
:width="
|
|
132
|
-
:height="
|
|
188
|
+
:x="layout.centerX - layout.nodeW / 2"
|
|
189
|
+
:y="layout.compY - layout.nodeH / 2"
|
|
190
|
+
:width="layout.nodeW"
|
|
191
|
+
:height="layout.nodeH"
|
|
133
192
|
rx="6"
|
|
134
193
|
class="fill-emerald-50 dark:fill-emerald-900/30 stroke-current"
|
|
135
194
|
stroke-width="1.2"
|
|
136
195
|
/>
|
|
137
|
-
<text
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
196
|
+
<text :x="layout.centerX" text-anchor="middle" class="text-[11px] fill-emerald-900 dark:fill-emerald-100">
|
|
197
|
+
<tspan
|
|
198
|
+
v-for="(line, i) in layout.comprehensiveLines"
|
|
199
|
+
:key="i"
|
|
200
|
+
:x="layout.centerX"
|
|
201
|
+
:y="layout.compY - (layout.comprehensiveLines.length - 1) * 7 + i * 14"
|
|
202
|
+
>{{ line }}</tspan>
|
|
203
|
+
</text>
|
|
144
204
|
</g>
|
|
145
205
|
|
|
146
206
|
<!-- Criterion italic under the comprehensive -->
|
|
147
207
|
<text
|
|
148
208
|
v-if="criterion"
|
|
149
209
|
:x="layout.centerX"
|
|
150
|
-
:y="layout.compY +
|
|
210
|
+
:y="layout.compY + layout.nodeH / 2 + 10"
|
|
151
211
|
text-anchor="middle"
|
|
152
212
|
class="text-[9px] italic fill-emerald-800 dark:fill-emerald-200/80"
|
|
153
213
|
>{{ criterion }}</text>
|
|
@@ -155,20 +215,20 @@ function emitNav(uri: string) {
|
|
|
155
215
|
<!-- Stem (vertical from comprehensive to spine) -->
|
|
156
216
|
<line
|
|
157
217
|
v-if="lineVariant === 'single'"
|
|
158
|
-
:x1="layout.centerX" :y1="layout.compY +
|
|
218
|
+
:x1="layout.centerX" :y1="layout.compY + layout.nodeH / 2"
|
|
159
219
|
:x2="layout.centerX" :y2="layout.spineY"
|
|
160
220
|
v-bind="strokeAttr('single')"
|
|
161
221
|
/>
|
|
162
222
|
<template v-else>
|
|
163
223
|
<line
|
|
164
|
-
:x1="layout.centerX - DOUBLE_GAP / 2" :y1="layout.compY +
|
|
224
|
+
:x1="layout.centerX - DOUBLE_GAP / 2" :y1="layout.compY + layout.nodeH / 2"
|
|
165
225
|
:x2="layout.centerX - DOUBLE_GAP / 2" :y2="layout.spineY"
|
|
166
226
|
v-bind="strokeAttr('double-solid')"
|
|
167
227
|
/>
|
|
168
228
|
<line
|
|
169
|
-
:x1="layout.centerX + DOUBLE_GAP / 2" :y1="layout.compY +
|
|
229
|
+
:x1="layout.centerX + DOUBLE_GAP / 2" :y1="layout.compY + layout.nodeH / 2"
|
|
170
230
|
:x2="layout.centerX + DOUBLE_GAP / 2" :y2="layout.spineY"
|
|
171
|
-
v-bind="strokeAttr(lineVariant === 'solid-plus-dashed' ? '
|
|
231
|
+
v-bind="strokeAttr(lineVariant, 1, lineVariant === 'solid-plus-dashed' ? 'dash' : undefined)"
|
|
172
232
|
/>
|
|
173
233
|
</template>
|
|
174
234
|
|
|
@@ -188,26 +248,23 @@ function emitNav(uri: string) {
|
|
|
188
248
|
<line
|
|
189
249
|
:x1="layout.leftX" :y1="layout.spineY + DOUBLE_GAP / 2"
|
|
190
250
|
:x2="layout.spineEndX" :y2="layout.spineY + DOUBLE_GAP / 2"
|
|
191
|
-
v-bind="strokeAttr(lineVariant === 'solid-plus-dashed' ? '
|
|
251
|
+
v-bind="strokeAttr(lineVariant, 1, lineVariant === 'solid-plus-dashed' ? 'dash' : undefined)"
|
|
192
252
|
/>
|
|
193
253
|
</template>
|
|
194
254
|
|
|
195
255
|
<!-- Drops + partitive nodes -->
|
|
196
256
|
<g v-for="(slot, i) in layout.partitiveSlots" :key="i">
|
|
197
|
-
<!-- drop from spine to node (always single line per ISO 704) -->
|
|
198
257
|
<line
|
|
199
258
|
:x1="slot.x" :y1="layout.spineY"
|
|
200
|
-
:x2="slot.x" :y2="layout.partY -
|
|
201
|
-
v-bind="strokeAttr('single', slot.member.certainty === 'possible' ? 0.5 : 1)"
|
|
202
|
-
:stroke-dasharray="slot.member.certainty === 'possible' ? DASH_PATTERN : undefined"
|
|
259
|
+
:x2="slot.x" :y2="layout.partY - layout.nodeH / 2"
|
|
260
|
+
v-bind="strokeAttr('single', slot.member.certainty === 'possible' ? 0.5 : 1, slot.member.certainty === 'possible' ? 'dash' : undefined)"
|
|
203
261
|
/>
|
|
204
|
-
<!-- node -->
|
|
205
262
|
<g class="cursor-pointer" @click="emitNav(slot.member.uri)">
|
|
206
263
|
<rect
|
|
207
|
-
:x="slot.x -
|
|
208
|
-
:y="layout.partY -
|
|
209
|
-
:width="
|
|
210
|
-
:height="
|
|
264
|
+
:x="slot.x - layout.nodeW / 2"
|
|
265
|
+
:y="layout.partY - layout.nodeH / 2"
|
|
266
|
+
:width="layout.nodeW"
|
|
267
|
+
:height="layout.nodeH"
|
|
211
268
|
rx="6"
|
|
212
269
|
:class="slot.member.certainty === 'possible'
|
|
213
270
|
? 'fill-emerald-50/60 dark:fill-emerald-900/20 stroke-current'
|
|
@@ -217,13 +274,18 @@ function emitNav(uri: string) {
|
|
|
217
274
|
/>
|
|
218
275
|
<text
|
|
219
276
|
:x="slot.x"
|
|
220
|
-
:y="layout.partY"
|
|
221
277
|
text-anchor="middle"
|
|
222
|
-
dominant-baseline="middle"
|
|
223
278
|
:class="slot.member.certainty === 'possible'
|
|
224
279
|
? 'text-[11px] fill-emerald-700/70 dark:fill-emerald-200/60 italic'
|
|
225
280
|
: 'text-[11px] fill-emerald-900 dark:fill-emerald-100'"
|
|
226
|
-
>
|
|
281
|
+
>
|
|
282
|
+
<tspan
|
|
283
|
+
v-for="(line, j) in slot.labelLines"
|
|
284
|
+
:key="j"
|
|
285
|
+
:x="slot.x"
|
|
286
|
+
:y="layout.partY - (slot.labelLines.length - 1) * 7 + j * 14"
|
|
287
|
+
>{{ line }}</tspan>
|
|
288
|
+
</text>
|
|
227
289
|
</g>
|
|
228
290
|
</g>
|
|
229
291
|
</svg>
|
|
@@ -15,7 +15,7 @@ import type { PartitiveRelationWire, Manifest } from '../adapters/types';
|
|
|
15
15
|
import { useRouter } from 'vue-router';
|
|
16
16
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
17
17
|
import { getFactory } from '../adapters/factory';
|
|
18
|
-
import { useI18n } from '../i18n';
|
|
18
|
+
import { useI18n, locale } from '../i18n';
|
|
19
19
|
import {
|
|
20
20
|
completenessLabel,
|
|
21
21
|
certaintyLabel,
|
|
@@ -35,12 +35,26 @@ const store = useVocabularyStore();
|
|
|
35
35
|
const factory = getFactory();
|
|
36
36
|
const { t } = useI18n();
|
|
37
37
|
|
|
38
|
-
function
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
|
|
38
|
+
function designationFor(uri: string): string {
|
|
39
|
+
const node = store.graph.getNode(uri);
|
|
40
|
+
if (node) {
|
|
41
|
+
const des = node.designations[locale.value]
|
|
42
|
+
|| node.designations.eng
|
|
43
|
+
|| Object.values(node.designations)[0];
|
|
44
|
+
if (des) return des;
|
|
45
|
+
}
|
|
46
|
+
const resolution = factory.resolve(uri);
|
|
47
|
+
if (resolution.type === 'internal') {
|
|
48
|
+
const adapter = store.datasets.get(resolution.registerId);
|
|
49
|
+
const entry = adapter?.getIndexEntry(resolution.conceptId);
|
|
50
|
+
if (entry) {
|
|
51
|
+
const des = entry.designations[locale.value]
|
|
52
|
+
|| entry.designations.eng
|
|
53
|
+
|| Object.values(entry.designations)[0];
|
|
54
|
+
if (des) return des;
|
|
55
|
+
}
|
|
42
56
|
}
|
|
43
|
-
return uri;
|
|
57
|
+
return resolution.type === 'internal' ? resolution.conceptId : uri;
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
function navigate(uri: string) {
|
|
@@ -78,7 +92,7 @@ function pluralityBadge(plurality: NonNullable<PartitiveRelationWire['plurality'
|
|
|
78
92
|
function labeledMembers(rel: PartitiveRelationWire): PartitiveMemberLabeled[] {
|
|
79
93
|
return rel.partitives.map(m => ({
|
|
80
94
|
uri: m.uri,
|
|
81
|
-
label:
|
|
95
|
+
label: designationFor(m.uri),
|
|
82
96
|
certainty: m.certainty,
|
|
83
97
|
}));
|
|
84
98
|
}
|
|
@@ -110,7 +124,7 @@ function labeledMembers(rel: PartitiveRelationWire): PartitiveMemberLabeled[] {
|
|
|
110
124
|
@click="navigate(rel.comprehensive)"
|
|
111
125
|
class="ml-auto text-sm font-medium text-emerald-900 dark:text-emerald-100 hover:underline truncate"
|
|
112
126
|
>
|
|
113
|
-
{{
|
|
127
|
+
{{ designationFor(rel.comprehensive) }}
|
|
114
128
|
</button>
|
|
115
129
|
</div>
|
|
116
130
|
<div
|
|
@@ -122,7 +136,7 @@ function labeledMembers(rel: PartitiveRelationWire): PartitiveMemberLabeled[] {
|
|
|
122
136
|
<!-- Rake diagram (ISO 704 partitive-relation convention) -->
|
|
123
137
|
<div class="px-3 py-3 overflow-x-auto">
|
|
124
138
|
<PartitiveRelationDiagram
|
|
125
|
-
:comprehensive-label="
|
|
139
|
+
:comprehensive-label="designationFor(rel.comprehensive)"
|
|
126
140
|
:partitives="labeledMembers(rel)"
|
|
127
141
|
:completeness="rel.completeness"
|
|
128
142
|
:plurality="rel.plurality"
|
|
@@ -25,7 +25,7 @@ import { hashSeed, expandParams, portSide, portPoint, idToUriGet } from '../util
|
|
|
25
25
|
import { useI18n } from '../i18n';
|
|
26
26
|
|
|
27
27
|
const { t, locale } = useI18n();
|
|
28
|
-
import type { Concept, Manifest, GraphEdge } from '../adapters/types';
|
|
28
|
+
import type { Concept, Manifest, GraphEdge, PartitiveRelationWire } from '../adapters/types';
|
|
29
29
|
import { conceptUri } from '../adapters/model-bridge';
|
|
30
30
|
|
|
31
31
|
const props = defineProps<{
|
|
@@ -33,6 +33,7 @@ const props = defineProps<{
|
|
|
33
33
|
manifest: Manifest;
|
|
34
34
|
registerId: string;
|
|
35
35
|
edges: GraphEdge[];
|
|
36
|
+
partitiveRelations?: PartitiveRelationWire[];
|
|
36
37
|
}>();
|
|
37
38
|
|
|
38
39
|
const emit = defineEmits<{
|
|
@@ -390,6 +391,38 @@ function buildGraph() {
|
|
|
390
391
|
|
|
391
392
|
nodes = Array.from(nodeMap.values());
|
|
392
393
|
links = resultLinks;
|
|
394
|
+
|
|
395
|
+
/* Ensure every PartitiveRelation member is present in the graph
|
|
396
|
+
so the rake bundle can render. The comprehensive is normally the
|
|
397
|
+
focus; partitives may need to be added as depth-1 neighbors. */
|
|
398
|
+
for (const rel of props.partitiveRelations ?? []) {
|
|
399
|
+
const compParsed = UriRouter.parseUri(rel.comprehensive);
|
|
400
|
+
if (!compParsed) continue;
|
|
401
|
+
const compNodeId = `${compParsed.registerId}/${compParsed.conceptId}`;
|
|
402
|
+
if (!nodeMap.has(compNodeId)) continue;
|
|
403
|
+
|
|
404
|
+
for (const member of rel.partitives) {
|
|
405
|
+
const parsed = UriRouter.parseUri(member.uri);
|
|
406
|
+
if (!parsed) continue;
|
|
407
|
+
const nid = `${parsed.registerId}/${parsed.conceptId}`;
|
|
408
|
+
if (nodeMap.has(nid)) continue;
|
|
409
|
+
if (nodeMap.size >= MAX_NODES) break;
|
|
410
|
+
const pos = fibonacciSpherePosition(1, nodeMap.size, MAX_NODES, hashSeed(parsed.conceptId));
|
|
411
|
+
nodeMap.set(nid, {
|
|
412
|
+
id: nid,
|
|
413
|
+
term: parsed.conceptId,
|
|
414
|
+
ref: parsed.registerId,
|
|
415
|
+
register: parsed.registerId,
|
|
416
|
+
conceptId: parsed.conceptId,
|
|
417
|
+
depth: 1,
|
|
418
|
+
x: pos.x, y: pos.y, z: pos.z,
|
|
419
|
+
vx: 0, vy: 0, vz: 0,
|
|
420
|
+
});
|
|
421
|
+
idToUri.set(nid, member.uri);
|
|
422
|
+
nodes = Array.from(nodeMap.values());
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
393
426
|
graphVersion.value++;
|
|
394
427
|
}
|
|
395
428
|
|
|
@@ -597,8 +630,10 @@ function drawEdges(cx: number, cy: number) {
|
|
|
597
630
|
svg.setAttribute('height', String(h));
|
|
598
631
|
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
|
|
599
632
|
svg.setAttribute('preserveAspectRatio', 'none');
|
|
600
|
-
/* Remove only path + text + rect (edges), keep defs (markers)
|
|
601
|
-
|
|
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());
|
|
602
637
|
if (!svg.querySelector('defs')) ensureMarkers(svg);
|
|
603
638
|
|
|
604
639
|
/* Compute visible node positions */
|
|
@@ -712,6 +747,182 @@ function drawEdges(cx: number, cy: number) {
|
|
|
712
747
|
label.textContent = labelText;
|
|
713
748
|
svg.appendChild(label);
|
|
714
749
|
}
|
|
750
|
+
|
|
751
|
+
drawRakeBundles(svg, pos, cx, cy);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Render PartitiveRelations as ISO 704 rake/bracket bundles in the sphere.
|
|
756
|
+
*
|
|
757
|
+
* Each relation renders as a real bracket:
|
|
758
|
+
*
|
|
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)
|
|
785
|
+
*
|
|
786
|
+
* A diamond marker at the comprehensive end signals the hyperedge origin.
|
|
787
|
+
*/
|
|
788
|
+
function drawRakeBundles(
|
|
789
|
+
svg: SVGSVGElement,
|
|
790
|
+
pos: Map<string, {x: number; y: number}>,
|
|
791
|
+
_cx: number,
|
|
792
|
+
_cy: number,
|
|
793
|
+
) {
|
|
794
|
+
const color = uiStore.isDark ? '#2dd4bf' : '#0d9488';
|
|
795
|
+
const DOUBLE_GAP = 4;
|
|
796
|
+
const DASH = '4 3';
|
|
797
|
+
|
|
798
|
+
for (const rel of props.partitiveRelations ?? []) {
|
|
799
|
+
const compParsed = UriRouter.parseUri(rel.comprehensive);
|
|
800
|
+
if (!compParsed) continue;
|
|
801
|
+
const compNodeId = `${compParsed.registerId}/${compParsed.conceptId}`;
|
|
802
|
+
const compPos = pos.get(compNodeId);
|
|
803
|
+
if (!compPos) continue;
|
|
804
|
+
|
|
805
|
+
const memberPositions = rel.partitives
|
|
806
|
+
.map(member => {
|
|
807
|
+
const parsed = UriRouter.parseUri(member.uri);
|
|
808
|
+
if (!parsed) return null;
|
|
809
|
+
const nid = `${parsed.registerId}/${parsed.conceptId}`;
|
|
810
|
+
const p = pos.get(nid);
|
|
811
|
+
return p ? { member, pos: p } : null;
|
|
812
|
+
})
|
|
813
|
+
.filter((x): x is { member: { uri: string; certainty: 'confirmed' | 'possible' }; pos: {x: number; y: number} } => x !== null);
|
|
814
|
+
|
|
815
|
+
if (memberPositions.length === 0) continue;
|
|
816
|
+
|
|
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
|
+
|
|
822
|
+
const isShared = !!rel.plurality?.isShared;
|
|
823
|
+
const isUncertain = !!rel.plurality?.isUncertain;
|
|
824
|
+
|
|
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
|
+
};
|
|
868
|
+
|
|
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
|
+
});
|
|
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
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
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)
|
|
916
|
+
const diamond = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
917
|
+
const dsize = 5;
|
|
918
|
+
diamond.setAttribute('d',
|
|
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`);
|
|
920
|
+
diamond.setAttribute('fill', color);
|
|
921
|
+
diamond.setAttribute('opacity', '0.9');
|
|
922
|
+
diamond.setAttribute('stroke', uiStore.isDark ? '#0a1f1c' : '#ffffff');
|
|
923
|
+
diamond.setAttribute('stroke-width', '1');
|
|
924
|
+
svg.appendChild(diamond);
|
|
925
|
+
}
|
|
715
926
|
}
|
|
716
927
|
|
|
717
928
|
function ensureMarkers(svg: SVGSVGElement) {
|
|
@@ -97,6 +97,41 @@ export function useConceptEdges(
|
|
|
97
97
|
return edgeDisplayCache.value.get(uri) ?? { uri, conceptId: uri, designation: '', tooltip: uri, isLocal: false, badge: null };
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a concept's designation in the current UI locale.
|
|
102
|
+
* Fallback chain: locale → eng → any available.
|
|
103
|
+
*
|
|
104
|
+
* Sources tried in order:
|
|
105
|
+
* 1. BFS graph node (already loaded as a neighbor)
|
|
106
|
+
* 2. Dataset adapter index entry (every concept in the dataset)
|
|
107
|
+
* 3. conceptId as last-resort fallback
|
|
108
|
+
*
|
|
109
|
+
* Used by PartitiveRelationList (sidebar rake diagram) so the rake
|
|
110
|
+
* shows designations like 'system of quantities' instead of bare
|
|
111
|
+
* IDs like '1.3'.
|
|
112
|
+
*/
|
|
113
|
+
function designationFor(uri: string): string {
|
|
114
|
+
const node = store.graph.getNode(uri);
|
|
115
|
+
if (node) {
|
|
116
|
+
const des = node.designations[locale.value]
|
|
117
|
+
|| node.designations.eng
|
|
118
|
+
|| Object.values(node.designations)[0];
|
|
119
|
+
if (des) return des;
|
|
120
|
+
}
|
|
121
|
+
const resolution = factory.resolve(uri);
|
|
122
|
+
if (resolution.type === 'internal') {
|
|
123
|
+
const adapter = store.datasets.get(resolution.registerId);
|
|
124
|
+
const entry = adapter?.getIndexEntry(resolution.conceptId);
|
|
125
|
+
if (entry) {
|
|
126
|
+
const des = entry.designations[locale.value]
|
|
127
|
+
|| entry.designations.eng
|
|
128
|
+
|| Object.values(entry.designations)[0];
|
|
129
|
+
if (des) return des;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return resolution.type === 'internal' ? resolution.conceptId : uri;
|
|
133
|
+
}
|
|
134
|
+
|
|
100
135
|
function edgeBadgeColor(type: string, direction: 'out' | 'in'): string {
|
|
101
136
|
if (type === 'supersedes' || type === 'superseded_by') {
|
|
102
137
|
return direction === 'out' ? 'text-orange-700 bg-orange-50' : 'text-red-700 bg-red-50';
|
|
@@ -262,6 +297,7 @@ export function useConceptEdges(
|
|
|
262
297
|
incomingEdges,
|
|
263
298
|
edgeDisplayCache,
|
|
264
299
|
getEdgeDisplay,
|
|
300
|
+
designationFor,
|
|
265
301
|
edgeBadgeColor,
|
|
266
302
|
inverseEdgeType,
|
|
267
303
|
conceptRelated,
|
|
@@ -3,6 +3,8 @@ import { computed, watch, ref, onMounted, onUnmounted } from 'vue';
|
|
|
3
3
|
import { useRouter } from 'vue-router';
|
|
4
4
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
5
5
|
import { conceptUri } from '../adapters/model-bridge';
|
|
6
|
+
import { UriRouter } from '../adapters/UriRouter';
|
|
7
|
+
import type { PartitiveRelationWire, PartitiveMemberWire } from '../adapters/types';
|
|
6
8
|
import ConceptDetail from '../components/ConceptDetail.vue';
|
|
7
9
|
import RelationSphere from '../components/RelationSphere.vue';
|
|
8
10
|
import ShortcutsModal from '../components/ShortcutsModal.vue';
|
|
@@ -58,6 +60,55 @@ const manifest = computed(() => store.currentManifest);
|
|
|
58
60
|
const edges = computed(() => store.conceptEdges);
|
|
59
61
|
const activeRegisterId = computed(() => sphereFocusPayload.value?.registerId ?? props.registerId);
|
|
60
62
|
const adjacent = ref({ prev: null as string | null, next: null as string | null });
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Project concept.partitiveRelations (glossarist-js v2 model) into the
|
|
66
|
+
* URI-resolved wire shape the sphere consumes. Mirrors the projection
|
|
67
|
+
* in use-concept-edges.ts but inline here so the sphere can render rake
|
|
68
|
+
* bundles without a full composable setup.
|
|
69
|
+
*/
|
|
70
|
+
const partitiveRelationsForSphere = computed<PartitiveRelationWire[]>(() => {
|
|
71
|
+
const c = concept.value;
|
|
72
|
+
const m = manifest.value;
|
|
73
|
+
if (!c || !m) return [];
|
|
74
|
+
const sourceUri = conceptUri(c, activeRegisterId.value, m.uriBase);
|
|
75
|
+
const out: PartitiveRelationWire[] = [];
|
|
76
|
+
for (const rel of c.partitiveRelations ?? []) {
|
|
77
|
+
const compRef = rel.comprehensive;
|
|
78
|
+
const compTarget = store.manifests.get(activeRegisterId.value)
|
|
79
|
+
? UriRouter.buildConceptUri(m.uriBase, activeRegisterId.value, compRef.id ?? '')
|
|
80
|
+
: null;
|
|
81
|
+
if (!compRef.id || !compTarget) continue;
|
|
82
|
+
const partitives: PartitiveMemberWire[] = rel.partitives
|
|
83
|
+
.map(member => {
|
|
84
|
+
const ref = member.ref;
|
|
85
|
+
if (!ref?.id) return null;
|
|
86
|
+
const targetRegister = store.manifests.has(activeRegisterId.value)
|
|
87
|
+
? activeRegisterId.value
|
|
88
|
+
: activeRegisterId.value;
|
|
89
|
+
const uri = UriRouter.buildConceptUri(m.uriBase, targetRegister, ref.id);
|
|
90
|
+
if (uri === sourceUri) return null;
|
|
91
|
+
return { uri, certainty: member.certainty };
|
|
92
|
+
})
|
|
93
|
+
.filter((x): x is PartitiveMemberWire => x !== null);
|
|
94
|
+
if (partitives.length === 0) continue;
|
|
95
|
+
out.push({
|
|
96
|
+
source: sourceUri,
|
|
97
|
+
comprehensive: compTarget,
|
|
98
|
+
partitives,
|
|
99
|
+
completeness: rel.completeness,
|
|
100
|
+
plurality: rel.plurality
|
|
101
|
+
? {
|
|
102
|
+
isShared: rel.plurality.isShared,
|
|
103
|
+
isUncertain: rel.plurality.isUncertain,
|
|
104
|
+
sharedType: null,
|
|
105
|
+
}
|
|
106
|
+
: null,
|
|
107
|
+
register: activeRegisterId.value,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
});
|
|
61
112
|
const viewMode = ref<'detail' | 'sphere'>(
|
|
62
113
|
router.currentRoute.value.query.view === 'sphere' ? 'sphere' : 'detail'
|
|
63
114
|
);
|
|
@@ -280,6 +331,7 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown));
|
|
|
280
331
|
:manifest="manifest"
|
|
281
332
|
:register-id="activeRegisterId"
|
|
282
333
|
:edges="edges"
|
|
334
|
+
:partitive-relations="partitiveRelationsForSphere"
|
|
283
335
|
@navigate="onSphereNavigate"
|
|
284
336
|
/>
|
|
285
337
|
<!-- Detail mode -->
|