@ltm-blueverse/alpha-semantic-hub 0.7.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 +1674 -0
- package/explorer/README.md +282 -0
- package/explorer/src/App.css +0 -0
- package/explorer/src/App.tsx +2035 -0
- package/explorer/src/ErrorBoundary.tsx +112 -0
- package/explorer/src/ExploreWorkspaceTabs.tsx +29 -0
- package/explorer/src/assets/hero.png +0 -0
- package/explorer/src/assets/react.svg +1 -0
- package/explorer/src/assets/vite.svg +1 -0
- package/explorer/src/explorerCapabilities.ts +24 -0
- package/explorer/src/index.css +71 -0
- package/explorer/src/main.tsx +7 -0
- package/explorer/src/store/edgePairKeys.d.ts +2 -0
- package/explorer/src/store/edgePairKeys.js +7 -0
- package/explorer/src/store/graphStore.ts +183 -0
- package/explorer/src/store/registryStore.ts +78 -0
- package/explorer/src/types.d.ts +16 -0
- package/explorer/src/ui/primitives.css +414 -0
- package/explorer/src/ui/primitives.tsx +170 -0
- package/explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx +279 -0
- package/explorer/src/workspaces/DiffMergeWorkspace/DiffMergeWorkspace.tsx +121 -0
- package/explorer/src/workspaces/EnrichWorkspace/EntityResolutionTab.tsx +454 -0
- package/explorer/src/workspaces/EnrichWorkspace/RegistryTab.tsx +285 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphCanvas.tsx +2673 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx +771 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx +410 -0
- package/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +3584 -0
- package/explorer/src/workspaces/GraphWorkspace/MarkdownContentViewer.tsx +705 -0
- package/explorer/src/workspaces/GraphWorkspace/SigmaSceneAdapter.tsx +55 -0
- package/explorer/src/workspaces/GraphWorkspace/TimelinePanel.tsx +193 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/clickSelectionBehavior.ts +29 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/fitViewBehavior.ts +15 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/focusCameraBehavior.ts +25 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/hoverActivationBehavior.ts +15 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/pathHighlightBehavior.ts +52 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/searchFocusBehavior.ts +36 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/types.ts +41 -0
- package/explorer/src/workspaces/GraphWorkspace/behaviors/viewModeSwitchBehavior.ts +29 -0
- package/explorer/src/workspaces/GraphWorkspace/graphAnalytics.ts +779 -0
- package/explorer/src/workspaces/GraphWorkspace/graphColorLegend.ts +36 -0
- package/explorer/src/workspaces/GraphWorkspace/graphConfig.ts +102 -0
- package/explorer/src/workspaces/GraphWorkspace/graphEntityShape.ts +34 -0
- package/explorer/src/workspaces/GraphWorkspace/graphLoading.ts +84 -0
- package/explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts +775 -0
- package/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts +2753 -0
- package/explorer/src/workspaces/GraphWorkspace/graphStructureLayer.ts +335 -0
- package/explorer/src/workspaces/GraphWorkspace/graphTheme.ts +1078 -0
- package/explorer/src/workspaces/GraphWorkspace/graphViewCopy.ts +41 -0
- package/explorer/src/workspaces/GraphWorkspace/localGraphTransition.ts +9 -0
- package/explorer/src/workspaces/GraphWorkspace/markdownEditorState.ts +114 -0
- package/explorer/src/workspaces/GraphWorkspace/markdownResourceClient.ts +103 -0
- package/explorer/src/workspaces/GraphWorkspace/markdownUrlSafety.ts +29 -0
- package/explorer/src/workspaces/GraphWorkspace/nodeMarkdownSync.ts +81 -0
- package/explorer/src/workspaces/GraphWorkspace/pluginRegistryPredicates.ts +24 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPlugin.tsx +353 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPluginPhaseC.tsx +522 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/index.ts +16 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/legendPlugin.tsx +120 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/neighborhoodPanelPlugin.tsx +231 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.tsx +142 -0
- package/explorer/src/workspaces/GraphWorkspace/plugins/types.ts +106 -0
- package/explorer/src/workspaces/GraphWorkspace/realtimeGraphAttributes.ts +50 -0
- package/explorer/src/workspaces/GraphWorkspace/scene.ts +79 -0
- package/explorer/src/workspaces/GraphWorkspace/sigmaNativeRendering.ts +421 -0
- package/explorer/src/workspaces/GraphWorkspace/smallGraphLayout.ts +135 -0
- package/explorer/src/workspaces/GraphWorkspace/temporalLifecyclePredicates.ts +31 -0
- package/explorer/src/workspaces/GraphWorkspace/temporalScrubberBounds.ts +46 -0
- package/explorer/src/workspaces/GraphWorkspace/temporalSnapshotGuards.ts +113 -0
- package/explorer/src/workspaces/GraphWorkspace/types.ts +395 -0
- package/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts +746 -0
- package/explorer/src/workspaces/GraphWorkspace/useMarkdownEditor.ts +177 -0
- package/explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx +196 -0
- package/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx +177 -0
- package/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx +291 -0
- package/explorer/src/workspaces/ManageWorkspace/OntologySummaryTab.tsx +346 -0
- package/explorer/src/workspaces/MemoryWorkspace.tsx +464 -0
- package/explorer/src/workspaces/OntologyWorkspace/AlignmentsTab.tsx +437 -0
- package/explorer/src/workspaces/OntologyWorkspace/HealthTab.tsx +214 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologyEditor.tsx +770 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologyLoader.tsx +913 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx +930 -0
- package/explorer/src/workspaces/OntologyWorkspace/OntologySearch.tsx +590 -0
- package/explorer/src/workspaces/OntologyWorkspace/ProposalReview.tsx +429 -0
- package/explorer/src/workspaces/OntologyWorkspace/SKOSVocabularyManager.tsx +696 -0
- package/explorer/src/workspaces/OntologyWorkspace/ShaclStudio.tsx +363 -0
- package/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx +546 -0
- package/explorer/src/workspaces/OntologyWorkspace/api.ts +164 -0
- package/explorer/src/workspaces/OntologyWorkspace/index.tsx +101 -0
- package/explorer/src/workspaces/OntologyWorkspace/ontologyEditorModel.ts +87 -0
- package/explorer/src/workspaces/OntologyWorkspace/ontologyUrlState.ts +94 -0
- package/explorer/src/workspaces/OntologyWorkspace/types.ts +119 -0
- package/explorer/src/workspaces/ReasoningWorkspace.tsx +243 -0
- package/explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx +271 -0
- package/explorer/src/workspaces/VocabularyWorkspace/ConceptTree.tsx +97 -0
- package/explorer/src/workspaces/VocabularyWorkspace/ImportDropzone.tsx +95 -0
- package/explorer/src/workspaces/VocabularyWorkspace/PropertyPanel.tsx +116 -0
- package/explorer/src/workspaces/VocabularyWorkspace/Sidebar.tsx +112 -0
- package/explorer/src/workspaces/VocabularyWorkspace/VocabularyWorkspace.tsx +70 -0
- package/explorer/src/workspaces/VocabularyWorkspace/queries.ts +52 -0
- package/explorer/src/workspaces/VocabularyWorkspace/types.ts +24 -0
- package/package.json +31 -0
- package/plugins/.claude-plugin/README.md +262 -0
- package/plugins/.claude-plugin/marketplace.json +20 -0
- package/plugins/.claude-plugin/plugin.json +33 -0
- package/plugins/.cline-plugin/README.md +32 -0
- package/plugins/.cline-plugin/marketplace.json +17 -0
- package/plugins/.cline-plugin/plugin.json +35 -0
- package/plugins/.codex-plugin/marketplace.json +21 -0
- package/plugins/.codex-plugin/plugin.json +35 -0
- package/plugins/.continue-plugin/README.md +34 -0
- package/plugins/.continue-plugin/marketplace.json +17 -0
- package/plugins/.continue-plugin/plugin.json +33 -0
- package/plugins/.cursor-plugin/marketplace.json +18 -0
- package/plugins/.cursor-plugin/plugin.json +32 -0
- package/plugins/.openclaw-plugin/README.md +62 -0
- package/plugins/.openclaw-plugin/marketplace.json +18 -0
- package/plugins/.openclaw-plugin/plugin.json +46 -0
- package/plugins/.pi-plugin/README.md +35 -0
- package/plugins/.vscode-plugin/README.md +48 -0
- package/plugins/.vscode-plugin/marketplace.json +17 -0
- package/plugins/.vscode-plugin/plugin.json +35 -0
- package/plugins/.windsurf-plugin/README.md +39 -0
- package/plugins/.windsurf-plugin/marketplace.json +17 -0
- package/plugins/.windsurf-plugin/plugin.json +35 -0
- package/plugins/agents/decision-advisor.md +126 -0
- package/plugins/agents/explainability.md +129 -0
- package/plugins/agents/kg-assistant.md +73 -0
- package/plugins/hooks/hooks.json +12 -0
- package/plugins/skills/causal/SKILL.md +74 -0
- package/plugins/skills/change/SKILL.md +80 -0
- package/plugins/skills/decision/SKILL.md +197 -0
- package/plugins/skills/deduplicate/SKILL.md +41 -0
- package/plugins/skills/embed/SKILL.md +230 -0
- package/plugins/skills/explain/SKILL.md +44 -0
- package/plugins/skills/export/SKILL.md +67 -0
- package/plugins/skills/extract/SKILL.md +93 -0
- package/plugins/skills/ingest/SKILL.md +38 -0
- package/plugins/skills/ontology/SKILL.md +89 -0
- package/plugins/skills/policy/SKILL.md +66 -0
- package/plugins/skills/provenance/SKILL.md +70 -0
- package/plugins/skills/query/SKILL.md +84 -0
- package/plugins/skills/reason/SKILL.md +201 -0
- package/plugins/skills/temporal/SKILL.md +164 -0
- package/plugins/skills/validate/SKILL.md +228 -0
- package/plugins/skills/visualize/SKILL.md +249 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import EdgeCurveProgram, { EdgeCurvedArrowProgram } from "@sigma/edge-curve";
|
|
2
|
+
import { NodeProgram, type ProgramInfo } from "sigma/rendering";
|
|
3
|
+
import { DEFAULT_EDGE_PROGRAM_CLASSES, DEFAULT_NODE_PROGRAM_CLASSES } from "sigma/settings";
|
|
4
|
+
import type { NodeDisplayData, RenderParams } from "sigma/types";
|
|
5
|
+
import { floatColor } from "sigma/utils";
|
|
6
|
+
import type { NodeHoverDrawingFunction, NodeLabelDrawingFunction } from "sigma/rendering";
|
|
7
|
+
|
|
8
|
+
import { GRAPH_THEME, type GraphEntityShapeVariant, withAlpha } from "./graphTheme";
|
|
9
|
+
|
|
10
|
+
type AlphaSemanticHubNodeDrawData = {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
size: number;
|
|
14
|
+
label: string;
|
|
15
|
+
color: string;
|
|
16
|
+
shellColor?: string;
|
|
17
|
+
coreScale?: number;
|
|
18
|
+
borderColor?: string;
|
|
19
|
+
borderSize?: number;
|
|
20
|
+
ringColor?: string;
|
|
21
|
+
ringSize?: number;
|
|
22
|
+
entityShape?: GraphEntityShapeVariant;
|
|
23
|
+
entityShapeKind?: number;
|
|
24
|
+
entityAspectRatio?: number;
|
|
25
|
+
nodeType?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ENTITY_TOKEN_UNIFORMS = ["u_sizeRatio", "u_correctionRatio", "u_matrix"] as const;
|
|
29
|
+
|
|
30
|
+
const ENTITY_TOKEN_FRAGMENT_SHADER = /* glsl */ `
|
|
31
|
+
precision highp float;
|
|
32
|
+
|
|
33
|
+
varying vec4 v_bodyColor;
|
|
34
|
+
varying vec4 v_glyphColor;
|
|
35
|
+
varying vec4 v_outlineColor;
|
|
36
|
+
varying vec4 v_color;
|
|
37
|
+
varying vec2 v_diffVector;
|
|
38
|
+
varying float v_radius;
|
|
39
|
+
varying float v_outlineSize;
|
|
40
|
+
varying float v_glyphScale;
|
|
41
|
+
varying float v_shapeKind;
|
|
42
|
+
varying float v_aspectRatio;
|
|
43
|
+
|
|
44
|
+
uniform float u_correctionRatio;
|
|
45
|
+
|
|
46
|
+
const float bias = 255.0 / 254.0;
|
|
47
|
+
const vec4 transparent = vec4(0.0, 0.0, 0.0, 0.0);
|
|
48
|
+
|
|
49
|
+
float hexMetric(vec2 point) {
|
|
50
|
+
vec2 q = abs(point);
|
|
51
|
+
return max(q.y, q.x * 0.8660254 + q.y * 0.5);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
vec2 rotate45(vec2 point) {
|
|
55
|
+
const float invSqrt2 = 0.70710678;
|
|
56
|
+
return vec2(
|
|
57
|
+
(point.x - point.y) * invSqrt2,
|
|
58
|
+
(point.x + point.y) * invSqrt2
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
float roundedBoxDistance(vec2 point, vec2 halfSize, float radius) {
|
|
63
|
+
vec2 q = abs(point) - halfSize + vec2(radius);
|
|
64
|
+
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - radius;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
float capsuleDistance(vec2 point) {
|
|
68
|
+
vec2 q = vec2(max(abs(point.x) - 0.44, 0.0), point.y);
|
|
69
|
+
return length(q) - 0.56;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
float shapeDistance(vec2 point, float shapeKind) {
|
|
73
|
+
if (shapeKind < 0.5) {
|
|
74
|
+
return length(point) - 1.0;
|
|
75
|
+
}
|
|
76
|
+
if (shapeKind < 1.5) {
|
|
77
|
+
return hexMetric(point) - 0.92;
|
|
78
|
+
}
|
|
79
|
+
if (shapeKind < 2.5) {
|
|
80
|
+
return roundedBoxDistance(rotate45(point), vec2(0.58, 0.58), 0.18);
|
|
81
|
+
}
|
|
82
|
+
if (shapeKind < 3.5) {
|
|
83
|
+
return capsuleDistance(point);
|
|
84
|
+
}
|
|
85
|
+
if (shapeKind < 4.5) {
|
|
86
|
+
return roundedBoxDistance(point, vec2(0.78, 0.78), 0.24);
|
|
87
|
+
}
|
|
88
|
+
return length(point) - 1.0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
float glyphDistance(vec2 point, float shapeKind, float scale) {
|
|
92
|
+
vec2 scaled = point / max(scale, 0.08);
|
|
93
|
+
if (shapeKind < 0.5) {
|
|
94
|
+
return 1.0;
|
|
95
|
+
}
|
|
96
|
+
if (shapeKind < 1.5) {
|
|
97
|
+
return abs(hexMetric(scaled) - 0.74) - 0.055;
|
|
98
|
+
}
|
|
99
|
+
if (shapeKind < 2.5) {
|
|
100
|
+
return abs(abs(scaled.x) + abs(scaled.y) - 0.78) - 0.045;
|
|
101
|
+
}
|
|
102
|
+
if (shapeKind < 3.5) {
|
|
103
|
+
return roundedBoxDistance(scaled, vec2(0.56, 0.07), 0.07);
|
|
104
|
+
}
|
|
105
|
+
if (shapeKind < 4.5) {
|
|
106
|
+
return abs(roundedBoxDistance(scaled, vec2(0.48, 0.48), 0.18)) - 0.045;
|
|
107
|
+
}
|
|
108
|
+
return 1.0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
void main(void) {
|
|
112
|
+
vec2 unit = vec2(
|
|
113
|
+
v_diffVector.x / max(v_radius * v_aspectRatio, 0.0001),
|
|
114
|
+
v_diffVector.y / max(v_radius, 0.0001)
|
|
115
|
+
);
|
|
116
|
+
float aa = (2.4 * u_correctionRatio) / max(v_radius, 1.0);
|
|
117
|
+
float distance = shapeDistance(unit, v_shapeKind);
|
|
118
|
+
float alpha = 1.0 - smoothstep(-aa, aa, distance);
|
|
119
|
+
|
|
120
|
+
#ifdef PICKING_MODE
|
|
121
|
+
if (alpha <= 0.0) {
|
|
122
|
+
gl_FragColor = transparent;
|
|
123
|
+
} else {
|
|
124
|
+
gl_FragColor = v_color;
|
|
125
|
+
gl_FragColor.a *= bias;
|
|
126
|
+
}
|
|
127
|
+
#else
|
|
128
|
+
if (alpha <= 0.0) {
|
|
129
|
+
gl_FragColor = transparent;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
float outlineNorm = clamp(v_outlineSize / max(v_radius, 1.0), 0.035, 0.28);
|
|
134
|
+
float outlineBlend = 1.0 - smoothstep(-outlineNorm - aa, -outlineNorm + aa, distance);
|
|
135
|
+
float isOutline = 1.0 - outlineBlend;
|
|
136
|
+
float topLight = clamp((-unit.y + 0.85) * 0.5, 0.0, 1.0);
|
|
137
|
+
vec4 color = v_bodyColor;
|
|
138
|
+
color.rgb += vec3(0.014) * pow(topLight, 2.2);
|
|
139
|
+
|
|
140
|
+
if (isOutline > 0.0) {
|
|
141
|
+
color = mix(color, v_outlineColor, isOutline);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
float glyphVisible = step(7.25, v_radius) * step(0.13, v_glyphScale) * step(0.5, v_shapeKind) * (1.0 - step(4.5, v_shapeKind));
|
|
145
|
+
float glyph = (1.0 - smoothstep(-aa * 1.4, aa * 1.4, glyphDistance(unit, v_shapeKind, clamp(v_glyphScale, 0.16, 0.52)))) * glyphVisible;
|
|
146
|
+
if (glyph > 0.0 && distance < -outlineNorm) {
|
|
147
|
+
color = mix(color, v_glyphColor, glyph * 0.38);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
color.a *= alpha;
|
|
151
|
+
gl_FragColor = color;
|
|
152
|
+
#endif
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
const ENTITY_TOKEN_VERTEX_SHADER = /* glsl */ `
|
|
157
|
+
attribute vec4 a_id;
|
|
158
|
+
attribute vec2 a_position;
|
|
159
|
+
attribute float a_size;
|
|
160
|
+
attribute float a_angle;
|
|
161
|
+
attribute vec4 a_bodyColor;
|
|
162
|
+
attribute vec4 a_glyphColor;
|
|
163
|
+
attribute vec4 a_outlineColor;
|
|
164
|
+
attribute float a_outlineSize;
|
|
165
|
+
attribute float a_glyphScale;
|
|
166
|
+
attribute float a_shapeKind;
|
|
167
|
+
attribute float a_aspectRatio;
|
|
168
|
+
|
|
169
|
+
uniform mat3 u_matrix;
|
|
170
|
+
uniform float u_sizeRatio;
|
|
171
|
+
uniform float u_correctionRatio;
|
|
172
|
+
|
|
173
|
+
varying vec4 v_bodyColor;
|
|
174
|
+
varying vec4 v_glyphColor;
|
|
175
|
+
varying vec4 v_outlineColor;
|
|
176
|
+
varying vec4 v_color;
|
|
177
|
+
varying vec2 v_diffVector;
|
|
178
|
+
varying float v_radius;
|
|
179
|
+
varying float v_outlineSize;
|
|
180
|
+
varying float v_glyphScale;
|
|
181
|
+
varying float v_shapeKind;
|
|
182
|
+
varying float v_aspectRatio;
|
|
183
|
+
|
|
184
|
+
const float bias = 255.0 / 254.0;
|
|
185
|
+
|
|
186
|
+
void main() {
|
|
187
|
+
float size = a_size * u_correctionRatio / u_sizeRatio * 4.0;
|
|
188
|
+
float aspect = max(a_aspectRatio, 1.0);
|
|
189
|
+
vec2 diffVector = size * vec2(cos(a_angle) * aspect, sin(a_angle));
|
|
190
|
+
vec2 position = a_position + diffVector;
|
|
191
|
+
|
|
192
|
+
gl_Position = vec4(
|
|
193
|
+
(u_matrix * vec3(position, 1)).xy,
|
|
194
|
+
0,
|
|
195
|
+
1
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
v_diffVector = diffVector;
|
|
199
|
+
v_radius = size / 2.0;
|
|
200
|
+
v_outlineSize = a_outlineSize;
|
|
201
|
+
v_glyphScale = a_glyphScale;
|
|
202
|
+
v_shapeKind = a_shapeKind;
|
|
203
|
+
v_aspectRatio = aspect;
|
|
204
|
+
|
|
205
|
+
#ifdef PICKING_MODE
|
|
206
|
+
v_color = a_id;
|
|
207
|
+
#else
|
|
208
|
+
v_bodyColor = a_bodyColor;
|
|
209
|
+
v_glyphColor = a_glyphColor;
|
|
210
|
+
v_outlineColor = a_outlineColor;
|
|
211
|
+
#endif
|
|
212
|
+
|
|
213
|
+
v_color.a *= bias;
|
|
214
|
+
}
|
|
215
|
+
`;
|
|
216
|
+
|
|
217
|
+
class EntityTokenNodeProgram extends NodeProgram<(typeof ENTITY_TOKEN_UNIFORMS)[number]> {
|
|
218
|
+
static readonly ANGLE_1 = 0;
|
|
219
|
+
static readonly ANGLE_2 = (2 * Math.PI) / 3;
|
|
220
|
+
static readonly ANGLE_3 = (4 * Math.PI) / 3;
|
|
221
|
+
|
|
222
|
+
drawLabel = drawAlphaSemanticHubNodeLabel;
|
|
223
|
+
|
|
224
|
+
drawHover = drawAlphaSemanticHubNodeHover;
|
|
225
|
+
|
|
226
|
+
getDefinition() {
|
|
227
|
+
return {
|
|
228
|
+
VERTICES: 3,
|
|
229
|
+
VERTEX_SHADER_SOURCE: ENTITY_TOKEN_VERTEX_SHADER,
|
|
230
|
+
FRAGMENT_SHADER_SOURCE: ENTITY_TOKEN_FRAGMENT_SHADER,
|
|
231
|
+
METHOD: WebGLRenderingContext.TRIANGLES,
|
|
232
|
+
UNIFORMS: ENTITY_TOKEN_UNIFORMS,
|
|
233
|
+
ATTRIBUTES: [
|
|
234
|
+
{ name: "a_position", size: 2, type: WebGLRenderingContext.FLOAT },
|
|
235
|
+
{ name: "a_size", size: 1, type: WebGLRenderingContext.FLOAT },
|
|
236
|
+
{ name: "a_bodyColor", size: 4, type: WebGLRenderingContext.UNSIGNED_BYTE, normalized: true },
|
|
237
|
+
{ name: "a_glyphColor", size: 4, type: WebGLRenderingContext.UNSIGNED_BYTE, normalized: true },
|
|
238
|
+
{ name: "a_outlineColor", size: 4, type: WebGLRenderingContext.UNSIGNED_BYTE, normalized: true },
|
|
239
|
+
{ name: "a_outlineSize", size: 1, type: WebGLRenderingContext.FLOAT },
|
|
240
|
+
{ name: "a_glyphScale", size: 1, type: WebGLRenderingContext.FLOAT },
|
|
241
|
+
{ name: "a_shapeKind", size: 1, type: WebGLRenderingContext.FLOAT },
|
|
242
|
+
{ name: "a_aspectRatio", size: 1, type: WebGLRenderingContext.FLOAT },
|
|
243
|
+
{ name: "a_id", size: 4, type: WebGLRenderingContext.UNSIGNED_BYTE, normalized: true },
|
|
244
|
+
],
|
|
245
|
+
CONSTANT_ATTRIBUTES: [
|
|
246
|
+
{ name: "a_angle", size: 1, type: WebGLRenderingContext.FLOAT },
|
|
247
|
+
],
|
|
248
|
+
CONSTANT_DATA: [
|
|
249
|
+
[EntityTokenNodeProgram.ANGLE_1],
|
|
250
|
+
[EntityTokenNodeProgram.ANGLE_2],
|
|
251
|
+
[EntityTokenNodeProgram.ANGLE_3],
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
processVisibleItem(nodeIndex: number, startIndex: number, data: NodeDisplayData & AlphaSemanticHubNodeDrawData): void {
|
|
257
|
+
const array = this.array;
|
|
258
|
+
const outlineColor = resolveAccentBorderColor(data.ringSize, data.ringColor, data.borderColor, GRAPH_THEME.nodes.selectedRing.color);
|
|
259
|
+
const outlineSize = Math.max(data.ringSize || 0, data.borderSize || 0.7);
|
|
260
|
+
|
|
261
|
+
array[startIndex++] = data.x;
|
|
262
|
+
array[startIndex++] = data.y;
|
|
263
|
+
array[startIndex++] = data.size;
|
|
264
|
+
array[startIndex++] = floatColor(data.color || GRAPH_THEME.palette.overview.nodeCore);
|
|
265
|
+
array[startIndex++] = floatColor(data.shellColor || withAlpha(GRAPH_THEME.palette.overview.nodeBase, 0.58));
|
|
266
|
+
array[startIndex++] = floatColor(outlineColor);
|
|
267
|
+
array[startIndex++] = outlineSize;
|
|
268
|
+
array[startIndex++] = data.coreScale ?? 0.22;
|
|
269
|
+
array[startIndex++] = data.entityShapeKind ?? GRAPH_THEME.nodes.entityShapes[data.entityShape || "entity"].shapeKind;
|
|
270
|
+
array[startIndex++] = data.entityAspectRatio ?? GRAPH_THEME.nodes.entityShapes[data.entityShape || "entity"].aspectRatio;
|
|
271
|
+
array[startIndex++] = nodeIndex;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
setUniforms(params: RenderParams, { gl, uniformLocations }: ProgramInfo<(typeof ENTITY_TOKEN_UNIFORMS)[number]>): void {
|
|
275
|
+
gl.uniform1f(uniformLocations.u_correctionRatio, params.correctionRatio);
|
|
276
|
+
gl.uniform1f(uniformLocations.u_sizeRatio, params.sizeRatio);
|
|
277
|
+
gl.uniformMatrix3fv(uniformLocations.u_matrix, false, params.matrix);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function resolveAccentBorderColor(
|
|
282
|
+
ringSize: number | undefined,
|
|
283
|
+
ringColor: string | undefined,
|
|
284
|
+
borderColor: string | undefined,
|
|
285
|
+
fallbackColor: string,
|
|
286
|
+
) {
|
|
287
|
+
if (typeof ringSize === "number" && ringSize > 0 && ringColor) {
|
|
288
|
+
return ringColor;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return borderColor || fallbackColor;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function drawRoundedRect(
|
|
295
|
+
context: CanvasRenderingContext2D,
|
|
296
|
+
x: number,
|
|
297
|
+
y: number,
|
|
298
|
+
width: number,
|
|
299
|
+
height: number,
|
|
300
|
+
radius: number,
|
|
301
|
+
) {
|
|
302
|
+
const clampedRadius = Math.max(0, Math.min(radius, Math.min(width, height) / 2));
|
|
303
|
+
context.beginPath();
|
|
304
|
+
context.moveTo(x + clampedRadius, y);
|
|
305
|
+
context.lineTo(x + width - clampedRadius, y);
|
|
306
|
+
context.quadraticCurveTo(x + width, y, x + width, y + clampedRadius);
|
|
307
|
+
context.lineTo(x + width, y + height - clampedRadius);
|
|
308
|
+
context.quadraticCurveTo(x + width, y + height, x + width - clampedRadius, y + height);
|
|
309
|
+
context.lineTo(x + clampedRadius, y + height);
|
|
310
|
+
context.quadraticCurveTo(x, y + height, x, y + height - clampedRadius);
|
|
311
|
+
context.lineTo(x, y + clampedRadius);
|
|
312
|
+
context.quadraticCurveTo(x, y, x + clampedRadius, y);
|
|
313
|
+
context.closePath();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export const drawAlphaSemanticHubNodeLabel: NodeLabelDrawingFunction = (context, rawData) => {
|
|
317
|
+
const data = rawData as typeof rawData & AlphaSemanticHubNodeDrawData;
|
|
318
|
+
if (!data.label) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const chipTheme = GRAPH_THEME.labels.chip;
|
|
323
|
+
const fontSize = Math.max(
|
|
324
|
+
chipTheme.fontSize,
|
|
325
|
+
Math.min(chipTheme.maxFontSize, data.size * chipTheme.sizeScale),
|
|
326
|
+
);
|
|
327
|
+
const font = `${chipTheme.fontWeight} ${fontSize}px ${chipTheme.fontFamily}`;
|
|
328
|
+
const paddingX = chipTheme.paddingX;
|
|
329
|
+
const paddingY = chipTheme.paddingY;
|
|
330
|
+
const borderColor = resolveAccentBorderColor(data.ringSize, data.ringColor, data.borderColor, chipTheme.borderColor);
|
|
331
|
+
|
|
332
|
+
context.save();
|
|
333
|
+
context.font = font;
|
|
334
|
+
context.textBaseline = "middle";
|
|
335
|
+
const metrics = context.measureText(data.label);
|
|
336
|
+
const width = metrics.width + paddingX * 2;
|
|
337
|
+
const height = fontSize + paddingY * 2;
|
|
338
|
+
const x = data.x + Math.max(data.size * 0.7, chipTheme.offsetX);
|
|
339
|
+
const y = data.y - Math.max(data.size * 0.9, chipTheme.offsetY) - height;
|
|
340
|
+
|
|
341
|
+
context.shadowColor = withAlpha(chipTheme.shadowColor, chipTheme.shadowAlpha);
|
|
342
|
+
context.shadowBlur = chipTheme.shadowBlur;
|
|
343
|
+
context.fillStyle = chipTheme.background;
|
|
344
|
+
drawRoundedRect(context, x, y, width, height, chipTheme.radius);
|
|
345
|
+
context.fill();
|
|
346
|
+
|
|
347
|
+
context.shadowBlur = 0;
|
|
348
|
+
context.strokeStyle = withAlpha(borderColor, chipTheme.borderAlpha);
|
|
349
|
+
context.lineWidth = 1;
|
|
350
|
+
drawRoundedRect(context, x, y, width, height, chipTheme.radius);
|
|
351
|
+
context.stroke();
|
|
352
|
+
|
|
353
|
+
context.fillStyle = chipTheme.textColor;
|
|
354
|
+
context.fillText(data.label, x + paddingX, y + height / 2);
|
|
355
|
+
context.restore();
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
export const drawAlphaSemanticHubNodeHover: NodeHoverDrawingFunction = (context, rawData) => {
|
|
359
|
+
const data = rawData as typeof rawData & AlphaSemanticHubNodeDrawData;
|
|
360
|
+
if (!data.label) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const hoverTheme = GRAPH_THEME.labels.hoverCard;
|
|
365
|
+
const borderColor = resolveAccentBorderColor(data.ringSize, data.ringColor, data.borderColor, hoverTheme.borderColor);
|
|
366
|
+
const metaLabel = (typeof data.nodeType === "string" && data.nodeType.trim().length > 0)
|
|
367
|
+
? data.nodeType.replaceAll("_", " ").toUpperCase()
|
|
368
|
+
: "NODE";
|
|
369
|
+
|
|
370
|
+
context.save();
|
|
371
|
+
context.textBaseline = "top";
|
|
372
|
+
|
|
373
|
+
const titleFont = `${hoverTheme.titleWeight} ${hoverTheme.titleSize}px ${hoverTheme.fontFamily}`;
|
|
374
|
+
const metaFont = `${hoverTheme.metaWeight} ${hoverTheme.metaSize}px ${hoverTheme.fontFamily}`;
|
|
375
|
+
context.font = titleFont;
|
|
376
|
+
const titleWidth = context.measureText(data.label).width;
|
|
377
|
+
context.font = metaFont;
|
|
378
|
+
const metaWidth = context.measureText(metaLabel).width;
|
|
379
|
+
|
|
380
|
+
const width = Math.max(titleWidth, metaWidth) + hoverTheme.paddingX * 2;
|
|
381
|
+
const height = hoverTheme.paddingY * 2 + hoverTheme.titleSize + hoverTheme.metaGap + hoverTheme.metaSize;
|
|
382
|
+
const x = data.x + Math.max(data.size * 0.9, hoverTheme.offsetX);
|
|
383
|
+
const y = data.y - Math.max(data.size * 1.1, hoverTheme.offsetY) - height;
|
|
384
|
+
|
|
385
|
+
context.shadowColor = withAlpha(hoverTheme.shadowColor, hoverTheme.shadowAlpha);
|
|
386
|
+
context.shadowBlur = hoverTheme.shadowBlur;
|
|
387
|
+
context.fillStyle = hoverTheme.background;
|
|
388
|
+
drawRoundedRect(context, x, y, width, height, hoverTheme.radius);
|
|
389
|
+
context.fill();
|
|
390
|
+
|
|
391
|
+
context.shadowBlur = 0;
|
|
392
|
+
context.strokeStyle = withAlpha(borderColor, hoverTheme.borderAlpha);
|
|
393
|
+
context.lineWidth = 1.2;
|
|
394
|
+
drawRoundedRect(context, x, y, width, height, hoverTheme.radius);
|
|
395
|
+
context.stroke();
|
|
396
|
+
|
|
397
|
+
context.fillStyle = hoverTheme.textColor;
|
|
398
|
+
context.font = titleFont;
|
|
399
|
+
context.fillText(data.label, x + hoverTheme.paddingX, y + hoverTheme.paddingY);
|
|
400
|
+
|
|
401
|
+
context.fillStyle = hoverTheme.metaColor;
|
|
402
|
+
context.font = metaFont;
|
|
403
|
+
context.fillText(
|
|
404
|
+
metaLabel,
|
|
405
|
+
x + hoverTheme.paddingX,
|
|
406
|
+
y + hoverTheme.paddingY + hoverTheme.titleSize + hoverTheme.metaGap,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
context.restore();
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
export const ASHUB_NODE_PROGRAM_CLASSES = {
|
|
413
|
+
...DEFAULT_NODE_PROGRAM_CLASSES,
|
|
414
|
+
circle: EntityTokenNodeProgram,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
export const ASHUB_EDGE_PROGRAM_CLASSES = {
|
|
418
|
+
...DEFAULT_EDGE_PROGRAM_CLASSES,
|
|
419
|
+
curve: EdgeCurveProgram,
|
|
420
|
+
curvedArrow: EdgeCurvedArrowProgram,
|
|
421
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export const SMALL_GRAPH_MAX_NODES = 48;
|
|
2
|
+
const PROVIDED_COORDINATE_COVERAGE = 0.92;
|
|
3
|
+
const MAX_COMPONENT_RADIUS = 78;
|
|
4
|
+
const COMPONENT_GAP = 48;
|
|
5
|
+
|
|
6
|
+
type LayoutEdge = {
|
|
7
|
+
source: string;
|
|
8
|
+
target: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function shouldUseSmallGraphLayout(nodeCount: number, coordinateCoverage: number): boolean {
|
|
12
|
+
return nodeCount > 0
|
|
13
|
+
&& nodeCount <= SMALL_GRAPH_MAX_NODES
|
|
14
|
+
&& coordinateCoverage < PROVIDED_COORDINATE_COVERAGE;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveGraphLayoutDecision(nodeCount: number, coordinateCoverage: number): {
|
|
18
|
+
useProvidedCoordinates: boolean;
|
|
19
|
+
useSmallGraphLayout: boolean;
|
|
20
|
+
layoutReady: boolean;
|
|
21
|
+
} {
|
|
22
|
+
const useProvidedCoordinates = coordinateCoverage >= PROVIDED_COORDINATE_COVERAGE;
|
|
23
|
+
const useSmallGraphLayout = shouldUseSmallGraphLayout(nodeCount, coordinateCoverage);
|
|
24
|
+
return {
|
|
25
|
+
useProvidedCoordinates,
|
|
26
|
+
useSmallGraphLayout,
|
|
27
|
+
layoutReady: useProvidedCoordinates || useSmallGraphLayout,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveNodeLayoutPosition(
|
|
32
|
+
decision: ReturnType<typeof resolveGraphLayoutDecision>,
|
|
33
|
+
provided: { x: number | null; y: number | null },
|
|
34
|
+
seeded: { x: number; y: number } | undefined,
|
|
35
|
+
): { x: number; y: number } {
|
|
36
|
+
if (decision.useProvidedCoordinates) {
|
|
37
|
+
return { x: provided.x ?? 0, y: provided.y ?? 0 };
|
|
38
|
+
}
|
|
39
|
+
if (decision.useSmallGraphLayout) {
|
|
40
|
+
return { x: seeded?.x ?? 0, y: seeded?.y ?? 0 };
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
x: provided.x ?? seeded?.x ?? 0,
|
|
44
|
+
y: provided.y ?? seeded?.y ?? 0,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Produce a compact deterministic layout for small graphs.
|
|
50
|
+
*
|
|
51
|
+
* ForceAtlas2 is useful for large connected datasets, but it makes tiny graphs
|
|
52
|
+
* with several disconnected components look like scattered dots. This layout
|
|
53
|
+
* keeps each connected component together and packs components into a centered
|
|
54
|
+
* grid so instance relationships remain legible on first render.
|
|
55
|
+
*/
|
|
56
|
+
export function buildSmallGraphSeedPositions(
|
|
57
|
+
nodeIds: string[],
|
|
58
|
+
edges: LayoutEdge[],
|
|
59
|
+
): Map<string, { x: number; y: number }> {
|
|
60
|
+
const ids = [...new Set(nodeIds)].sort((left, right) => left.localeCompare(right));
|
|
61
|
+
const adjacency = new Map(ids.map((id) => [id, new Set<string>()]));
|
|
62
|
+
|
|
63
|
+
edges.forEach(({ source, target }) => {
|
|
64
|
+
if (!adjacency.has(source) || !adjacency.has(target) || source === target) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
adjacency.get(source)?.add(target);
|
|
68
|
+
adjacency.get(target)?.add(source);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const visited = new Set<string>();
|
|
72
|
+
const components: string[][] = [];
|
|
73
|
+
ids.forEach((start) => {
|
|
74
|
+
if (visited.has(start)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const component: string[] = [];
|
|
78
|
+
const queue = [start];
|
|
79
|
+
visited.add(start);
|
|
80
|
+
while (queue.length > 0) {
|
|
81
|
+
const current = queue.shift();
|
|
82
|
+
if (!current) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
component.push(current);
|
|
86
|
+
[...(adjacency.get(current) ?? [])]
|
|
87
|
+
.sort((left, right) => left.localeCompare(right))
|
|
88
|
+
.forEach((neighbor) => {
|
|
89
|
+
if (!visited.has(neighbor)) {
|
|
90
|
+
visited.add(neighbor);
|
|
91
|
+
queue.push(neighbor);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
component.sort((left, right) => {
|
|
96
|
+
const degreeDelta = (adjacency.get(right)?.size ?? 0) - (adjacency.get(left)?.size ?? 0);
|
|
97
|
+
return degreeDelta || left.localeCompare(right);
|
|
98
|
+
});
|
|
99
|
+
components.push(component);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
components.sort((left, right) => right.length - left.length || left[0].localeCompare(right[0]));
|
|
103
|
+
|
|
104
|
+
const columns = Math.max(1, Math.ceil(Math.sqrt(components.length)));
|
|
105
|
+
const rows = Math.max(1, Math.ceil(components.length / columns));
|
|
106
|
+
// Adjacent cells must leave room for two maximum-radius components plus a
|
|
107
|
+
// readable gap. A smaller row height allows valid 12-node components to
|
|
108
|
+
// overlap vertically.
|
|
109
|
+
const cellWidth = MAX_COMPONENT_RADIUS * 2 + COMPONENT_GAP;
|
|
110
|
+
const cellHeight = MAX_COMPONENT_RADIUS * 2 + COMPONENT_GAP;
|
|
111
|
+
const positions = new Map<string, { x: number; y: number }>();
|
|
112
|
+
|
|
113
|
+
components.forEach((component, componentIndex) => {
|
|
114
|
+
const column = componentIndex % columns;
|
|
115
|
+
const row = Math.floor(componentIndex / columns);
|
|
116
|
+
const centerX = (column - (columns - 1) / 2) * cellWidth;
|
|
117
|
+
const centerY = (row - (rows - 1) / 2) * cellHeight;
|
|
118
|
+
|
|
119
|
+
if (component.length === 1) {
|
|
120
|
+
positions.set(component[0], { x: centerX, y: centerY });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const radius = Math.min(MAX_COMPONENT_RADIUS, 30 + component.length * 9);
|
|
125
|
+
component.forEach((nodeId, nodeIndex) => {
|
|
126
|
+
const angle = -Math.PI / 2 + (nodeIndex * Math.PI * 2) / component.length;
|
|
127
|
+
positions.set(nodeId, {
|
|
128
|
+
x: centerX + Math.cos(angle) * radius,
|
|
129
|
+
y: centerY + Math.sin(angle) * radius,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return positions;
|
|
135
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { GraphLoadSummary } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Predicates for gating GraphWorkspace temporal API requests.
|
|
5
|
+
*
|
|
6
|
+
* Temporal bounds and snapshot requests must strictly not execute until the
|
|
7
|
+
* initial graph load has succeeded (summary !== undefined). An empty graph
|
|
8
|
+
* (nodeCount: 0) is still a successful load and must not be rejected.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export function shouldFetchTemporalBounds(
|
|
12
|
+
summary: GraphLoadSummary | undefined,
|
|
13
|
+
): boolean {
|
|
14
|
+
return summary !== undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function shouldFetchTemporalSnapshot({
|
|
18
|
+
debouncedTime,
|
|
19
|
+
isLoading,
|
|
20
|
+
summary,
|
|
21
|
+
}: {
|
|
22
|
+
debouncedTime: Date | null;
|
|
23
|
+
isLoading: boolean;
|
|
24
|
+
summary: GraphLoadSummary | undefined;
|
|
25
|
+
}): boolean {
|
|
26
|
+
return (
|
|
27
|
+
summary !== undefined &&
|
|
28
|
+
debouncedTime !== null &&
|
|
29
|
+
!isLoading
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface ScrubberBoundsInput {
|
|
2
|
+
minDate?: string;
|
|
3
|
+
maxDate?: string;
|
|
4
|
+
now: Date;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ScrubberBounds {
|
|
8
|
+
minBound: Date;
|
|
9
|
+
maxBound: Date;
|
|
10
|
+
defaultTime: Date;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_MIN_DATE = new Date("1970-01-01T00:00:00Z");
|
|
14
|
+
const ONE_DAY_MS = 1000 * 60 * 60 * 24;
|
|
15
|
+
const PLAY_FRAMES = 60;
|
|
16
|
+
|
|
17
|
+
function parseBound(value: string | undefined, fallback: Date): Date {
|
|
18
|
+
if (!value) return fallback;
|
|
19
|
+
const parsed = new Date(value);
|
|
20
|
+
return Number.isNaN(parsed.getTime()) ? fallback : parsed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function clamp(value: Date, minBound: Date, maxBound: Date): Date {
|
|
24
|
+
if (value < minBound) return minBound;
|
|
25
|
+
if (value > maxBound) return maxBound;
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `/api/temporal/bounds` reports `max: null` for graphs whose nodes carry
|
|
31
|
+
* `valid_from` instants and no `valid_until`, which is the common case rather
|
|
32
|
+
* than malformed data. Such a graph is known up to the present and no further,
|
|
33
|
+
* so `now` is the honest upper bound and the honest starting playhead.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveScrubberBounds({ minDate, maxDate, now }: ScrubberBoundsInput): ScrubberBounds {
|
|
36
|
+
const minBound = parseBound(minDate, DEFAULT_MIN_DATE);
|
|
37
|
+
const maxBound = parseBound(maxDate, now);
|
|
38
|
+
const orderedMax = maxBound > minBound ? maxBound : minBound;
|
|
39
|
+
return { minBound, maxBound: orderedMax, defaultTime: clamp(now, minBound, orderedMax) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Keeps a play-through at ~PLAY_FRAMES steps whatever the span, with a one-day floor. */
|
|
43
|
+
export function resolvePlayStepMs(minBound: Date, maxBound: Date): number {
|
|
44
|
+
const span = maxBound.getTime() - minBound.getTime();
|
|
45
|
+
return Math.max(ONE_DAY_MS, Math.round(span / PLAY_FRAMES));
|
|
46
|
+
}
|