@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.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1674 -0
  3. package/explorer/README.md +282 -0
  4. package/explorer/src/App.css +0 -0
  5. package/explorer/src/App.tsx +2035 -0
  6. package/explorer/src/ErrorBoundary.tsx +112 -0
  7. package/explorer/src/ExploreWorkspaceTabs.tsx +29 -0
  8. package/explorer/src/assets/hero.png +0 -0
  9. package/explorer/src/assets/react.svg +1 -0
  10. package/explorer/src/assets/vite.svg +1 -0
  11. package/explorer/src/explorerCapabilities.ts +24 -0
  12. package/explorer/src/index.css +71 -0
  13. package/explorer/src/main.tsx +7 -0
  14. package/explorer/src/store/edgePairKeys.d.ts +2 -0
  15. package/explorer/src/store/edgePairKeys.js +7 -0
  16. package/explorer/src/store/graphStore.ts +183 -0
  17. package/explorer/src/store/registryStore.ts +78 -0
  18. package/explorer/src/types.d.ts +16 -0
  19. package/explorer/src/ui/primitives.css +414 -0
  20. package/explorer/src/ui/primitives.tsx +170 -0
  21. package/explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx +279 -0
  22. package/explorer/src/workspaces/DiffMergeWorkspace/DiffMergeWorkspace.tsx +121 -0
  23. package/explorer/src/workspaces/EnrichWorkspace/EntityResolutionTab.tsx +454 -0
  24. package/explorer/src/workspaces/EnrichWorkspace/RegistryTab.tsx +285 -0
  25. package/explorer/src/workspaces/GraphWorkspace/GraphCanvas.tsx +2673 -0
  26. package/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx +771 -0
  27. package/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx +410 -0
  28. package/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +3584 -0
  29. package/explorer/src/workspaces/GraphWorkspace/MarkdownContentViewer.tsx +705 -0
  30. package/explorer/src/workspaces/GraphWorkspace/SigmaSceneAdapter.tsx +55 -0
  31. package/explorer/src/workspaces/GraphWorkspace/TimelinePanel.tsx +193 -0
  32. package/explorer/src/workspaces/GraphWorkspace/behaviors/clickSelectionBehavior.ts +29 -0
  33. package/explorer/src/workspaces/GraphWorkspace/behaviors/fitViewBehavior.ts +15 -0
  34. package/explorer/src/workspaces/GraphWorkspace/behaviors/focusCameraBehavior.ts +25 -0
  35. package/explorer/src/workspaces/GraphWorkspace/behaviors/hoverActivationBehavior.ts +15 -0
  36. package/explorer/src/workspaces/GraphWorkspace/behaviors/pathHighlightBehavior.ts +52 -0
  37. package/explorer/src/workspaces/GraphWorkspace/behaviors/searchFocusBehavior.ts +36 -0
  38. package/explorer/src/workspaces/GraphWorkspace/behaviors/types.ts +41 -0
  39. package/explorer/src/workspaces/GraphWorkspace/behaviors/viewModeSwitchBehavior.ts +29 -0
  40. package/explorer/src/workspaces/GraphWorkspace/graphAnalytics.ts +779 -0
  41. package/explorer/src/workspaces/GraphWorkspace/graphColorLegend.ts +36 -0
  42. package/explorer/src/workspaces/GraphWorkspace/graphConfig.ts +102 -0
  43. package/explorer/src/workspaces/GraphWorkspace/graphEntityShape.ts +34 -0
  44. package/explorer/src/workspaces/GraphWorkspace/graphLoading.ts +84 -0
  45. package/explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts +775 -0
  46. package/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts +2753 -0
  47. package/explorer/src/workspaces/GraphWorkspace/graphStructureLayer.ts +335 -0
  48. package/explorer/src/workspaces/GraphWorkspace/graphTheme.ts +1078 -0
  49. package/explorer/src/workspaces/GraphWorkspace/graphViewCopy.ts +41 -0
  50. package/explorer/src/workspaces/GraphWorkspace/localGraphTransition.ts +9 -0
  51. package/explorer/src/workspaces/GraphWorkspace/markdownEditorState.ts +114 -0
  52. package/explorer/src/workspaces/GraphWorkspace/markdownResourceClient.ts +103 -0
  53. package/explorer/src/workspaces/GraphWorkspace/markdownUrlSafety.ts +29 -0
  54. package/explorer/src/workspaces/GraphWorkspace/nodeMarkdownSync.ts +81 -0
  55. package/explorer/src/workspaces/GraphWorkspace/pluginRegistryPredicates.ts +24 -0
  56. package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPlugin.tsx +353 -0
  57. package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPluginPhaseC.tsx +522 -0
  58. package/explorer/src/workspaces/GraphWorkspace/plugins/index.ts +16 -0
  59. package/explorer/src/workspaces/GraphWorkspace/plugins/legendPlugin.tsx +120 -0
  60. package/explorer/src/workspaces/GraphWorkspace/plugins/neighborhoodPanelPlugin.tsx +231 -0
  61. package/explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.tsx +142 -0
  62. package/explorer/src/workspaces/GraphWorkspace/plugins/types.ts +106 -0
  63. package/explorer/src/workspaces/GraphWorkspace/realtimeGraphAttributes.ts +50 -0
  64. package/explorer/src/workspaces/GraphWorkspace/scene.ts +79 -0
  65. package/explorer/src/workspaces/GraphWorkspace/sigmaNativeRendering.ts +421 -0
  66. package/explorer/src/workspaces/GraphWorkspace/smallGraphLayout.ts +135 -0
  67. package/explorer/src/workspaces/GraphWorkspace/temporalLifecyclePredicates.ts +31 -0
  68. package/explorer/src/workspaces/GraphWorkspace/temporalScrubberBounds.ts +46 -0
  69. package/explorer/src/workspaces/GraphWorkspace/temporalSnapshotGuards.ts +113 -0
  70. package/explorer/src/workspaces/GraphWorkspace/types.ts +395 -0
  71. package/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts +746 -0
  72. package/explorer/src/workspaces/GraphWorkspace/useMarkdownEditor.ts +177 -0
  73. package/explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx +196 -0
  74. package/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx +177 -0
  75. package/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx +291 -0
  76. package/explorer/src/workspaces/ManageWorkspace/OntologySummaryTab.tsx +346 -0
  77. package/explorer/src/workspaces/MemoryWorkspace.tsx +464 -0
  78. package/explorer/src/workspaces/OntologyWorkspace/AlignmentsTab.tsx +437 -0
  79. package/explorer/src/workspaces/OntologyWorkspace/HealthTab.tsx +214 -0
  80. package/explorer/src/workspaces/OntologyWorkspace/OntologyEditor.tsx +770 -0
  81. package/explorer/src/workspaces/OntologyWorkspace/OntologyLoader.tsx +913 -0
  82. package/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx +930 -0
  83. package/explorer/src/workspaces/OntologyWorkspace/OntologySearch.tsx +590 -0
  84. package/explorer/src/workspaces/OntologyWorkspace/ProposalReview.tsx +429 -0
  85. package/explorer/src/workspaces/OntologyWorkspace/SKOSVocabularyManager.tsx +696 -0
  86. package/explorer/src/workspaces/OntologyWorkspace/ShaclStudio.tsx +363 -0
  87. package/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx +546 -0
  88. package/explorer/src/workspaces/OntologyWorkspace/api.ts +164 -0
  89. package/explorer/src/workspaces/OntologyWorkspace/index.tsx +101 -0
  90. package/explorer/src/workspaces/OntologyWorkspace/ontologyEditorModel.ts +87 -0
  91. package/explorer/src/workspaces/OntologyWorkspace/ontologyUrlState.ts +94 -0
  92. package/explorer/src/workspaces/OntologyWorkspace/types.ts +119 -0
  93. package/explorer/src/workspaces/ReasoningWorkspace.tsx +243 -0
  94. package/explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx +271 -0
  95. package/explorer/src/workspaces/VocabularyWorkspace/ConceptTree.tsx +97 -0
  96. package/explorer/src/workspaces/VocabularyWorkspace/ImportDropzone.tsx +95 -0
  97. package/explorer/src/workspaces/VocabularyWorkspace/PropertyPanel.tsx +116 -0
  98. package/explorer/src/workspaces/VocabularyWorkspace/Sidebar.tsx +112 -0
  99. package/explorer/src/workspaces/VocabularyWorkspace/VocabularyWorkspace.tsx +70 -0
  100. package/explorer/src/workspaces/VocabularyWorkspace/queries.ts +52 -0
  101. package/explorer/src/workspaces/VocabularyWorkspace/types.ts +24 -0
  102. package/package.json +31 -0
  103. package/plugins/.claude-plugin/README.md +262 -0
  104. package/plugins/.claude-plugin/marketplace.json +20 -0
  105. package/plugins/.claude-plugin/plugin.json +33 -0
  106. package/plugins/.cline-plugin/README.md +32 -0
  107. package/plugins/.cline-plugin/marketplace.json +17 -0
  108. package/plugins/.cline-plugin/plugin.json +35 -0
  109. package/plugins/.codex-plugin/marketplace.json +21 -0
  110. package/plugins/.codex-plugin/plugin.json +35 -0
  111. package/plugins/.continue-plugin/README.md +34 -0
  112. package/plugins/.continue-plugin/marketplace.json +17 -0
  113. package/plugins/.continue-plugin/plugin.json +33 -0
  114. package/plugins/.cursor-plugin/marketplace.json +18 -0
  115. package/plugins/.cursor-plugin/plugin.json +32 -0
  116. package/plugins/.openclaw-plugin/README.md +62 -0
  117. package/plugins/.openclaw-plugin/marketplace.json +18 -0
  118. package/plugins/.openclaw-plugin/plugin.json +46 -0
  119. package/plugins/.pi-plugin/README.md +35 -0
  120. package/plugins/.vscode-plugin/README.md +48 -0
  121. package/plugins/.vscode-plugin/marketplace.json +17 -0
  122. package/plugins/.vscode-plugin/plugin.json +35 -0
  123. package/plugins/.windsurf-plugin/README.md +39 -0
  124. package/plugins/.windsurf-plugin/marketplace.json +17 -0
  125. package/plugins/.windsurf-plugin/plugin.json +35 -0
  126. package/plugins/agents/decision-advisor.md +126 -0
  127. package/plugins/agents/explainability.md +129 -0
  128. package/plugins/agents/kg-assistant.md +73 -0
  129. package/plugins/hooks/hooks.json +12 -0
  130. package/plugins/skills/causal/SKILL.md +74 -0
  131. package/plugins/skills/change/SKILL.md +80 -0
  132. package/plugins/skills/decision/SKILL.md +197 -0
  133. package/plugins/skills/deduplicate/SKILL.md +41 -0
  134. package/plugins/skills/embed/SKILL.md +230 -0
  135. package/plugins/skills/explain/SKILL.md +44 -0
  136. package/plugins/skills/export/SKILL.md +67 -0
  137. package/plugins/skills/extract/SKILL.md +93 -0
  138. package/plugins/skills/ingest/SKILL.md +38 -0
  139. package/plugins/skills/ontology/SKILL.md +89 -0
  140. package/plugins/skills/policy/SKILL.md +66 -0
  141. package/plugins/skills/provenance/SKILL.md +70 -0
  142. package/plugins/skills/query/SKILL.md +84 -0
  143. package/plugins/skills/reason/SKILL.md +201 -0
  144. package/plugins/skills/temporal/SKILL.md +164 -0
  145. package/plugins/skills/validate/SKILL.md +228 -0
  146. package/plugins/skills/visualize/SKILL.md +249 -0
@@ -0,0 +1,775 @@
1
+ import type Graph from "graphology";
2
+ import Sigma from "sigma";
3
+
4
+ import { graph, type EdgeAttributes, type NodeAttributes } from "../../store/graphStore";
5
+ import { blendHex, GRAPH_THEME, withAlpha, zoomTierAtLeast } from "./graphTheme";
6
+ import type {
7
+ GraphAnalyticsSnapshot,
8
+ GraphEffectsState,
9
+ GraphInteractionState,
10
+ GraphTemporalState,
11
+ } from "./types";
12
+
13
+ type GraphRef = typeof graph | Graph<NodeAttributes, EdgeAttributes>;
14
+
15
+ export type ViewportPoint = { x: number; y: number };
16
+
17
+ export type PathSegmentOverlay = {
18
+ sourceId: string;
19
+ targetId: string;
20
+ source: ViewportPoint;
21
+ target: ViewportPoint;
22
+ color: string;
23
+ size: number;
24
+ };
25
+
26
+ export type VisibleNodeSample = {
27
+ nodeId: string;
28
+ point: ViewportPoint;
29
+ size: number;
30
+ attrs: NodeAttributes;
31
+ };
32
+
33
+ function isPointNearViewport(point: ViewportPoint, width: number, height: number, padding = 96) {
34
+ return point.x >= -padding
35
+ && point.y >= -padding
36
+ && point.x <= width + padding
37
+ && point.y <= height + padding;
38
+ }
39
+
40
+ function drawGlowHalo(
41
+ context: CanvasRenderingContext2D,
42
+ x: number,
43
+ y: number,
44
+ radius: number,
45
+ color: string,
46
+ ) {
47
+ const gradient = context.createRadialGradient(x, y, 0, x, y, radius);
48
+ gradient.addColorStop(0, color);
49
+ gradient.addColorStop(1, "rgba(0,0,0,0)");
50
+ context.fillStyle = gradient;
51
+ context.beginPath();
52
+ context.arc(x, y, radius, 0, Math.PI * 2);
53
+ context.fill();
54
+ }
55
+
56
+ function createScratchCanvas(width: number, height: number) {
57
+ const canvas = document.createElement("canvas");
58
+ canvas.width = width;
59
+ canvas.height = height;
60
+ return canvas;
61
+ }
62
+
63
+ function parseCssColor(color: string) {
64
+ if (color.startsWith("#")) {
65
+ const hex = color.slice(1);
66
+ const normalized = hex.length === 3
67
+ ? hex.split("").map((char) => `${char}${char}`).join("")
68
+ : hex;
69
+ if (normalized.length === 6) {
70
+ return {
71
+ r: Number.parseInt(normalized.slice(0, 2), 16),
72
+ g: Number.parseInt(normalized.slice(2, 4), 16),
73
+ b: Number.parseInt(normalized.slice(4, 6), 16),
74
+ };
75
+ }
76
+ }
77
+
78
+ const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
79
+ if (match) {
80
+ return {
81
+ r: Number.parseInt(match[1], 10),
82
+ g: Number.parseInt(match[2], 10),
83
+ b: Number.parseInt(match[3], 10),
84
+ };
85
+ }
86
+
87
+ return { r: 130, g: 145, b: 165 };
88
+ }
89
+
90
+ function createThresholdMask(
91
+ alphaValues: Uint8ClampedArray,
92
+ width: number,
93
+ height: number,
94
+ threshold: number,
95
+ ) {
96
+ const mask = new Uint8Array(width * height);
97
+ let count = 0;
98
+ let sumX = 0;
99
+ let sumY = 0;
100
+ let sumWeight = 0;
101
+
102
+ for (let y = 0; y < height; y += 1) {
103
+ for (let x = 0; x < width; x += 1) {
104
+ const index = y * width + x;
105
+ const alpha = alphaValues[index];
106
+ if (alpha < threshold) {
107
+ continue;
108
+ }
109
+
110
+ mask[index] = 1;
111
+ count += 1;
112
+ sumX += x * alpha;
113
+ sumY += y * alpha;
114
+ sumWeight += alpha;
115
+ }
116
+ }
117
+
118
+ const centroid = sumWeight > 0
119
+ ? { x: sumX / sumWeight, y: sumY / sumWeight }
120
+ : { x: width / 2, y: height / 2 };
121
+
122
+ return { mask, count, centroid };
123
+ }
124
+
125
+ function blurAlphaValues(
126
+ alphaValues: Uint8ClampedArray,
127
+ width: number,
128
+ height: number,
129
+ passes: number,
130
+ ) {
131
+ if (passes <= 0) {
132
+ return alphaValues;
133
+ }
134
+
135
+ let source = alphaValues;
136
+ for (let pass = 0; pass < passes; pass += 1) {
137
+ const next = new Uint8ClampedArray(width * height);
138
+ for (let y = 0; y < height; y += 1) {
139
+ for (let x = 0; x < width; x += 1) {
140
+ let sum = 0;
141
+ let samples = 0;
142
+ for (let oy = -1; oy <= 1; oy += 1) {
143
+ const sampleY = y + oy;
144
+ if (sampleY < 0 || sampleY >= height) {
145
+ continue;
146
+ }
147
+ for (let ox = -1; ox <= 1; ox += 1) {
148
+ const sampleX = x + ox;
149
+ if (sampleX < 0 || sampleX >= width) {
150
+ continue;
151
+ }
152
+ sum += source[sampleY * width + sampleX];
153
+ samples += 1;
154
+ }
155
+ }
156
+ next[y * width + x] = Math.round(sum / Math.max(samples, 1));
157
+ }
158
+ }
159
+ source = next;
160
+ }
161
+
162
+ return source;
163
+ }
164
+
165
+ function isolateDominantMask(
166
+ mask: Uint8Array,
167
+ width: number,
168
+ height: number,
169
+ alphaValues: Uint8ClampedArray,
170
+ ) {
171
+ const visited = new Uint8Array(mask.length);
172
+ const queue = new Int32Array(mask.length);
173
+ let bestPixels: number[] = [];
174
+ let totalCount = 0;
175
+
176
+ for (let index = 0; index < mask.length; index += 1) {
177
+ if (!mask[index] || visited[index]) {
178
+ continue;
179
+ }
180
+
181
+ let head = 0;
182
+ let tail = 0;
183
+ const component: number[] = [];
184
+ visited[index] = 1;
185
+ queue[tail++] = index;
186
+
187
+ while (head < tail) {
188
+ const current = queue[head++];
189
+ component.push(current);
190
+
191
+ const x = current % width;
192
+ const y = Math.floor(current / width);
193
+ const neighbors = [
194
+ current - 1,
195
+ current + 1,
196
+ current - width,
197
+ current + width,
198
+ ];
199
+
200
+ for (let n = 0; n < neighbors.length; n += 1) {
201
+ const neighbor = neighbors[n];
202
+ if (neighbor < 0 || neighbor >= mask.length || visited[neighbor] || !mask[neighbor]) {
203
+ continue;
204
+ }
205
+ if ((n === 0 && x === 0) || (n === 1 && x === width - 1) || (n === 2 && y === 0) || (n === 3 && y === height - 1)) {
206
+ continue;
207
+ }
208
+ visited[neighbor] = 1;
209
+ queue[tail++] = neighbor;
210
+ }
211
+ }
212
+
213
+ totalCount += component.length;
214
+ if (component.length > bestPixels.length) {
215
+ bestPixels = component;
216
+ }
217
+ }
218
+
219
+ const dominantMask = new Uint8Array(mask.length);
220
+ let sumX = 0;
221
+ let sumY = 0;
222
+ let sumWeight = 0;
223
+ bestPixels.forEach((index) => {
224
+ dominantMask[index] = 1;
225
+ const alpha = alphaValues[index];
226
+ const x = index % width;
227
+ const y = Math.floor(index / width);
228
+ sumX += x * alpha;
229
+ sumY += y * alpha;
230
+ sumWeight += alpha;
231
+ });
232
+
233
+ return {
234
+ mask: dominantMask,
235
+ count: bestPixels.length,
236
+ centroid: sumWeight > 0
237
+ ? { x: sumX / sumWeight, y: sumY / sumWeight }
238
+ : { x: width / 2, y: height / 2 },
239
+ occupancyRatio: bestPixels.length / Math.max(width * height, 1),
240
+ dominantMassRatio: bestPixels.length / Math.max(totalCount, 1),
241
+ };
242
+ }
243
+
244
+ function renderDensityField(
245
+ samples: VisibleNodeSample[],
246
+ width: number,
247
+ height: number,
248
+ ) {
249
+ const scale = GRAPH_THEME.effects.semanticRegions.fogResolutionScale;
250
+ const gridWidth = Math.max(72, Math.round(width * scale));
251
+ const gridHeight = Math.max(72, Math.round(height * scale));
252
+ const canvas = createScratchCanvas(gridWidth, gridHeight);
253
+ const context = canvas.getContext("2d");
254
+ if (!context) {
255
+ return null;
256
+ }
257
+
258
+ context.clearRect(0, 0, gridWidth, gridHeight);
259
+ context.globalCompositeOperation = "source-over";
260
+
261
+ samples.forEach((sample) => {
262
+ const x = sample.point.x * scale;
263
+ const y = sample.point.y * scale;
264
+ const radius = Math.max(
265
+ 2.5,
266
+ (GRAPH_THEME.effects.semanticRegions.splatRadius + sample.size * 0.9) * scale,
267
+ );
268
+ const gradient = context.createRadialGradient(x, y, 0, x, y, radius);
269
+ gradient.addColorStop(0, "rgba(255,255,255,0.05)");
270
+ gradient.addColorStop(0.58, "rgba(255,255,255,0.02)");
271
+ gradient.addColorStop(1, "rgba(255,255,255,0)");
272
+ context.fillStyle = gradient;
273
+ context.beginPath();
274
+ context.arc(x, y, radius, 0, Math.PI * 2);
275
+ context.fill();
276
+ });
277
+
278
+ const imageData = context.getImageData(0, 0, gridWidth, gridHeight);
279
+ const alphaValues = new Uint8ClampedArray(gridWidth * gridHeight);
280
+ let maxAlpha = 0;
281
+ for (let index = 0; index < alphaValues.length; index += 1) {
282
+ const alpha = imageData.data[index * 4 + 3];
283
+ alphaValues[index] = alpha;
284
+ if (alpha > maxAlpha) {
285
+ maxAlpha = alpha;
286
+ }
287
+ }
288
+
289
+ if (maxAlpha <= 0) {
290
+ return null;
291
+ }
292
+
293
+ const blurredAlphaValues = blurAlphaValues(
294
+ alphaValues,
295
+ gridWidth,
296
+ gridHeight,
297
+ GRAPH_THEME.effects.semanticRegions.blurPasses,
298
+ );
299
+ const blurredMaxAlpha = blurredAlphaValues.reduce((value, alpha) => Math.max(value, alpha), 0);
300
+ if (blurredMaxAlpha <= 0) {
301
+ return null;
302
+ }
303
+
304
+ return { canvas, gridWidth, gridHeight, alphaValues: blurredAlphaValues, maxAlpha: blurredMaxAlpha, scale };
305
+ }
306
+
307
+ function drawDensityFog(
308
+ context: CanvasRenderingContext2D,
309
+ densityField: ReturnType<typeof renderDensityField>,
310
+ color: string,
311
+ alpha: number,
312
+ ) {
313
+ if (!densityField) {
314
+ return;
315
+ }
316
+
317
+ const { gridWidth, gridHeight, alphaValues } = densityField;
318
+ const fogCanvas = createScratchCanvas(gridWidth, gridHeight);
319
+ const fogContext = fogCanvas.getContext("2d");
320
+ if (!fogContext) {
321
+ return;
322
+ }
323
+
324
+ const imageData = fogContext.createImageData(gridWidth, gridHeight);
325
+ const rgb = parseCssColor(color);
326
+ for (let index = 0; index < alphaValues.length; index += 1) {
327
+ const sourceAlpha = alphaValues[index] / 255;
328
+ if (sourceAlpha <= 0) {
329
+ continue;
330
+ }
331
+
332
+ const pixelIndex = index * 4;
333
+ imageData.data[pixelIndex] = rgb.r;
334
+ imageData.data[pixelIndex + 1] = rgb.g;
335
+ imageData.data[pixelIndex + 2] = rgb.b;
336
+ imageData.data[pixelIndex + 3] = Math.round(255 * Math.pow(sourceAlpha, 1.7) * alpha);
337
+ }
338
+
339
+ fogContext.putImageData(imageData, 0, 0);
340
+ context.save();
341
+ context.imageSmoothingEnabled = true;
342
+ context.globalCompositeOperation = "lighter";
343
+ context.drawImage(fogCanvas, 0, 0, context.canvas.width, context.canvas.height);
344
+ context.restore();
345
+ }
346
+
347
+ function drawMaskContour(
348
+ context: CanvasRenderingContext2D,
349
+ mask: Uint8Array,
350
+ gridWidth: number,
351
+ gridHeight: number,
352
+ color: string,
353
+ alpha: number,
354
+ ) {
355
+ const contourCanvas = createScratchCanvas(gridWidth, gridHeight);
356
+ const contourContext = contourCanvas.getContext("2d");
357
+ if (!contourContext) {
358
+ return;
359
+ }
360
+
361
+ const imageData = contourContext.createImageData(gridWidth, gridHeight);
362
+ const rgb = parseCssColor(color);
363
+
364
+ for (let y = 1; y < gridHeight - 1; y += 1) {
365
+ for (let x = 1; x < gridWidth - 1; x += 1) {
366
+ const index = y * gridWidth + x;
367
+ if (!mask[index]) {
368
+ continue;
369
+ }
370
+
371
+ const isEdge = !mask[index - 1]
372
+ || !mask[index + 1]
373
+ || !mask[index - gridWidth]
374
+ || !mask[index + gridWidth];
375
+ if (!isEdge) {
376
+ continue;
377
+ }
378
+
379
+ const pixelIndex = index * 4;
380
+ imageData.data[pixelIndex] = rgb.r;
381
+ imageData.data[pixelIndex + 1] = rgb.g;
382
+ imageData.data[pixelIndex + 2] = rgb.b;
383
+ imageData.data[pixelIndex + 3] = Math.round(255 * alpha);
384
+ }
385
+ }
386
+
387
+ contourContext.putImageData(imageData, 0, 0);
388
+ context.save();
389
+ context.imageSmoothingEnabled = true;
390
+ context.drawImage(contourCanvas, 0, 0, context.canvas.width, context.canvas.height);
391
+ context.restore();
392
+ }
393
+
394
+ function buildRegionRenderColors(color: string) {
395
+ return {
396
+ fogColor: blendHex("#0E1927", color, 0.46),
397
+ innerContourColor: blendHex("#1C2C3F", color, 0.78),
398
+ outerContourColor: blendHex("#132131", color, 0.6),
399
+ labelBorderColor: blendHex("#2B3F57", color, 0.78),
400
+ };
401
+ }
402
+
403
+ export function collectVisibleNodeSamples(
404
+ sigma: Sigma,
405
+ graphRef: GraphRef,
406
+ viewportWidth: number,
407
+ viewportHeight: number,
408
+ ): VisibleNodeSample[] {
409
+ const samples: VisibleNodeSample[] = [];
410
+
411
+ graphRef.forEachNode((nodeId, attrs) => {
412
+ const displayData = sigma.getNodeDisplayData(nodeId);
413
+ if (!displayData) {
414
+ return;
415
+ }
416
+
417
+ const point = sigma.graphToViewport({ x: displayData.x, y: displayData.y });
418
+ if (!isPointNearViewport(point, viewportWidth, viewportHeight, 84)) {
419
+ return;
420
+ }
421
+
422
+ samples.push({
423
+ nodeId,
424
+ point,
425
+ size: displayData.size,
426
+ attrs: attrs as NodeAttributes,
427
+ });
428
+ });
429
+
430
+ return samples;
431
+ }
432
+
433
+ function drawRegionLabel(
434
+ context: CanvasRenderingContext2D,
435
+ x: number,
436
+ y: number,
437
+ text: string,
438
+ color: string,
439
+ ) {
440
+ const label = text.length > 22 ? `${text.slice(0, 21)}…` : text;
441
+ context.save();
442
+ context.font = `600 11px "IBM Plex Sans", Inter, system-ui, sans-serif`;
443
+ context.textBaseline = "middle";
444
+ const width = context.measureText(label).width + 14;
445
+ const height = 22;
446
+ const left = x - width / 2;
447
+ const top = y - height / 2;
448
+
449
+ context.fillStyle = "rgba(8, 14, 24, 0.84)";
450
+ context.strokeStyle = withAlpha(color, 0.24);
451
+ context.lineWidth = 1;
452
+ context.beginPath();
453
+ context.roundRect(left, top, width, height, 999);
454
+ context.fill();
455
+ context.stroke();
456
+
457
+ context.fillStyle = "rgba(235, 244, 255, 0.88)";
458
+ context.fillText(label, left + 7, y);
459
+ context.restore();
460
+ }
461
+
462
+ export function drawSemanticRegionsLayer(
463
+ context: CanvasRenderingContext2D,
464
+ analytics: GraphAnalyticsSnapshot | null,
465
+ visibleNodes: VisibleNodeSample[],
466
+ interactionState: GraphInteractionState,
467
+ effectsState: GraphEffectsState,
468
+ ) {
469
+ if (!effectsState.semanticRegionsEnabled || !analytics?.semanticRegions.ready) {
470
+ return;
471
+ }
472
+
473
+ if (!zoomTierAtLeast(interactionState.zoomTier, GRAPH_THEME.effects.semanticRegions.minZoomTier)) {
474
+ return;
475
+ }
476
+
477
+ const visibleByGroup = new Map<string, VisibleNodeSample[]>();
478
+ visibleNodes.forEach((sample) => {
479
+ const group = String(sample.attrs.semanticGroup || sample.attrs.nodeType || "entity");
480
+ const entry = visibleByGroup.get(group);
481
+ if (entry) {
482
+ entry.push(sample);
483
+ } else {
484
+ visibleByGroup.set(group, [sample]);
485
+ }
486
+ });
487
+
488
+ const summaries = analytics.semanticRegions.summaries
489
+ .slice(0, GRAPH_THEME.effects.semanticRegions.maxRegions);
490
+ const maxProminence = summaries.reduce((value, summary) => Math.max(value, summary.prominence), 1);
491
+ const semanticConfig = GRAPH_THEME.effects.semanticRegions;
492
+
493
+ summaries.forEach((summary) => {
494
+ const samples = visibleByGroup.get(summary.semanticGroup);
495
+ if (!samples || samples.length < semanticConfig.minVisibleSamples) {
496
+ return;
497
+ }
498
+
499
+ const densityField = renderDensityField(samples, context.canvas.width, context.canvas.height);
500
+ if (!densityField) {
501
+ return;
502
+ }
503
+
504
+ const prominenceRatio = 0.52 + (summary.prominence / maxProminence) * 0.48;
505
+ const densityThreshold = Math.max(18, densityField.maxAlpha * semanticConfig.densityThreshold);
506
+ const contourThreshold = Math.max(28, densityField.maxAlpha * semanticConfig.contourThreshold);
507
+ const outerContourThreshold = Math.max(18, contourThreshold * 0.72);
508
+
509
+ const densityMask = createThresholdMask(
510
+ densityField.alphaValues,
511
+ densityField.gridWidth,
512
+ densityField.gridHeight,
513
+ densityThreshold,
514
+ );
515
+ const dominantDensityMask = isolateDominantMask(
516
+ densityMask.mask,
517
+ densityField.gridWidth,
518
+ densityField.gridHeight,
519
+ densityField.alphaValues,
520
+ );
521
+ if (
522
+ dominantDensityMask.count < semanticConfig.minMaskPixels
523
+ || dominantDensityMask.occupancyRatio < semanticConfig.minOccupancyRatio
524
+ || dominantDensityMask.dominantMassRatio < semanticConfig.dominantMassRatio
525
+ ) {
526
+ return;
527
+ }
528
+
529
+ const contourMask = createThresholdMask(
530
+ densityField.alphaValues,
531
+ densityField.gridWidth,
532
+ densityField.gridHeight,
533
+ contourThreshold,
534
+ );
535
+ const outerContourMask = createThresholdMask(
536
+ densityField.alphaValues,
537
+ densityField.gridWidth,
538
+ densityField.gridHeight,
539
+ outerContourThreshold,
540
+ );
541
+ const dominantContourMask = isolateDominantMask(
542
+ contourMask.mask,
543
+ densityField.gridWidth,
544
+ densityField.gridHeight,
545
+ densityField.alphaValues,
546
+ );
547
+ const dominantOuterContourMask = isolateDominantMask(
548
+ outerContourMask.mask,
549
+ densityField.gridWidth,
550
+ densityField.gridHeight,
551
+ densityField.alphaValues,
552
+ );
553
+
554
+ const colors = buildRegionRenderColors(summary.color);
555
+ drawDensityFog(
556
+ context,
557
+ densityField,
558
+ colors.fogColor,
559
+ semanticConfig.fogAlpha * prominenceRatio,
560
+ );
561
+ if (
562
+ dominantOuterContourMask.count >= semanticConfig.outerContourMinMaskPixels
563
+ && dominantOuterContourMask.dominantMassRatio >= semanticConfig.dominantMassRatio
564
+ ) {
565
+ drawMaskContour(
566
+ context,
567
+ dominantOuterContourMask.mask,
568
+ densityField.gridWidth,
569
+ densityField.gridHeight,
570
+ colors.outerContourColor,
571
+ semanticConfig.outerContourAlpha * prominenceRatio,
572
+ );
573
+ }
574
+ drawMaskContour(
575
+ context,
576
+ dominantContourMask.mask,
577
+ densityField.gridWidth,
578
+ densityField.gridHeight,
579
+ colors.innerContourColor,
580
+ semanticConfig.innerContourAlpha * prominenceRatio,
581
+ );
582
+
583
+ if (summary.visibleNodeCount >= 18) {
584
+ const labelX = dominantDensityMask.centroid.x / densityField.scale;
585
+ const labelY = dominantDensityMask.centroid.y / densityField.scale - 18;
586
+ drawRegionLabel(
587
+ context,
588
+ labelX,
589
+ labelY,
590
+ summary.semanticGroup,
591
+ colors.labelBorderColor,
592
+ );
593
+ }
594
+ });
595
+ }
596
+
597
+ export function drawContourLayer(
598
+ context: CanvasRenderingContext2D,
599
+ analytics: GraphAnalyticsSnapshot | null,
600
+ visibleNodes: VisibleNodeSample[],
601
+ interactionState: GraphInteractionState,
602
+ effectsState: GraphEffectsState,
603
+ ) {
604
+ if (!effectsState.contoursEnabled || !analytics?.centrality.ready) {
605
+ return;
606
+ }
607
+
608
+ if (!zoomTierAtLeast(interactionState.zoomTier, GRAPH_THEME.effects.contours.minZoomTier)) {
609
+ return;
610
+ }
611
+
612
+ const visibleById = new Map(visibleNodes.map((sample) => [sample.nodeId, sample] as const));
613
+
614
+ analytics.centrality.topNodes.slice(0, GRAPH_THEME.effects.contours.maxContours).forEach((node) => {
615
+ const sample = visibleById.get(node.id);
616
+ if (!sample) {
617
+ return;
618
+ }
619
+
620
+ drawGlowHalo(
621
+ context,
622
+ sample.point.x,
623
+ sample.point.y,
624
+ GRAPH_THEME.effects.contours.baseRadius + sample.size * 3.2,
625
+ withAlpha(node.color, GRAPH_THEME.effects.contours.glowAlpha),
626
+ );
627
+ });
628
+ }
629
+
630
+ function isTemporalNodeActive(attrs: NodeAttributes, currentTime: Date | null) {
631
+ if (!currentTime || (!attrs.valid_from && !attrs.valid_until)) {
632
+ return false;
633
+ }
634
+
635
+ const time = currentTime.getTime();
636
+ const from = attrs.valid_from ? new Date(attrs.valid_from).getTime() : Number.NEGATIVE_INFINITY;
637
+ const until = attrs.valid_until ? new Date(attrs.valid_until).getTime() : Number.POSITIVE_INFINITY;
638
+ return time >= from && time <= until;
639
+ }
640
+
641
+ export function drawTemporalEmphasisLayer(
642
+ context: CanvasRenderingContext2D,
643
+ visibleNodes: VisibleNodeSample[],
644
+ temporalState: GraphTemporalState | null | undefined,
645
+ interactionState: GraphInteractionState,
646
+ effectsState: GraphEffectsState,
647
+ ) {
648
+ if (!effectsState.temporalEmphasisEnabled || !temporalState?.currentTime) {
649
+ return;
650
+ }
651
+
652
+ if (!zoomTierAtLeast(interactionState.zoomTier, GRAPH_THEME.effects.temporalEmphasis.minZoomTier)) {
653
+ return;
654
+ }
655
+
656
+ visibleNodes
657
+ .filter((sample) => isTemporalNodeActive(sample.attrs, temporalState.currentTime))
658
+ .slice(0, GRAPH_THEME.effects.temporalEmphasis.maxHighlights)
659
+ .forEach((sample) => {
660
+ drawGlowHalo(
661
+ context,
662
+ sample.point.x,
663
+ sample.point.y,
664
+ Math.max(sample.size * GRAPH_THEME.effects.temporalEmphasis.radiusMultiplier, 14),
665
+ withAlpha(GRAPH_THEME.palette.accent.temporal, GRAPH_THEME.effects.temporalEmphasis.glowAlpha),
666
+ );
667
+ });
668
+ }
669
+
670
+ export function drawLensLayer(
671
+ context: CanvasRenderingContext2D,
672
+ sigma: Sigma,
673
+ primaryNodeId: string,
674
+ focusIds: Set<string>,
675
+ ) {
676
+ const primaryData = sigma.getNodeDisplayData(primaryNodeId);
677
+ if (!primaryData) {
678
+ return;
679
+ }
680
+
681
+ const center = sigma.graphToViewport({ x: primaryData.x, y: primaryData.y });
682
+ drawGlowHalo(
683
+ context,
684
+ center.x,
685
+ center.y,
686
+ GRAPH_THEME.effects.lens.radius,
687
+ withAlpha(GRAPH_THEME.palette.accent.hovered, GRAPH_THEME.effects.lens.glowAlpha),
688
+ );
689
+
690
+ focusIds.forEach((neighborId) => {
691
+ if (neighborId === primaryNodeId || !graph.hasNode(neighborId)) {
692
+ return;
693
+ }
694
+
695
+ if (
696
+ !graph.hasDirectedEdge(primaryNodeId, neighborId)
697
+ && !graph.hasDirectedEdge(neighborId, primaryNodeId)
698
+ ) {
699
+ return;
700
+ }
701
+
702
+ const neighborData = sigma.getNodeDisplayData(neighborId);
703
+ if (!neighborData) {
704
+ return;
705
+ }
706
+
707
+ const neighborPoint = sigma.graphToViewport({ x: neighborData.x, y: neighborData.y });
708
+ context.strokeStyle = withAlpha(GRAPH_THEME.palette.accent.hovered, GRAPH_THEME.effects.lens.edgeAlpha * 0.38);
709
+ context.lineWidth = GRAPH_THEME.effects.lens.edgeLineWidth + 3.2;
710
+ context.lineCap = "round";
711
+ context.beginPath();
712
+ context.moveTo(center.x, center.y);
713
+ context.lineTo(neighborPoint.x, neighborPoint.y);
714
+ context.stroke();
715
+
716
+ context.strokeStyle = withAlpha(GRAPH_THEME.palette.accent.hovered, GRAPH_THEME.effects.lens.edgeAlpha);
717
+ context.lineWidth = GRAPH_THEME.effects.lens.edgeLineWidth;
718
+ context.beginPath();
719
+ context.moveTo(center.x, center.y);
720
+ context.lineTo(neighborPoint.x, neighborPoint.y);
721
+ context.stroke();
722
+ });
723
+ }
724
+
725
+ export function drawPathEffectsLayer(
726
+ context: CanvasRenderingContext2D,
727
+ segments: PathSegmentOverlay[],
728
+ effectsState: GraphEffectsState,
729
+ effectAvailability: {
730
+ pathPulse: { available: boolean };
731
+ pathFlow: { available: boolean };
732
+ },
733
+ now: number,
734
+ ) {
735
+ if (effectsState.pathFlowEnabled && effectAvailability.pathFlow.available) {
736
+ segments.forEach((segment, index) => {
737
+ const t = ((now * GRAPH_THEME.effects.pathFlow.speed) + index * GRAPH_THEME.effects.pathFlow.spacing) % 1;
738
+ const headX = segment.source.x + (segment.target.x - segment.source.x) * t;
739
+ const headY = segment.source.y + (segment.target.y - segment.source.y) * t;
740
+ const tailT = Math.max(0, t - 0.08);
741
+ const tailX = segment.source.x + (segment.target.x - segment.source.x) * tailT;
742
+ const tailY = segment.source.y + (segment.target.y - segment.source.y) * tailT;
743
+
744
+ context.strokeStyle = withAlpha(segment.color, 0.28);
745
+ context.lineWidth = Math.max(segment.size + 3.6, 4.6);
746
+ context.lineCap = "round";
747
+ context.beginPath();
748
+ context.moveTo(tailX, tailY);
749
+ context.lineTo(headX, headY);
750
+ context.stroke();
751
+
752
+ context.strokeStyle = withAlpha(GRAPH_THEME.palette.accent.selected, GRAPH_THEME.effects.pathFlow.opacity);
753
+ context.lineWidth = Math.max(segment.size + 1.3, 2.4);
754
+ context.beginPath();
755
+ context.moveTo(tailX, tailY);
756
+ context.lineTo(headX, headY);
757
+ context.stroke();
758
+ });
759
+ }
760
+
761
+ if (effectsState.pathPulseEnabled && effectAvailability.pathPulse.available) {
762
+ segments.forEach((segment, index) => {
763
+ const t = ((now * GRAPH_THEME.effects.pathPulse.speed) + index * 0.17) % 1;
764
+ const x = segment.source.x + (segment.target.x - segment.source.x) * t;
765
+ const y = segment.source.y + (segment.target.y - segment.source.y) * t;
766
+ const glow = context.createRadialGradient(x, y, 0, x, y, GRAPH_THEME.effects.pathPulse.radius);
767
+ glow.addColorStop(0, withAlpha(GRAPH_THEME.palette.accent.path, GRAPH_THEME.effects.pathPulse.glowAlpha));
768
+ glow.addColorStop(1, "rgba(0,0,0,0)");
769
+ context.fillStyle = glow;
770
+ context.beginPath();
771
+ context.arc(x, y, GRAPH_THEME.effects.pathPulse.radius, 0, Math.PI * 2);
772
+ context.fill();
773
+ });
774
+ }
775
+ }