@pgcorp/ui-kit 0.1.0
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/LICENSE +21 -0
- package/README.md +157 -0
- package/docs/accessibility.md +27 -0
- package/docs/getting-started.md +80 -0
- package/docs/licensing.md +30 -0
- package/docs/public-api.md +39 -0
- package/docs/security.md +21 -0
- package/docs/theming.md +35 -0
- package/docs/versioning.md +23 -0
- package/index.ts +3 -0
- package/package.json +592 -0
- package/src/components/icons/_internal/generatedSputnigUiIconMarkup.ts +971 -0
- package/src/components/icons/sputnigUiIcons.ts +530 -0
- package/src/components/layout/SAppShell.vue +232 -0
- package/src/components/layout/SDockRegion.vue +200 -0
- package/src/components/layout/SResizeHandle.vue +251 -0
- package/src/components/layout/SSideMenuButton.vue +104 -0
- package/src/components/layout/SSideMenuRail.vue +47 -0
- package/src/components/layout/SStack.css +25 -0
- package/src/components/layout/SStack.vue +57 -0
- package/src/components/layout/SStatusBarShell.css +15 -0
- package/src/components/layout/SStatusBarShell.vue +26 -0
- package/src/components/layout/SWorkbenchLayout.vue +53 -0
- package/src/components/layout/SWorkbenchPanelShell.css +161 -0
- package/src/components/layout/SWorkbenchPanelShell.vue +286 -0
- package/src/components/layout/_internal/SAppBrand.css +45 -0
- package/src/components/layout/_internal/SAppBrand.vue +37 -0
- package/src/components/layout/_internal/SAppBrandContent.vue +30 -0
- package/src/components/layout/types.ts +28 -0
- package/src/components/shared/_internal/SDialogSurface.css +127 -0
- package/src/components/shared/_internal/SDialogSurface.vue +117 -0
- package/src/components/shared/_internal/SDisclosureSummary.css +92 -0
- package/src/components/shared/_internal/SDisclosureSummary.vue +121 -0
- package/src/components/shared/_internal/SListboxGroup.css +18 -0
- package/src/components/shared/_internal/SListboxGroup.vue +24 -0
- package/src/components/shared/_internal/SMenuItem.css +112 -0
- package/src/components/shared/_internal/SMenuItem.vue +213 -0
- package/src/components/shared/_internal/SMenuLayer.css +21 -0
- package/src/components/shared/_internal/SMenuLayer.vue +48 -0
- package/src/components/shared/_internal/SMenuSeparator.css +5 -0
- package/src/components/shared/_internal/SMenuSeparator.vue +13 -0
- package/src/components/shared/_internal/SToastItem.css +65 -0
- package/src/components/shared/_internal/SToastItem.vue +58 -0
- package/src/components/shared/_internal/STreeNode.vue +761 -0
- package/src/components/shared/_internal/dialogSurface.ts +105 -0
- package/src/components/shared/_internal/focusTrap.ts +40 -0
- package/src/components/shared/_internal/listboxContext.ts +16 -0
- package/src/components/shared/_internal/markerContract.ts +10 -0
- package/src/components/shared/_internal/menuContext.ts +23 -0
- package/src/components/shared/_internal/selectionContract.ts +239 -0
- package/src/components/shared/_internal/sidebarGroupContext.ts +31 -0
- package/src/components/shared/_internal/tabsContext.ts +57 -0
- package/src/components/shared/_internal/treeRuntime.ts +203 -0
- package/src/components/shared/_internal/useSelectionRoving.ts +162 -0
- package/src/components/shared/codeLanguages.ts +168 -0
- package/src/components/shared/complex/SKanbanCard.css +8 -0
- package/src/components/shared/complex/SKanbanCard.vue +76 -0
- package/src/components/shared/complex/SKanbanLane.vue +82 -0
- package/src/components/shared/complex/STree.css +24 -0
- package/src/components/shared/complex/STree.vue +288 -0
- package/src/components/shared/complex/treeAdapter.ts +210 -0
- package/src/components/shared/complex/types.ts +102 -0
- package/src/components/shared/containers/SCloudChrome.css +103 -0
- package/src/components/shared/containers/SCloudChrome.vue +87 -0
- package/src/components/shared/containers/SCloudChromeEmpty.css +10 -0
- package/src/components/shared/containers/SCloudChromeEmpty.vue +18 -0
- package/src/components/shared/containers/SCloudChromeRow.css +78 -0
- package/src/components/shared/containers/SCloudChromeRow.vue +108 -0
- package/src/components/shared/containers/SCollapsiblePanel.css +57 -0
- package/src/components/shared/containers/SCollapsiblePanel.vue +360 -0
- package/src/components/shared/containers/SDrawer.css +60 -0
- package/src/components/shared/containers/SDrawer.vue +86 -0
- package/src/components/shared/containers/SFormGrid.css +26 -0
- package/src/components/shared/containers/SFormGrid.vue +46 -0
- package/src/components/shared/containers/SFormGridItem.css +18 -0
- package/src/components/shared/containers/SFormGridItem.vue +29 -0
- package/src/components/shared/containers/SLeftSidebar.vue +74 -0
- package/src/components/shared/containers/SModal.css +27 -0
- package/src/components/shared/containers/SModal.vue +105 -0
- package/src/components/shared/containers/SPageHeader.css +66 -0
- package/src/components/shared/containers/SPageHeader.vue +45 -0
- package/src/components/shared/containers/SPanel.css +134 -0
- package/src/components/shared/containers/SPanel.vue +169 -0
- package/src/components/shared/containers/SPopover.css +35 -0
- package/src/components/shared/containers/SPopover.vue +551 -0
- package/src/components/shared/containers/SSidebarGroup.css +12 -0
- package/src/components/shared/containers/SSidebarGroup.vue +443 -0
- package/src/components/shared/containers/SSidebarSection.css +41 -0
- package/src/components/shared/containers/SSidebarSection.vue +268 -0
- package/src/components/shared/containers/disclosureSummary.ts +48 -0
- package/src/components/shared/containers/popover.ts +72 -0
- package/src/components/shared/containers/sidebar.ts +206 -0
- package/src/components/shared/controls/SButton.css +274 -0
- package/src/components/shared/controls/SButton.vue +211 -0
- package/src/components/shared/controls/SCheckbox.css +65 -0
- package/src/components/shared/controls/SCheckbox.vue +130 -0
- package/src/components/shared/controls/SCheckboxGroup.vue +134 -0
- package/src/components/shared/controls/SChoiceCards.css +29 -0
- package/src/components/shared/controls/SChoiceCards.vue +145 -0
- package/src/components/shared/controls/SColorSelect.vue +146 -0
- package/src/components/shared/controls/SComboboxTrigger.css +35 -0
- package/src/components/shared/controls/SComboboxTrigger.vue +122 -0
- package/src/components/shared/controls/SContextMenu.css +11 -0
- package/src/components/shared/controls/SContextMenu.vue +186 -0
- package/src/components/shared/controls/SContextToggleButton.vue +64 -0
- package/src/components/shared/controls/SDragHandle.vue +100 -0
- package/src/components/shared/controls/SDropdownMenu.css +5 -0
- package/src/components/shared/controls/SDropdownMenu.vue +188 -0
- package/src/components/shared/controls/SField.css +79 -0
- package/src/components/shared/controls/SField.vue +89 -0
- package/src/components/shared/controls/SFieldGroup.css +58 -0
- package/src/components/shared/controls/SFieldGroup.vue +117 -0
- package/src/components/shared/controls/SFieldLabel.css +12 -0
- package/src/components/shared/controls/SFieldLabel.vue +51 -0
- package/src/components/shared/controls/SInlineTokenEditor.css +61 -0
- package/src/components/shared/controls/SInlineTokenEditor.vue +1081 -0
- package/src/components/shared/controls/SInputText.css +66 -0
- package/src/components/shared/controls/SInputText.vue +321 -0
- package/src/components/shared/controls/SInteractiveSurface.css +72 -0
- package/src/components/shared/controls/SInteractiveSurface.vue +326 -0
- package/src/components/shared/controls/SLink.css +114 -0
- package/src/components/shared/controls/SLink.ts +85 -0
- package/src/components/shared/controls/SLink.vue +114 -0
- package/src/components/shared/controls/SListbox.css +31 -0
- package/src/components/shared/controls/SListbox.vue +388 -0
- package/src/components/shared/controls/SListboxOption.css +63 -0
- package/src/components/shared/controls/SListboxOption.vue +85 -0
- package/src/components/shared/controls/SMarker.css +19 -0
- package/src/components/shared/controls/SMarker.vue +33 -0
- package/src/components/shared/controls/SMenuSurface.css +20 -0
- package/src/components/shared/controls/SMenuSurface.vue +333 -0
- package/src/components/shared/controls/SSegmentedControl.css +38 -0
- package/src/components/shared/controls/SSegmentedControl.vue +134 -0
- package/src/components/shared/controls/SSelect.css +12 -0
- package/src/components/shared/controls/SSelect.vue +338 -0
- package/src/components/shared/controls/STextarea.css +31 -0
- package/src/components/shared/controls/STextarea.vue +316 -0
- package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +224 -0
- package/src/components/shared/controls/menu.ts +178 -0
- package/src/components/shared/data-display/SActionCard.css +6 -0
- package/src/components/shared/data-display/SActionCard.vue +49 -0
- package/src/components/shared/data-display/SActionList.css +15 -0
- package/src/components/shared/data-display/SActionList.vue +34 -0
- package/src/components/shared/data-display/SActionListItem.css +114 -0
- package/src/components/shared/data-display/SActionListItem.vue +230 -0
- package/src/components/shared/data-display/SBadge.css +73 -0
- package/src/components/shared/data-display/SBadge.vue +52 -0
- package/src/components/shared/data-display/SCodeBlock.css +56 -0
- package/src/components/shared/data-display/SCodeBlock.vue +188 -0
- package/src/components/shared/data-display/SCodeEditor.css +178 -0
- package/src/components/shared/data-display/SCodeEditor.vue +274 -0
- package/src/components/shared/data-display/SCodeSearchPanel.vue +327 -0
- package/src/components/shared/data-display/SCopyField.vue +77 -0
- package/src/components/shared/data-display/SDocBlock.css +136 -0
- package/src/components/shared/data-display/SDocBlock.vue +358 -0
- package/src/components/shared/data-display/SEmptyState.css +42 -0
- package/src/components/shared/data-display/SEmptyState.vue +79 -0
- package/src/components/shared/data-display/SExpandableText.vue +314 -0
- package/src/components/shared/data-display/SJsonTree.css +6 -0
- package/src/components/shared/data-display/SJsonTree.vue +111 -0
- package/src/components/shared/data-display/SKeyValueGrid.css +37 -0
- package/src/components/shared/data-display/SKeyValueGrid.vue +277 -0
- package/src/components/shared/data-display/SLinkedSystemsList.vue +270 -0
- package/src/components/shared/data-display/SMessage.css +88 -0
- package/src/components/shared/data-display/SMessage.vue +152 -0
- package/src/components/shared/data-display/SMetricCard.css +32 -0
- package/src/components/shared/data-display/SMetricCard.vue +117 -0
- package/src/components/shared/data-display/SProgressBar.css +84 -0
- package/src/components/shared/data-display/SProgressBar.vue +114 -0
- package/src/components/shared/data-display/SProgressIndicator.css +59 -0
- package/src/components/shared/data-display/SProgressIndicator.vue +104 -0
- package/src/components/shared/data-display/SSectionHeader.css +84 -0
- package/src/components/shared/data-display/SSectionHeader.vue +58 -0
- package/src/components/shared/data-display/SStatus.css +90 -0
- package/src/components/shared/data-display/SStatus.vue +79 -0
- package/src/components/shared/data-display/STable.css +83 -0
- package/src/components/shared/data-display/STable.vue +339 -0
- package/src/components/shared/data-display/SToastContainer.css +53 -0
- package/src/components/shared/data-display/SToastContainer.vue +31 -0
- package/src/components/shared/data-display/STooltip.css +114 -0
- package/src/components/shared/data-display/STooltip.vue +550 -0
- package/src/components/shared/data-display/STooltipAnchor.css +21 -0
- package/src/components/shared/data-display/STooltipAnchor.vue +119 -0
- package/src/components/shared/data-display/SVirtualList.css +24 -0
- package/src/components/shared/data-display/SVirtualList.ts +19 -0
- package/src/components/shared/data-display/SVirtualList.vue +361 -0
- package/src/components/shared/data-display/json.ts +136 -0
- package/src/components/shared/data-display/table.ts +76 -0
- package/src/components/shared/database/SDataGrid.css +3 -0
- package/src/components/shared/database/SDataGrid.vue +464 -0
- package/src/components/shared/database/SSqlEditor.vue +363 -0
- package/src/components/shared/database/dataGrid.ts +61 -0
- package/src/components/shared/feedback/SAsyncState.css +26 -0
- package/src/components/shared/feedback/SAsyncState.vue +190 -0
- package/src/components/shared/graph/SGraphViewport.ts +1 -0
- package/src/components/shared/graph/SGraphViewport.vue +858 -0
- package/src/components/shared/graph/_internal/graphViewportContract.ts +362 -0
- package/src/components/shared/markdown.ts +169 -0
- package/src/components/shared/navigation/SBottomNav.vue +692 -0
- package/src/components/shared/navigation/SBreadcrumbs.css +26 -0
- package/src/components/shared/navigation/SBreadcrumbs.vue +155 -0
- package/src/components/shared/navigation/SCatalogNavigator.css +100 -0
- package/src/components/shared/navigation/SCatalogNavigator.vue +308 -0
- package/src/components/shared/navigation/STab.vue +363 -0
- package/src/components/shared/navigation/STabList.vue +442 -0
- package/src/components/shared/navigation/STabPanel.vue +165 -0
- package/src/components/shared/navigation/STabPanels.vue +33 -0
- package/src/components/shared/navigation/STabs.vue +617 -0
- package/src/components/shared/navigation/SWizardSteps.css +69 -0
- package/src/components/shared/navigation/SWizardSteps.vue +171 -0
- package/src/components/shared/navigation/_internal/SCatalogNavigatorGroup.vue +63 -0
- package/src/components/shared/persona/SPersonaProjectPicker.css +36 -0
- package/src/components/shared/persona/SPersonaProjectPicker.vue +361 -0
- package/src/components/shared/persona/SPersonaRunWorkbench.css +109 -0
- package/src/components/shared/persona/SPersonaRunWorkbench.vue +549 -0
- package/src/components/shared/persona/_internal/SPersonaRunConsole.css +40 -0
- package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +298 -0
- package/src/components/shared/persona/_internal/inventory.ts +110 -0
- package/src/components/shared/persona/_internal/labels.ts +79 -0
- package/src/components/shared/persona/presentation.ts +371 -0
- package/src/components/shared/persona/types.ts +256 -0
- package/src/composables/uiPreferencesReset.ts +52 -0
- package/src/composables/useClipboard.ts +37 -0
- package/src/composables/useDialogOverlay.ts +85 -0
- package/src/composables/useDisclosure.ts +40 -0
- package/src/composables/useFloatingPosition.ts +245 -0
- package/src/composables/useNotifier.ts +26 -0
- package/src/composables/useSidebarPanelState.ts +610 -0
- package/src/internal/codeEditorContract.ts +110 -0
- package/src/internal/codeEditorExtensions.ts +27 -0
- package/src/internal/feedbackPresentation.ts +136 -0
- package/src/internal/fieldSurface.ts +59 -0
- package/src/internal/focusNavigation.ts +69 -0
- package/src/internal/inlineTokenEditorContract.ts +136 -0
- package/src/internal/interactiveElement.ts +33 -0
- package/src/internal/layerStack.ts +202 -0
- package/src/internal/linkTarget.ts +256 -0
- package/src/internal/ownedAttrs.ts +464 -0
- package/src/internal/passiveContentContract.ts +129 -0
- package/src/internal/pointerInteractionLease.ts +81 -0
- package/src/internal/runtimeContract.ts +194 -0
- package/src/internal/runtimeTheme.ts +159 -0
- package/src/internal/semanticSizing.ts +60 -0
- package/src/internal/useMenuLayer.ts +129 -0
- package/src/stores/useNotifierStore.ts +69 -0
- package/src/styles/reference.css +4 -0
- package/src/styles/style.css +151 -0
- package/src/styles/tokens.css +836 -0
- package/src/theme.ts +449 -0
- package/src/types/highlight-js-lib.d.ts +76 -0
- package/src/variants.ts +15 -0
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
class="s-graph-viewport"
|
|
5
|
+
:data-render-state="renderState"
|
|
6
|
+
:data-positioning="resolvedPositioningMode"
|
|
7
|
+
>
|
|
8
|
+
<div ref="rendererRef" class="s-graph-viewport__renderer" aria-hidden="true" />
|
|
9
|
+
<div v-if="renderError" class="s-graph-viewport__error">
|
|
10
|
+
<SAsyncState
|
|
11
|
+
:state="{ status: 'error', message: renderError.message }"
|
|
12
|
+
presentation="block"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
<div
|
|
16
|
+
class="s-graph-viewport__node-inventory"
|
|
17
|
+
:class="{ 's-graph-viewport__node-inventory--fallback': renderError !== null }"
|
|
18
|
+
>
|
|
19
|
+
<SListbox
|
|
20
|
+
:label="resolvedNodeListLabel"
|
|
21
|
+
:model-value="selectedNodeId"
|
|
22
|
+
:options="nodeListOptions"
|
|
23
|
+
:empty-label="resolvedNodeListEmptyLabel"
|
|
24
|
+
@update:model-value="handleInventorySelection"
|
|
25
|
+
@option-keydown="handleInventoryOptionKeydown"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup lang="ts">
|
|
32
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
33
|
+
import Graph from 'graphology'
|
|
34
|
+
import Sigma from 'sigma'
|
|
35
|
+
import type { Settings } from 'sigma/settings'
|
|
36
|
+
import type {
|
|
37
|
+
SGraphEdge,
|
|
38
|
+
SGraphNode,
|
|
39
|
+
SGraphNodeSizeMode,
|
|
40
|
+
SGraphNodePointerEvent,
|
|
41
|
+
SGraphPosition,
|
|
42
|
+
SGraphPositionChange,
|
|
43
|
+
SGraphRenderError,
|
|
44
|
+
SGraphRenderErrorReason,
|
|
45
|
+
SGraphRenderMetric,
|
|
46
|
+
SGraphSelection,
|
|
47
|
+
SGraphTone,
|
|
48
|
+
SGraphViewportGraph,
|
|
49
|
+
SGraphViewportInteraction,
|
|
50
|
+
} from './_internal/graphViewportContract'
|
|
51
|
+
import {
|
|
52
|
+
assertSGraphViewportGraph,
|
|
53
|
+
assertSGraphViewportInteraction,
|
|
54
|
+
SGraphContractError,
|
|
55
|
+
} from './_internal/graphViewportContract'
|
|
56
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
57
|
+
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
58
|
+
import { useRuntimeTheme } from '../../../internal/runtimeTheme'
|
|
59
|
+
import SListbox, { type SListboxOptionKeydownPayload } from '../controls/SListbox.vue'
|
|
60
|
+
import SAsyncState from '../feedback/SAsyncState.vue'
|
|
61
|
+
|
|
62
|
+
defineOptions({ inheritAttrs: false })
|
|
63
|
+
|
|
64
|
+
type SGraphAttributes = Record<string, unknown>
|
|
65
|
+
|
|
66
|
+
type GraphNodeAttributes = SGraphAttributes & SGraphNode & {
|
|
67
|
+
color: string
|
|
68
|
+
borderColor: string
|
|
69
|
+
labelColor: string
|
|
70
|
+
highlighted: boolean
|
|
71
|
+
hidden: boolean
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type GraphEdgeAttributes = SGraphAttributes & SGraphEdge & {
|
|
75
|
+
color: string
|
|
76
|
+
hidden: boolean
|
|
77
|
+
size: number
|
|
78
|
+
opacity: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const props = withDefaults(defineProps<{
|
|
82
|
+
graph: SGraphViewportGraph
|
|
83
|
+
interaction: SGraphViewportInteraction
|
|
84
|
+
nodeListLabel: string
|
|
85
|
+
nodeListEmptyLabel: string
|
|
86
|
+
showEdgeLabels?: boolean
|
|
87
|
+
labelZoomThreshold?: number
|
|
88
|
+
nodeSizeMode?: SGraphNodeSizeMode
|
|
89
|
+
}>(), {
|
|
90
|
+
showEdgeLabels: false,
|
|
91
|
+
labelZoomThreshold: 0.76,
|
|
92
|
+
nodeSizeMode: 'screen',
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const emit = defineEmits<{
|
|
96
|
+
'hover-node': [event: SGraphNodePointerEvent]
|
|
97
|
+
'leave-node': []
|
|
98
|
+
'selection-change': [selection: SGraphSelection]
|
|
99
|
+
'activate-node': [nodeId: string]
|
|
100
|
+
'positions-change': [changes: SGraphPositionChange[]]
|
|
101
|
+
'render-metric': [metric: SGraphRenderMetric]
|
|
102
|
+
'render-error': [error: SGraphRenderError]
|
|
103
|
+
}>()
|
|
104
|
+
|
|
105
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SGraphViewport', owner: 'graph viewport root' })
|
|
106
|
+
useInteractiveLeafRegistration({ owner: 'SGraphViewport' })
|
|
107
|
+
const rendererRef = ref<HTMLElement | null>(null)
|
|
108
|
+
const runtimeTheme = useRuntimeTheme()
|
|
109
|
+
const renderState = ref<'idle' | 'painted' | 'failed'>('idle')
|
|
110
|
+
const renderError = ref<SGraphRenderError | null>(null)
|
|
111
|
+
let sigma: Sigma<GraphNodeAttributes, GraphEdgeAttributes> | null = null
|
|
112
|
+
let renderedGraph: Graph<GraphNodeAttributes, GraphEdgeAttributes> | null = null
|
|
113
|
+
let resizeObserver: ResizeObserver | null = null
|
|
114
|
+
let draggedNodeId: string | null = null
|
|
115
|
+
let draggedNodeStartPosition: SGraphPosition | null = null
|
|
116
|
+
let draggedGroupStartPositions = new Map<string, SGraphPosition>()
|
|
117
|
+
let didDrag = false
|
|
118
|
+
|
|
119
|
+
const SIGMA_INITIAL_CAMERA_RATIO = 1
|
|
120
|
+
|
|
121
|
+
function assertViewportConfiguration(): void {
|
|
122
|
+
assertSGraphViewportGraph(props.graph)
|
|
123
|
+
assertSGraphViewportInteraction(props.interaction, props.graph)
|
|
124
|
+
if (typeof props.nodeListLabel !== 'string' || props.nodeListLabel.trim().length === 0) {
|
|
125
|
+
throw new SGraphContractError('SGraphViewport: nodeListLabel должен быть непустой строкой.')
|
|
126
|
+
}
|
|
127
|
+
if (typeof props.nodeListEmptyLabel !== 'string' || props.nodeListEmptyLabel.trim().length === 0) {
|
|
128
|
+
throw new SGraphContractError('SGraphViewport: nodeListEmptyLabel должен быть непустой строкой.')
|
|
129
|
+
}
|
|
130
|
+
if (!Number.isFinite(props.labelZoomThreshold) || props.labelZoomThreshold <= 0) {
|
|
131
|
+
throw new SGraphContractError('SGraphViewport: labelZoomThreshold должен быть конечным числом больше 0.')
|
|
132
|
+
}
|
|
133
|
+
if (!['screen', 'graph'].includes(props.nodeSizeMode)) {
|
|
134
|
+
throw new SGraphContractError(`SGraphViewport: неизвестный nodeSizeMode "${String(props.nodeSizeMode)}".`)
|
|
135
|
+
}
|
|
136
|
+
if (typeof props.showEdgeLabels !== 'boolean') {
|
|
137
|
+
throw new SGraphContractError('SGraphViewport: showEdgeLabels должен быть boolean.')
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const interactionContract = computed(() => {
|
|
142
|
+
assertViewportConfiguration()
|
|
143
|
+
return props.interaction
|
|
144
|
+
})
|
|
145
|
+
const resolvedNodeListLabel = computed(() => {
|
|
146
|
+
assertViewportConfiguration()
|
|
147
|
+
return props.nodeListLabel
|
|
148
|
+
})
|
|
149
|
+
const resolvedNodeListEmptyLabel = computed(() => {
|
|
150
|
+
assertViewportConfiguration()
|
|
151
|
+
return props.nodeListEmptyLabel
|
|
152
|
+
})
|
|
153
|
+
const resolvedPositioningMode = computed(() => interactionContract.value.positioning.mode)
|
|
154
|
+
const selectedNodeId = computed(() => (
|
|
155
|
+
interactionContract.value.selection.mode === 'single'
|
|
156
|
+
? interactionContract.value.selection.nodeId
|
|
157
|
+
: null
|
|
158
|
+
))
|
|
159
|
+
const selectedNodeSet = computed(() => new Set(selectedNodeId.value === null ? [] : [selectedNodeId.value]))
|
|
160
|
+
const nodeListOptions = computed(() => {
|
|
161
|
+
assertViewportConfiguration()
|
|
162
|
+
return props.graph.nodes.map((node) => ({ value: node.id, label: node.label }))
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
function requiredRuntimeToken(name: string): string {
|
|
166
|
+
const element = rendererRef.value
|
|
167
|
+
if (!element) {
|
|
168
|
+
throw new Error(`SGraphViewport: невозможно прочитать semantic token ${name} до монтирования viewport.`)
|
|
169
|
+
}
|
|
170
|
+
const value = getComputedStyle(element).getPropertyValue(name).trim()
|
|
171
|
+
if (!value) {
|
|
172
|
+
throw new Error(`SGraphViewport: обязательный semantic token ${name} отсутствует в computed style.`)
|
|
173
|
+
}
|
|
174
|
+
return value
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function requiredRuntimeNumber(name: string): number {
|
|
178
|
+
const raw = requiredRuntimeToken(name)
|
|
179
|
+
const value = Number(raw)
|
|
180
|
+
if (!Number.isFinite(value)) {
|
|
181
|
+
throw new Error(`SGraphViewport: semantic token ${name} должен быть конечным unitless number, получено '${raw}'.`)
|
|
182
|
+
}
|
|
183
|
+
return value
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function requiredRuntimePixels(name: string): number {
|
|
187
|
+
const raw = requiredRuntimeToken(name)
|
|
188
|
+
const match = raw.match(/^(-?(?:\d+(?:\.\d+)?|\.\d+))px$/u)
|
|
189
|
+
if (!match) {
|
|
190
|
+
throw new Error(`SGraphViewport: semantic token ${name} должен быть вычислен в px, получено '${raw}'.`)
|
|
191
|
+
}
|
|
192
|
+
return Number(match[1])
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function requiredRuntimeDuration(name: string): number {
|
|
196
|
+
const raw = requiredRuntimeToken(name)
|
|
197
|
+
const match = raw.match(/^((?:\d+(?:\.\d+)?|\.\d+))(ms|s)$/u)
|
|
198
|
+
if (!match) {
|
|
199
|
+
throw new Error(`SGraphViewport: semantic token ${name} должен быть вычислен в ms или s, получено '${raw}'.`)
|
|
200
|
+
}
|
|
201
|
+
const value = Number(match[1])
|
|
202
|
+
return match[2] === 's' ? value * 1000 : value
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function requiredRuntimeHexColor(name: string): string {
|
|
206
|
+
const value = requiredRuntimeToken(name)
|
|
207
|
+
if (!/^#[\da-f]{6}$/iu.test(value)) {
|
|
208
|
+
throw new Error(`SGraphViewport: semantic token ${name} должен быть вычислен в #RRGGBB, получено '${value}'.`)
|
|
209
|
+
}
|
|
210
|
+
return value
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function resolveNodeTone(tone: SGraphTone): Pick<GraphNodeAttributes, 'color' | 'borderColor' | 'labelColor'> {
|
|
214
|
+
return {
|
|
215
|
+
color: requiredRuntimeHexColor(`--s-graph-node-tone-${tone}-fill`),
|
|
216
|
+
borderColor: requiredRuntimeHexColor(`--s-graph-node-tone-${tone}-border`),
|
|
217
|
+
labelColor: requiredRuntimeHexColor(`--s-graph-node-tone-${tone}-label`),
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resolveEdgeTone(tone: SGraphTone): string {
|
|
222
|
+
return requiredRuntimeHexColor(`--s-graph-edge-tone-${tone}`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function colorWithOpacity(color: string, opacity: number | undefined): string {
|
|
226
|
+
if (opacity === undefined || opacity === 1) return color
|
|
227
|
+
const red = Number.parseInt(color.slice(1, 3), 16)
|
|
228
|
+
const green = Number.parseInt(color.slice(3, 5), 16)
|
|
229
|
+
const blue = Number.parseInt(color.slice(5, 7), 16)
|
|
230
|
+
return `rgba(${red}, ${green}, ${blue}, ${opacity})`
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildGraph(source: SGraphViewportGraph): Graph<GraphNodeAttributes, GraphEdgeAttributes> {
|
|
234
|
+
assertSGraphViewportGraph(source)
|
|
235
|
+
const result = new Graph<GraphNodeAttributes, GraphEdgeAttributes>({ multi: true, type: 'mixed' })
|
|
236
|
+
for (const node of source.nodes) {
|
|
237
|
+
result.addNode(node.id, {
|
|
238
|
+
...node,
|
|
239
|
+
...resolveNodeTone(node.tone),
|
|
240
|
+
highlighted: false,
|
|
241
|
+
hidden: false,
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
for (const edge of source.edges) {
|
|
245
|
+
result.addEdgeWithKey(edge.id, edge.source, edge.target, {
|
|
246
|
+
...edge,
|
|
247
|
+
color: resolveEdgeTone(edge.tone),
|
|
248
|
+
hidden: edge.hidden ?? false,
|
|
249
|
+
size: edge.size ?? 1,
|
|
250
|
+
opacity: edge.opacity ?? 1,
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
return result
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function refreshGraphToneAttributes(): void {
|
|
257
|
+
if (!renderedGraph) return
|
|
258
|
+
for (const node of props.graph.nodes) {
|
|
259
|
+
if (!renderedGraph.hasNode(node.id)) {
|
|
260
|
+
throw new Error(`SGraphViewport: node "${node.id}" отсутствует при обновлении runtime theme.`)
|
|
261
|
+
}
|
|
262
|
+
const tone = resolveNodeTone(node.tone)
|
|
263
|
+
renderedGraph.setNodeAttribute(node.id, 'color', tone.color)
|
|
264
|
+
renderedGraph.setNodeAttribute(node.id, 'borderColor', tone.borderColor)
|
|
265
|
+
renderedGraph.setNodeAttribute(node.id, 'labelColor', tone.labelColor)
|
|
266
|
+
}
|
|
267
|
+
for (const edge of props.graph.edges) {
|
|
268
|
+
if (!renderedGraph.hasEdge(edge.id)) {
|
|
269
|
+
throw new Error(`SGraphViewport: edge "${edge.id}" отсутствует при обновлении runtime theme.`)
|
|
270
|
+
}
|
|
271
|
+
renderedGraph.setEdgeAttribute(edge.id, 'color', resolveEdgeTone(edge.tone))
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function graphSettings(): Partial<Settings<GraphNodeAttributes, GraphEdgeAttributes>> {
|
|
276
|
+
return {
|
|
277
|
+
renderLabels: true,
|
|
278
|
+
renderEdgeLabels: props.showEdgeLabels,
|
|
279
|
+
hideEdgesOnMove: true,
|
|
280
|
+
hideLabelsOnMove: true,
|
|
281
|
+
enableEdgeEvents: false,
|
|
282
|
+
defaultNodeColor: requiredRuntimeHexColor('--s-graph-node-default-color'),
|
|
283
|
+
defaultEdgeColor: requiredRuntimeHexColor('--s-graph-edge-default-color'),
|
|
284
|
+
labelColor: { attribute: 'labelColor', color: requiredRuntimeHexColor('--s-graph-label-color') },
|
|
285
|
+
labelFont: requiredRuntimeToken('--s-graph-label-font-family'),
|
|
286
|
+
labelSize: requiredRuntimePixels('--s-graph-label-font-size'),
|
|
287
|
+
labelWeight: requiredRuntimeToken('--s-graph-label-font-weight'),
|
|
288
|
+
labelDensity: requiredRuntimeNumber('--s-graph-label-density'),
|
|
289
|
+
labelGridCellSize: requiredRuntimePixels('--s-graph-label-grid-cell-size'),
|
|
290
|
+
labelRenderedSizeThreshold: requiredRuntimePixels('--s-graph-label-rendered-size-threshold'),
|
|
291
|
+
minEdgeThickness: requiredRuntimePixels('--s-graph-min-edge-thickness'),
|
|
292
|
+
stagePadding: requiredRuntimePixels('--s-graph-stage-padding'),
|
|
293
|
+
zIndex: true,
|
|
294
|
+
...(props.nodeSizeMode === 'screen'
|
|
295
|
+
? {
|
|
296
|
+
zoomToSizeRatioFunction: (ratio: number) =>
|
|
297
|
+
Math.max(0.08, Math.min(0.72, Math.sqrt(Math.max(0.0001, ratio)) * 0.78)),
|
|
298
|
+
}
|
|
299
|
+
: {}),
|
|
300
|
+
nodeReducer: (nodeId, data) => {
|
|
301
|
+
const isSelected = selectedNodeSet.value.has(nodeId)
|
|
302
|
+
const forceLabel = Boolean(data.forceLabel) || isSelected || data.highlighted
|
|
303
|
+
// Sigma синхронно вызывает reducer внутри конструктора до присваивания
|
|
304
|
+
// готового instance. Камера Sigma стартует с ratio 1; следующие проходы
|
|
305
|
+
// reducer читают состояние уже смонтированной камеры.
|
|
306
|
+
const zoom = sigma?.getCamera().getState().ratio ?? SIGMA_INITIAL_CAMERA_RATIO
|
|
307
|
+
const label = forceLabel || zoom < props.labelZoomThreshold ? data.label : ''
|
|
308
|
+
return {
|
|
309
|
+
x: data.x,
|
|
310
|
+
y: data.y,
|
|
311
|
+
label,
|
|
312
|
+
color: isSelected ? data.borderColor : data.color,
|
|
313
|
+
size: isSelected ? Math.min(data.size * 1.16, data.size + 1.4) : data.size,
|
|
314
|
+
forceLabel,
|
|
315
|
+
highlighted: data.highlighted || isSelected,
|
|
316
|
+
zIndex: data.role === 'group' || data.role === 'coverage' ? 4 : isSelected ? 5 : 1,
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
edgeReducer: (_edgeId, data) => {
|
|
320
|
+
if (!renderedGraph) throw new Error('SGraphViewport: edge reducer вызван без rendered graph.')
|
|
321
|
+
if (!renderedGraph.hasNode(data.source) || !renderedGraph.hasNode(data.target)) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`SGraphViewport: edge "${data.id}" ссылается на отсутствующий rendered node при reducer.`,
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
const selected = selectedNodeId.value
|
|
327
|
+
const relatedToSelection = selected ? data.source === selected || data.target === selected : false
|
|
328
|
+
const sourceHighlighted = Boolean(renderedGraph.getNodeAttribute(data.source, 'highlighted'))
|
|
329
|
+
const targetHighlighted = Boolean(renderedGraph.getNodeAttribute(data.target, 'highlighted'))
|
|
330
|
+
const relatedToInteraction = relatedToSelection || sourceHighlighted || targetHighlighted
|
|
331
|
+
return {
|
|
332
|
+
color: colorWithOpacity(data.color, relatedToInteraction ? Math.max(data.opacity, 0.55) : data.opacity),
|
|
333
|
+
size: relatedToInteraction ? Math.max(data.size, 1.4) : data.size,
|
|
334
|
+
hidden: data.hidden
|
|
335
|
+
|| Boolean(selected && !relatedToSelection)
|
|
336
|
+
|| Boolean(data.showOnInteractionOnly && !relatedToInteraction),
|
|
337
|
+
forceLabel: false,
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function emitRenderMetric(phase: SGraphRenderMetric['phase'], startedAt: number): void {
|
|
344
|
+
emit('render-metric', {
|
|
345
|
+
phase,
|
|
346
|
+
durationMs: performance.now() - startedAt,
|
|
347
|
+
nodes: props.graph.nodes.length,
|
|
348
|
+
edges: props.graph.edges.length,
|
|
349
|
+
renderer: 'sigma',
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function failRender(reason: SGraphRenderErrorReason, message: string): void {
|
|
354
|
+
sigma?.kill()
|
|
355
|
+
sigma = null
|
|
356
|
+
renderedGraph = null
|
|
357
|
+
renderState.value = 'failed'
|
|
358
|
+
renderError.value = {
|
|
359
|
+
reason,
|
|
360
|
+
message,
|
|
361
|
+
nodes: props.graph.nodes.length,
|
|
362
|
+
edges: props.graph.edges.length,
|
|
363
|
+
}
|
|
364
|
+
emit('render-error', renderError.value)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function clearRenderError(): void {
|
|
368
|
+
renderError.value = null
|
|
369
|
+
if (renderState.value === 'failed') renderState.value = 'idle'
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function hasRenderableContainer(): boolean {
|
|
373
|
+
const container = rendererRef.value
|
|
374
|
+
if (!container) return false
|
|
375
|
+
return container.clientWidth > 0 && container.clientHeight > 0
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function hasWebgl2Support(): boolean {
|
|
379
|
+
const canvas = document.createElement('canvas')
|
|
380
|
+
return canvas.getContext('webgl2') !== null
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function tryMountSigma(): void {
|
|
384
|
+
if (sigma || !hasRenderableContainer()) return
|
|
385
|
+
mountSigma()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function refreshRuntimeTheme(): void {
|
|
389
|
+
if (!sigma) return
|
|
390
|
+
try {
|
|
391
|
+
refreshGraphToneAttributes()
|
|
392
|
+
sigma.setSettings(graphSettings())
|
|
393
|
+
sigma.refresh()
|
|
394
|
+
} catch (error) {
|
|
395
|
+
failRender(
|
|
396
|
+
'sigma_mount_failed',
|
|
397
|
+
error instanceof Error ? error.message : 'SGraphViewport: не удалось применить semantic runtime theme.',
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function mountSigma(): void {
|
|
403
|
+
const container = rendererRef.value
|
|
404
|
+
if (!container || !hasRenderableContainer()) return
|
|
405
|
+
let nextGraph: Graph<GraphNodeAttributes, GraphEdgeAttributes>
|
|
406
|
+
try {
|
|
407
|
+
assertViewportConfiguration()
|
|
408
|
+
nextGraph = buildGraph(props.graph)
|
|
409
|
+
} catch (error) {
|
|
410
|
+
failRender(
|
|
411
|
+
error instanceof SGraphContractError ? 'invalid_graph' : 'sigma_mount_failed',
|
|
412
|
+
error instanceof Error ? error.message : 'Граф содержит некорректные данные.',
|
|
413
|
+
)
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
if (!hasWebgl2Support()) {
|
|
417
|
+
failRender('webgl_unavailable', 'Графический движок требует WebGL2.')
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
const startedAt = performance.now()
|
|
421
|
+
try {
|
|
422
|
+
renderedGraph = nextGraph
|
|
423
|
+
sigma = new Sigma<GraphNodeAttributes, GraphEdgeAttributes>(renderedGraph, container, graphSettings())
|
|
424
|
+
clearRenderError()
|
|
425
|
+
renderState.value = 'painted'
|
|
426
|
+
bindSigmaEvents()
|
|
427
|
+
emitRenderMetric('sigma_mount', startedAt)
|
|
428
|
+
} catch (error) {
|
|
429
|
+
failRender(
|
|
430
|
+
error instanceof SGraphContractError ? 'invalid_graph' : 'sigma_mount_failed',
|
|
431
|
+
error instanceof Error ? error.message : 'Графический движок не смог построить карту.',
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function assertRenderedNode(nodeId: string, operation: string): void {
|
|
437
|
+
if (!renderedGraph?.hasNode(nodeId)) {
|
|
438
|
+
throw new Error(`SGraphViewport: node "${nodeId}" отсутствует при ${operation}.`)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function requestSelection(selection: SGraphSelection): void {
|
|
443
|
+
if (selection.mode === 'single' && !props.graph.nodes.some((node) => node.id === selection.nodeId)) {
|
|
444
|
+
throw new SGraphContractError(
|
|
445
|
+
`SGraphViewport: selection-change ссылается на отсутствующий node "${selection.nodeId}".`,
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
emit('selection-change', selection)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function requestActivation(nodeId: string): void {
|
|
452
|
+
if (!props.graph.nodes.some((node) => node.id === nodeId)) {
|
|
453
|
+
throw new SGraphContractError(`SGraphViewport: activate-node ссылается на отсутствующий node "${nodeId}".`)
|
|
454
|
+
}
|
|
455
|
+
emit('activate-node', nodeId)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function bindSigmaEvents(): void {
|
|
459
|
+
if (!sigma || !renderedGraph) {
|
|
460
|
+
throw new Error('SGraphViewport: Sigma event binding требует mounted renderer и graph.')
|
|
461
|
+
}
|
|
462
|
+
sigma.on('enterNode', ({ node, event }) => {
|
|
463
|
+
assertRenderedNode(node, 'hover')
|
|
464
|
+
renderedGraph?.setNodeAttribute(node, 'highlighted', true)
|
|
465
|
+
sigma?.refresh()
|
|
466
|
+
emit('hover-node', { nodeId: node, pointer: { x: event.x, y: event.y } })
|
|
467
|
+
})
|
|
468
|
+
sigma.on('leaveNode', ({ node }) => {
|
|
469
|
+
assertRenderedNode(node, 'leave')
|
|
470
|
+
renderedGraph?.setNodeAttribute(node, 'highlighted', false)
|
|
471
|
+
sigma?.refresh()
|
|
472
|
+
emit('leave-node')
|
|
473
|
+
})
|
|
474
|
+
sigma.on('clickNode', ({ node }) => {
|
|
475
|
+
if (!didDrag) requestSelection({ mode: 'single', nodeId: node })
|
|
476
|
+
})
|
|
477
|
+
sigma.on('clickStage', () => {
|
|
478
|
+
if (!didDrag) requestSelection({ mode: 'none' })
|
|
479
|
+
})
|
|
480
|
+
sigma.on('doubleClickNode', ({ node }) => requestActivation(node))
|
|
481
|
+
sigma.on('downNode', ({ node }) => startNodeDrag(node))
|
|
482
|
+
sigma.getMouseCaptor().on('mousemovebody', updateNodeDrag)
|
|
483
|
+
sigma.getMouseCaptor().on('mouseup', finishNodeDrag)
|
|
484
|
+
sigma.getMouseCaptor().on('mousedown', () => {
|
|
485
|
+
if (!sigma?.getCustomBBox()) sigma?.setCustomBBox(sigma.getBBox())
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function replaceGraph(): void {
|
|
490
|
+
const startedAt = performance.now()
|
|
491
|
+
let nextGraph: Graph<GraphNodeAttributes, GraphEdgeAttributes>
|
|
492
|
+
try {
|
|
493
|
+
assertViewportConfiguration()
|
|
494
|
+
nextGraph = buildGraph(props.graph)
|
|
495
|
+
} catch (error) {
|
|
496
|
+
failRender('invalid_graph', error instanceof Error ? error.message : 'Граф содержит некорректные данные.')
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
renderedGraph = nextGraph
|
|
501
|
+
if (sigma) {
|
|
502
|
+
sigma.setGraph(nextGraph)
|
|
503
|
+
sigma.setSettings(graphSettings())
|
|
504
|
+
sigma.refresh()
|
|
505
|
+
clearRenderError()
|
|
506
|
+
renderState.value = 'painted'
|
|
507
|
+
emitRenderMetric('graph_replace', startedAt)
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
tryMountSigma()
|
|
511
|
+
} catch (error) {
|
|
512
|
+
failRender(
|
|
513
|
+
error instanceof SGraphContractError ? 'invalid_graph' : 'sigma_mount_failed',
|
|
514
|
+
error instanceof Error ? error.message : 'SGraphViewport: не удалось заменить граф.',
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function refreshValidatedInteraction(): void {
|
|
520
|
+
try {
|
|
521
|
+
assertViewportConfiguration()
|
|
522
|
+
} catch (error) {
|
|
523
|
+
failRender('invalid_graph', error instanceof Error ? error.message : 'SGraphViewport: interaction некорректен.')
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
if (resolvedPositioningMode.value === 'readonly') cancelNodeDrag()
|
|
527
|
+
if (!sigma) {
|
|
528
|
+
tryMountSigma()
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
sigma.refresh()
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function refreshValidatedSettings(): void {
|
|
535
|
+
try {
|
|
536
|
+
assertViewportConfiguration()
|
|
537
|
+
} catch (error) {
|
|
538
|
+
failRender('invalid_graph', error instanceof Error ? error.message : 'SGraphViewport: renderer settings некорректны.')
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
if (!sigma) {
|
|
542
|
+
tryMountSigma()
|
|
543
|
+
return
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
sigma.setSettings(graphSettings())
|
|
547
|
+
sigma.refresh()
|
|
548
|
+
} catch (error) {
|
|
549
|
+
failRender(
|
|
550
|
+
'sigma_mount_failed',
|
|
551
|
+
error instanceof Error ? error.message : 'SGraphViewport: не удалось обновить renderer settings.',
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function isEditablePositioning(): boolean {
|
|
557
|
+
return interactionContract.value.positioning.mode === 'editable'
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function startNodeDrag(nodeId: string): void {
|
|
561
|
+
if (!isEditablePositioning()) return
|
|
562
|
+
if (!sigma || !renderedGraph) {
|
|
563
|
+
throw new Error('SGraphViewport: pointer positioning требует mounted renderer и graph.')
|
|
564
|
+
}
|
|
565
|
+
assertRenderedNode(nodeId, 'начале drag')
|
|
566
|
+
draggedNodeId = nodeId
|
|
567
|
+
draggedNodeStartPosition = {
|
|
568
|
+
x: Number(renderedGraph.getNodeAttribute(nodeId, 'x')),
|
|
569
|
+
y: Number(renderedGraph.getNodeAttribute(nodeId, 'y')),
|
|
570
|
+
}
|
|
571
|
+
const dragGroupIds = renderedGraph.getNodeAttribute(nodeId, 'dragGroupIds') ?? []
|
|
572
|
+
draggedGroupStartPositions = new Map()
|
|
573
|
+
for (const groupNodeId of [nodeId, ...dragGroupIds]) {
|
|
574
|
+
assertRenderedNode(groupNodeId, 'начале group drag')
|
|
575
|
+
draggedGroupStartPositions.set(groupNodeId, {
|
|
576
|
+
x: Number(renderedGraph.getNodeAttribute(groupNodeId, 'x')),
|
|
577
|
+
y: Number(renderedGraph.getNodeAttribute(groupNodeId, 'y')),
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
renderedGraph.setNodeAttribute(nodeId, 'highlighted', true)
|
|
581
|
+
didDrag = false
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function updateNodeDrag(event: { x: number; y: number; preventSigmaDefault: () => void; original: Event }): void {
|
|
585
|
+
if (!draggedNodeId) return
|
|
586
|
+
if (!isEditablePositioning()) {
|
|
587
|
+
cancelNodeDrag()
|
|
588
|
+
return
|
|
589
|
+
}
|
|
590
|
+
if (!sigma || !renderedGraph || !draggedNodeStartPosition) {
|
|
591
|
+
throw new Error('SGraphViewport: active drag потерял renderer, graph или start position.')
|
|
592
|
+
}
|
|
593
|
+
const position = sigma.viewportToGraph(event)
|
|
594
|
+
const delta = {
|
|
595
|
+
x: position.x - draggedNodeStartPosition.x,
|
|
596
|
+
y: position.y - draggedNodeStartPosition.y,
|
|
597
|
+
}
|
|
598
|
+
if (Math.abs(delta.x) + Math.abs(delta.y) > 0.5) didDrag = true
|
|
599
|
+
for (const [nodeId, startPosition] of draggedGroupStartPositions.entries()) {
|
|
600
|
+
assertRenderedNode(nodeId, 'обновлении drag')
|
|
601
|
+
renderedGraph.setNodeAttribute(nodeId, 'x', startPosition.x + delta.x)
|
|
602
|
+
renderedGraph.setNodeAttribute(nodeId, 'y', startPosition.y + delta.y)
|
|
603
|
+
}
|
|
604
|
+
event.preventSigmaDefault()
|
|
605
|
+
event.original.preventDefault()
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function finishNodeDrag(): void {
|
|
609
|
+
if (!draggedNodeId) {
|
|
610
|
+
didDrag = false
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
if (!isEditablePositioning()) {
|
|
614
|
+
cancelNodeDrag()
|
|
615
|
+
return
|
|
616
|
+
}
|
|
617
|
+
if (!renderedGraph) throw new Error('SGraphViewport: active drag потерял rendered graph при завершении.')
|
|
618
|
+
assertRenderedNode(draggedNodeId, 'завершении drag')
|
|
619
|
+
renderedGraph.setNodeAttribute(draggedNodeId, 'highlighted', false)
|
|
620
|
+
const changes: SGraphPositionChange[] = []
|
|
621
|
+
for (const nodeId of draggedGroupStartPositions.keys()) {
|
|
622
|
+
assertRenderedNode(nodeId, 'завершении group drag')
|
|
623
|
+
changes.push({
|
|
624
|
+
nodeId,
|
|
625
|
+
position: {
|
|
626
|
+
x: Number(renderedGraph.getNodeAttribute(nodeId, 'x')),
|
|
627
|
+
y: Number(renderedGraph.getNodeAttribute(nodeId, 'y')),
|
|
628
|
+
},
|
|
629
|
+
})
|
|
630
|
+
}
|
|
631
|
+
if (didDrag) emit('positions-change', changes)
|
|
632
|
+
draggedNodeId = null
|
|
633
|
+
draggedNodeStartPosition = null
|
|
634
|
+
draggedGroupStartPositions = new Map()
|
|
635
|
+
window.setTimeout(() => { didDrag = false }, 0)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function cancelNodeDrag(): void {
|
|
639
|
+
if (renderedGraph && draggedNodeId && renderedGraph.hasNode(draggedNodeId)) {
|
|
640
|
+
renderedGraph.setNodeAttribute(draggedNodeId, 'highlighted', false)
|
|
641
|
+
}
|
|
642
|
+
draggedNodeId = null
|
|
643
|
+
draggedNodeStartPosition = null
|
|
644
|
+
draggedGroupStartPositions = new Map()
|
|
645
|
+
didDrag = false
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function handleInventorySelection(value: unknown): void {
|
|
649
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
650
|
+
throw new SGraphContractError('SGraphViewport: SListbox вернул недопустимый node id.')
|
|
651
|
+
}
|
|
652
|
+
requestSelection({ mode: 'single', nodeId: value })
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function sourcePosition(nodeId: string): SGraphPosition {
|
|
656
|
+
if (renderedGraph?.hasNode(nodeId)) {
|
|
657
|
+
return {
|
|
658
|
+
x: Number(renderedGraph.getNodeAttribute(nodeId, 'x')),
|
|
659
|
+
y: Number(renderedGraph.getNodeAttribute(nodeId, 'y')),
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const node = props.graph.nodes.find((candidate) => candidate.id === nodeId)
|
|
663
|
+
if (!node) throw new Error(`SGraphViewport: node "${nodeId}" отсутствует при keyboard positioning.`)
|
|
664
|
+
return { x: node.x, y: node.y }
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function nudgeNode(nodeId: string, delta: SGraphPosition): void {
|
|
668
|
+
const sourceNode = props.graph.nodes.find((node) => node.id === nodeId)
|
|
669
|
+
if (!sourceNode) throw new Error(`SGraphViewport: node "${nodeId}" отсутствует при keyboard positioning.`)
|
|
670
|
+
const ids = [sourceNode.id, ...(sourceNode.dragGroupIds ?? [])]
|
|
671
|
+
const changes = ids.map((id): SGraphPositionChange => {
|
|
672
|
+
const position = sourcePosition(id)
|
|
673
|
+
const next = { x: position.x + delta.x, y: position.y + delta.y }
|
|
674
|
+
if (renderedGraph?.hasNode(id)) {
|
|
675
|
+
renderedGraph.setNodeAttribute(id, 'x', next.x)
|
|
676
|
+
renderedGraph.setNodeAttribute(id, 'y', next.y)
|
|
677
|
+
}
|
|
678
|
+
return { nodeId: id, position: next }
|
|
679
|
+
})
|
|
680
|
+
sigma?.refresh()
|
|
681
|
+
requestSelection({ mode: 'single', nodeId })
|
|
682
|
+
emit('positions-change', changes)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function handleInventoryOptionKeydown(payload: SListboxOptionKeydownPayload<string>): void {
|
|
686
|
+
const { event, value: nodeId } = payload
|
|
687
|
+
if (nodeId === null) {
|
|
688
|
+
throw new SGraphContractError('SGraphViewport: node inventory не поддерживает clear option.')
|
|
689
|
+
}
|
|
690
|
+
if (event.key === 'Enter' && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
|
|
691
|
+
requestActivation(nodeId)
|
|
692
|
+
return
|
|
693
|
+
}
|
|
694
|
+
const positioning = interactionContract.value.positioning
|
|
695
|
+
if (positioning.mode !== 'editable' || !event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
|
696
|
+
const step = positioning.keyboardStep
|
|
697
|
+
const deltas: Partial<Record<string, SGraphPosition>> = {
|
|
698
|
+
ArrowLeft: { x: -step, y: 0 },
|
|
699
|
+
ArrowRight: { x: step, y: 0 },
|
|
700
|
+
ArrowUp: { x: 0, y: -step },
|
|
701
|
+
ArrowDown: { x: 0, y: step },
|
|
702
|
+
}
|
|
703
|
+
const delta = deltas[event.key]
|
|
704
|
+
if (!delta) return
|
|
705
|
+
event.preventDefault()
|
|
706
|
+
event.stopPropagation()
|
|
707
|
+
nudgeNode(nodeId, delta)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
onMounted(() => {
|
|
711
|
+
assertViewportConfiguration()
|
|
712
|
+
const renderer = rendererRef.value
|
|
713
|
+
if (typeof ResizeObserver !== 'undefined' && renderer) {
|
|
714
|
+
resizeObserver = new ResizeObserver(() => {
|
|
715
|
+
if (!sigma) {
|
|
716
|
+
tryMountSigma()
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
sigma.resize()
|
|
720
|
+
sigma.refresh()
|
|
721
|
+
})
|
|
722
|
+
resizeObserver.observe(renderer)
|
|
723
|
+
}
|
|
724
|
+
void nextTick(tryMountSigma)
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
onBeforeUnmount(() => {
|
|
728
|
+
resizeObserver?.disconnect()
|
|
729
|
+
resizeObserver = null
|
|
730
|
+
sigma?.kill()
|
|
731
|
+
sigma = null
|
|
732
|
+
renderedGraph = null
|
|
733
|
+
renderError.value = null
|
|
734
|
+
renderState.value = 'idle'
|
|
735
|
+
cancelNodeDrag()
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
watch(() => props.graph, replaceGraph, { deep: true })
|
|
739
|
+
watch(runtimeTheme, refreshRuntimeTheme)
|
|
740
|
+
watch(() => props.interaction, refreshValidatedInteraction, { deep: true })
|
|
741
|
+
watch(() => props.showEdgeLabels, refreshValidatedSettings)
|
|
742
|
+
watch(() => props.labelZoomThreshold, refreshValidatedSettings)
|
|
743
|
+
watch(() => props.nodeSizeMode, refreshValidatedSettings)
|
|
744
|
+
|
|
745
|
+
function zoomIn(): void {
|
|
746
|
+
const camera = sigma?.getCamera()
|
|
747
|
+
if (!camera) return
|
|
748
|
+
const state = camera.getState()
|
|
749
|
+
camera.animate(
|
|
750
|
+
{ ratio: Math.max(state.ratio * 0.82, 0.02) },
|
|
751
|
+
{ duration: requiredRuntimeDuration('--s-graph-camera-duration') },
|
|
752
|
+
)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function zoomOut(): void {
|
|
756
|
+
const camera = sigma?.getCamera()
|
|
757
|
+
if (!camera) return
|
|
758
|
+
const state = camera.getState()
|
|
759
|
+
camera.animate(
|
|
760
|
+
{ ratio: Math.min(state.ratio * 1.18, 12) },
|
|
761
|
+
{ duration: requiredRuntimeDuration('--s-graph-camera-duration') },
|
|
762
|
+
)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function fit(): void {
|
|
766
|
+
sigma?.setCustomBBox(null)
|
|
767
|
+
sigma?.getCamera().animatedReset({ duration: requiredRuntimeDuration('--s-graph-camera-reset-duration') })
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
defineExpose({ zoomIn, zoomOut, fit })
|
|
771
|
+
</script>
|
|
772
|
+
|
|
773
|
+
<style scoped>
|
|
774
|
+
.s-graph-viewport {
|
|
775
|
+
position: absolute;
|
|
776
|
+
inset: 0;
|
|
777
|
+
min-width: 0;
|
|
778
|
+
min-height: 0;
|
|
779
|
+
overflow: hidden;
|
|
780
|
+
border: var(--s-graph-surface-border-width) solid var(--s-graph-surface-border);
|
|
781
|
+
border-radius: var(--s-graph-surface-radius);
|
|
782
|
+
background: var(--s-graph-surface-background);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.s-graph-viewport::before {
|
|
786
|
+
position: absolute;
|
|
787
|
+
z-index: var(--s-graph-grid-layer);
|
|
788
|
+
inset: 0;
|
|
789
|
+
background-image: var(--s-graph-grid-background-image);
|
|
790
|
+
background-position: var(--s-graph-grid-background-position);
|
|
791
|
+
background-size: var(--s-graph-grid-background-size);
|
|
792
|
+
content: "";
|
|
793
|
+
opacity: var(--s-graph-grid-opacity);
|
|
794
|
+
pointer-events: none;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.s-graph-viewport__renderer {
|
|
798
|
+
position: absolute;
|
|
799
|
+
z-index: var(--s-graph-renderer-layer);
|
|
800
|
+
inset: 0;
|
|
801
|
+
min-width: 0;
|
|
802
|
+
min-height: 0;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.s-graph-viewport__renderer :deep(canvas) {
|
|
806
|
+
border-radius: var(--s-graph-surface-radius);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.s-graph-viewport__error {
|
|
810
|
+
position: absolute;
|
|
811
|
+
z-index: var(--s-graph-error-layer);
|
|
812
|
+
inset: var(--s-graph-error-inset);
|
|
813
|
+
display: grid;
|
|
814
|
+
place-items: center;
|
|
815
|
+
border: var(--s-graph-error-border-width) solid var(--s-graph-error-border);
|
|
816
|
+
border-radius: var(--s-graph-error-radius);
|
|
817
|
+
background: var(--s-graph-error-background);
|
|
818
|
+
color: var(--s-graph-error-color);
|
|
819
|
+
font-family: var(--s-graph-label-font-family);
|
|
820
|
+
font-size: var(--s-graph-error-font-size);
|
|
821
|
+
font-weight: var(--s-graph-error-font-weight);
|
|
822
|
+
line-height: var(--s-graph-error-line-height);
|
|
823
|
+
text-align: center;
|
|
824
|
+
padding: var(--s-graph-error-padding);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.s-graph-viewport__node-inventory {
|
|
828
|
+
position: absolute;
|
|
829
|
+
z-index: var(--s-graph-inventory-layer);
|
|
830
|
+
inset-block-start: var(--s-graph-inventory-offset);
|
|
831
|
+
inset-inline-start: var(--s-graph-inventory-offset);
|
|
832
|
+
inline-size: min(var(--s-graph-inventory-inline-size), calc(100% - 2 * var(--s-graph-inventory-offset)));
|
|
833
|
+
max-block-size: calc(100% - 2 * var(--s-graph-inventory-offset));
|
|
834
|
+
overflow: auto;
|
|
835
|
+
border: var(--s-graph-inventory-border-width) solid transparent;
|
|
836
|
+
border-radius: var(--s-graph-inventory-radius);
|
|
837
|
+
opacity: 0;
|
|
838
|
+
clip-path: inset(0 0 100% 0);
|
|
839
|
+
pointer-events: none;
|
|
840
|
+
transform: translateY(calc(-1 * var(--s-graph-inventory-reveal-distance)));
|
|
841
|
+
transition:
|
|
842
|
+
opacity var(--s-motion-fast) ease,
|
|
843
|
+
transform var(--s-motion-fast) ease,
|
|
844
|
+
clip-path var(--s-motion-fast) ease;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.s-graph-viewport__node-inventory:focus-within,
|
|
848
|
+
.s-graph-viewport__node-inventory--fallback {
|
|
849
|
+
border-color: var(--s-graph-inventory-border);
|
|
850
|
+
background: var(--s-graph-inventory-background);
|
|
851
|
+
box-shadow: var(--s-graph-inventory-shadow);
|
|
852
|
+
opacity: 1;
|
|
853
|
+
clip-path: inset(0);
|
|
854
|
+
padding: var(--s-graph-inventory-padding);
|
|
855
|
+
pointer-events: auto;
|
|
856
|
+
transform: translateY(0);
|
|
857
|
+
}
|
|
858
|
+
</style>
|