@cosmos.gl/graph 2.3.1-beta.1 → 2.4.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/.eslintrc +61 -0
- package/CHARTER.md +69 -0
- package/GOVERNANCE.md +21 -0
- package/dist/index.d.ts +46 -15
- package/dist/index.js +2986 -2701
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +180 -62
- package/dist/index.min.js.map +1 -1
- package/dist/modules/GraphData/index.d.ts +18 -2
- package/dist/modules/Points/atlas-utils.d.ts +24 -0
- package/dist/modules/Points/index.d.ts +21 -2
- package/dist/modules/Store/index.d.ts +6 -1
- package/dist/stories/create-story.d.ts +5 -1
- package/dist/stories/shapes/image-example/index.d.ts +5 -0
- package/dist/stories/shapes.stories.d.ts +1 -0
- package/package.json +4 -4
- package/src/config.ts +1 -0
- package/src/declaration.d.ts +5 -0
- package/src/index.ts +119 -67
- package/src/modules/GraphData/index.ts +68 -6
- package/src/modules/Points/atlas-utils.ts +137 -0
- package/src/modules/Points/draw-highlighted.vert +3 -3
- package/src/modules/Points/draw-points.frag +106 -14
- package/src/modules/Points/draw-points.vert +51 -25
- package/src/modules/Points/find-points-on-area-selection.frag +6 -5
- package/src/modules/Points/index.ts +121 -13
- package/src/modules/Store/index.ts +11 -3
- package/src/stories/3. api-reference.mdx +48 -1
- package/src/stories/create-story.ts +32 -5
- package/src/stories/shapes/image-example/icons/box.png +0 -0
- package/src/stories/shapes/image-example/icons/lego.png +0 -0
- package/src/stories/shapes/image-example/icons/s.png +0 -0
- package/src/stories/shapes/image-example/icons/swift.png +0 -0
- package/src/stories/shapes/image-example/icons/toolbox.png +0 -0
- package/src/stories/shapes/image-example/index.ts +239 -0
- package/src/stories/shapes.stories.ts +12 -0
|
@@ -20,6 +20,7 @@ import dragPointFrag from '@/graph/modules/Points/drag-point.frag'
|
|
|
20
20
|
import updateVert from '@/graph/modules/Shared/quad.vert'
|
|
21
21
|
import clearFrag from '@/graph/modules/Shared/clear.frag'
|
|
22
22
|
import { readPixels } from '@/graph/helper'
|
|
23
|
+
import { createAtlasDataFromImageData } from '@/graph/modules/Points/atlas-utils'
|
|
23
24
|
|
|
24
25
|
export class Points extends CoreModule {
|
|
25
26
|
public currentPositionFbo: regl.Framebuffer2D | undefined
|
|
@@ -30,14 +31,22 @@ export class Points extends CoreModule {
|
|
|
30
31
|
public greyoutStatusFbo: regl.Framebuffer2D | undefined
|
|
31
32
|
public scaleX: ((x: number) => number) | undefined
|
|
32
33
|
public scaleY: ((y: number) => number) | undefined
|
|
33
|
-
public
|
|
34
|
+
public shouldSkipRescale: boolean | undefined
|
|
35
|
+
public imageAtlasTexture: regl.Texture2D | undefined
|
|
36
|
+
public imageCount = 0
|
|
34
37
|
private colorBuffer: regl.Buffer | undefined
|
|
35
38
|
private sizeFbo: regl.Framebuffer2D | undefined
|
|
36
39
|
private sizeBuffer: regl.Buffer | undefined
|
|
37
40
|
private shapeBuffer: regl.Buffer | undefined
|
|
41
|
+
private imageIndicesBuffer: regl.Buffer | undefined
|
|
42
|
+
private imageSizesBuffer: regl.Buffer | undefined
|
|
43
|
+
private imageAtlasCoordsTexture: regl.Texture2D | undefined
|
|
44
|
+
private imageAtlasCoordsTextureSize: number | undefined
|
|
38
45
|
private trackedIndicesFbo: regl.Framebuffer2D | undefined
|
|
39
46
|
private trackedPositionsFbo: regl.Framebuffer2D | undefined
|
|
40
47
|
private sampledPointsFbo: regl.Framebuffer2D | undefined
|
|
48
|
+
private trackedPositions: Map<number, [number, number]> | undefined
|
|
49
|
+
private isPositionsUpToDate = false
|
|
41
50
|
private drawCommand: regl.DrawCommand | undefined
|
|
42
51
|
private drawHighlightedCommand: regl.DrawCommand | undefined
|
|
43
52
|
private updatePositionCommand: regl.DrawCommand | undefined
|
|
@@ -72,21 +81,21 @@ export class Points extends CoreModule {
|
|
|
72
81
|
let shouldRescale = rescalePositions
|
|
73
82
|
// If rescalePositions isn't specified in config and simulation is disabled, default to true
|
|
74
83
|
if (rescalePositions === undefined && !enableSimulation) shouldRescale = true
|
|
75
|
-
// Skip rescaling if `
|
|
84
|
+
// Skip rescaling if `shouldSkipRescale` flag is set (allowing one-time skip of rescaling)
|
|
76
85
|
// Temporary flag is used to skip rescaling when change point positions or adding new points by function `setPointPositions`
|
|
77
86
|
// This flag overrides any other rescaling settings
|
|
78
|
-
if (this.
|
|
87
|
+
if (this.shouldSkipRescale) shouldRescale = false
|
|
79
88
|
|
|
80
89
|
if (shouldRescale) {
|
|
81
90
|
this.rescaleInitialNodePositions()
|
|
82
|
-
} else if (!this.
|
|
91
|
+
} else if (!this.shouldSkipRescale) {
|
|
83
92
|
// Only reset scale functions if not temporarily skipping rescale
|
|
84
93
|
this.scaleX = undefined
|
|
85
94
|
this.scaleY = undefined
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
// Reset temporary flag
|
|
89
|
-
this.
|
|
98
|
+
this.shouldSkipRescale = undefined
|
|
90
99
|
|
|
91
100
|
for (let i = 0; i < data.pointsNumber; ++i) {
|
|
92
101
|
initialState[i * 4 + 0] = data.pointPositions[i * 2 + 0] as number
|
|
@@ -227,6 +236,14 @@ export class Points extends CoreModule {
|
|
|
227
236
|
buffer: () => this.shapeBuffer,
|
|
228
237
|
size: 1,
|
|
229
238
|
},
|
|
239
|
+
imageIndex: {
|
|
240
|
+
buffer: () => this.imageIndicesBuffer,
|
|
241
|
+
size: 1,
|
|
242
|
+
},
|
|
243
|
+
imageSize: {
|
|
244
|
+
buffer: () => this.imageSizesBuffer,
|
|
245
|
+
size: 1,
|
|
246
|
+
},
|
|
230
247
|
},
|
|
231
248
|
uniforms: {
|
|
232
249
|
positionsTexture: () => this.currentPositionFbo,
|
|
@@ -241,11 +258,16 @@ export class Points extends CoreModule {
|
|
|
241
258
|
greyoutOpacity: () => config.pointGreyoutOpacity ?? -1,
|
|
242
259
|
greyoutColor: () => store.greyoutPointColor,
|
|
243
260
|
backgroundColor: () => store.backgroundColor,
|
|
244
|
-
|
|
261
|
+
isDarkenGreyout: () => store.isDarkenGreyout,
|
|
245
262
|
scalePointsOnZoom: () => config.scalePointsOnZoom,
|
|
246
263
|
maxPointSize: () => store.maxPointSize,
|
|
247
264
|
skipSelected: reglInstance.prop<{ skipSelected: boolean }, 'skipSelected'>('skipSelected'),
|
|
248
265
|
skipUnselected: reglInstance.prop<{ skipUnselected: boolean }, 'skipUnselected'>('skipUnselected'),
|
|
266
|
+
imageAtlasTexture: () => this.imageAtlasTexture,
|
|
267
|
+
imageAtlasCoords: () => this.imageAtlasCoordsTexture,
|
|
268
|
+
hasImages: () => this.imageCount > 0,
|
|
269
|
+
imageCount: () => this.imageCount,
|
|
270
|
+
imageAtlasCoordsTextureSize: () => this.imageAtlasCoordsTextureSize,
|
|
249
271
|
},
|
|
250
272
|
blend: {
|
|
251
273
|
enable: true,
|
|
@@ -285,8 +307,8 @@ export class Points extends CoreModule {
|
|
|
285
307
|
sizeScale: () => config.pointSizeScale,
|
|
286
308
|
transformationMatrix: () => store.transform,
|
|
287
309
|
ratio: () => config.pixelRatio,
|
|
288
|
-
|
|
289
|
-
|
|
310
|
+
selection0: () => store.selectedArea[0],
|
|
311
|
+
selection1: () => store.selectedArea[1],
|
|
290
312
|
scalePointsOnZoom: () => config.scalePointsOnZoom,
|
|
291
313
|
maxPointSize: () => store.maxPointSize,
|
|
292
314
|
},
|
|
@@ -422,7 +444,7 @@ export class Points extends CoreModule {
|
|
|
422
444
|
pointGreyoutStatusTexture: () => this.greyoutStatusFbo,
|
|
423
445
|
universalPointOpacity: () => config.pointOpacity,
|
|
424
446
|
greyoutOpacity: () => config.pointGreyoutOpacity ?? -1,
|
|
425
|
-
|
|
447
|
+
isDarkenGreyout: () => store.isDarkenGreyout,
|
|
426
448
|
backgroundColor: () => store.backgroundColor,
|
|
427
449
|
greyoutColor: () => store.greyoutPointColor,
|
|
428
450
|
},
|
|
@@ -532,6 +554,54 @@ export class Points extends CoreModule {
|
|
|
532
554
|
this.shapeBuffer(data.pointShapes)
|
|
533
555
|
}
|
|
534
556
|
|
|
557
|
+
public updateImageIndices (): void {
|
|
558
|
+
const { reglInstance, data } = this
|
|
559
|
+
if (data.pointsNumber === undefined || data.pointImageIndices === undefined) return
|
|
560
|
+
if (!this.imageIndicesBuffer) this.imageIndicesBuffer = reglInstance.buffer(0)
|
|
561
|
+
this.imageIndicesBuffer(data.pointImageIndices)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
public updateImageSizes (): void {
|
|
565
|
+
const { reglInstance, data } = this
|
|
566
|
+
if (data.pointsNumber === undefined || data.pointImageSizes === undefined) return
|
|
567
|
+
if (!this.imageSizesBuffer) this.imageSizesBuffer = reglInstance.buffer(0)
|
|
568
|
+
this.imageSizesBuffer(data.pointImageSizes)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
public createAtlas (): void {
|
|
572
|
+
const { reglInstance, data, store } = this
|
|
573
|
+
if (!this.imageAtlasTexture) this.imageAtlasTexture = reglInstance.texture()
|
|
574
|
+
if (!this.imageAtlasCoordsTexture) this.imageAtlasCoordsTexture = reglInstance.texture()
|
|
575
|
+
|
|
576
|
+
if (!data.inputImageData?.length) {
|
|
577
|
+
this.imageCount = 0
|
|
578
|
+
this.imageAtlasCoordsTextureSize = 0
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const atlasResult = createAtlasDataFromImageData(data.inputImageData, store.webglMaxTextureSize)
|
|
583
|
+
if (!atlasResult) {
|
|
584
|
+
console.warn('Failed to create atlas from image data')
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
this.imageCount = data.inputImageData.length
|
|
589
|
+
const { atlasData, atlasSize, atlasCoords, atlasCoordsSize } = atlasResult
|
|
590
|
+
this.imageAtlasCoordsTextureSize = atlasCoordsSize
|
|
591
|
+
|
|
592
|
+
this.imageAtlasTexture({
|
|
593
|
+
data: atlasData,
|
|
594
|
+
shape: [atlasSize, atlasSize, 4],
|
|
595
|
+
type: 'uint8',
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
this.imageAtlasCoordsTexture({
|
|
599
|
+
data: atlasCoords,
|
|
600
|
+
shape: [atlasCoordsSize, atlasCoordsSize, 4],
|
|
601
|
+
type: 'float',
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
|
|
535
605
|
public updateSampledPointsGrid (): void {
|
|
536
606
|
const { store: { screenSize }, config: { pointSamplingDistance }, reglInstance } = this
|
|
537
607
|
let dist = pointSamplingDistance ?? Math.min(...screenSize) / 2
|
|
@@ -557,6 +627,9 @@ export class Points extends CoreModule {
|
|
|
557
627
|
if (!this.colorBuffer) this.updateColor()
|
|
558
628
|
if (!this.sizeBuffer) this.updateSize()
|
|
559
629
|
if (!this.shapeBuffer) this.updateShape()
|
|
630
|
+
if (!this.imageIndicesBuffer) this.updateImageIndices()
|
|
631
|
+
if (!this.imageSizesBuffer) this.updateImageSizes()
|
|
632
|
+
if (!this.imageAtlasCoordsTexture || !this.imageAtlasTexture) this.createAtlas()
|
|
560
633
|
|
|
561
634
|
// Render in layers: unselected points first (behind), then selected points (in front)
|
|
562
635
|
if (store.selectedIndices && store.selectedIndices.length > 0) {
|
|
@@ -589,11 +662,15 @@ export class Points extends CoreModule {
|
|
|
589
662
|
public updatePosition (): void {
|
|
590
663
|
this.updatePositionCommand?.()
|
|
591
664
|
this.swapFbo()
|
|
665
|
+
// Invalidate tracked positions cache since positions have changed
|
|
666
|
+
this.isPositionsUpToDate = false
|
|
592
667
|
}
|
|
593
668
|
|
|
594
669
|
public drag (): void {
|
|
595
670
|
this.dragPointCommand?.()
|
|
596
671
|
this.swapFbo()
|
|
672
|
+
// Invalidate tracked positions cache since positions have changed
|
|
673
|
+
this.isPositionsUpToDate = false
|
|
597
674
|
}
|
|
598
675
|
|
|
599
676
|
public findPointsOnAreaSelection (): void {
|
|
@@ -651,7 +728,12 @@ export class Points extends CoreModule {
|
|
|
651
728
|
public trackPointsByIndices (indices?: number[] | undefined): void {
|
|
652
729
|
const { store: { pointsTextureSize }, reglInstance } = this
|
|
653
730
|
this.trackedIndices = indices
|
|
654
|
-
|
|
731
|
+
|
|
732
|
+
// Clear cache when changing tracked indices
|
|
733
|
+
this.trackedPositions = undefined
|
|
734
|
+
this.isPositionsUpToDate = false
|
|
735
|
+
|
|
736
|
+
if (!indices?.length || !pointsTextureSize) return
|
|
655
737
|
const textureSize = Math.ceil(Math.sqrt(indices.length))
|
|
656
738
|
|
|
657
739
|
const initialState = new Float32Array(textureSize * textureSize * 4).fill(-1)
|
|
@@ -688,10 +770,29 @@ export class Points extends CoreModule {
|
|
|
688
770
|
this.trackPoints()
|
|
689
771
|
}
|
|
690
772
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
773
|
+
/**
|
|
774
|
+
* Get current X and Y coordinates of the tracked points.
|
|
775
|
+
*
|
|
776
|
+
* When the simulation is disabled or stopped, this method returns a cached
|
|
777
|
+
* result to avoid expensive GPU-to-CPU memory transfers (`readPixels`).
|
|
778
|
+
*
|
|
779
|
+
* @returns A ReadonlyMap where keys are point indices and values are [x, y] coordinates.
|
|
780
|
+
*/
|
|
781
|
+
public getTrackedPositionsMap (): ReadonlyMap<number, [number, number]> {
|
|
782
|
+
if (!this.trackedIndices) return new Map()
|
|
783
|
+
|
|
784
|
+
const { config: { enableSimulation }, store: { isSimulationRunning } } = this
|
|
785
|
+
|
|
786
|
+
// Use cached positions when simulation is inactive and cache is valid
|
|
787
|
+
if ((!enableSimulation || !isSimulationRunning) &&
|
|
788
|
+
this.isPositionsUpToDate &&
|
|
789
|
+
this.trackedPositions) {
|
|
790
|
+
return this.trackedPositions
|
|
791
|
+
}
|
|
792
|
+
|
|
694
793
|
const pixels = readPixels(this.reglInstance, this.trackedPositionsFbo as regl.Framebuffer2D)
|
|
794
|
+
|
|
795
|
+
const tracked = new Map<number, [number, number]>()
|
|
695
796
|
for (let i = 0; i < pixels.length / 4; i += 1) {
|
|
696
797
|
const x = pixels[i * 4]
|
|
697
798
|
const y = pixels[i * 4 + 1]
|
|
@@ -700,6 +801,13 @@ export class Points extends CoreModule {
|
|
|
700
801
|
tracked.set(index, [x, y])
|
|
701
802
|
}
|
|
702
803
|
}
|
|
804
|
+
|
|
805
|
+
// If simulation is inactive, cache the result for next time
|
|
806
|
+
if (!enableSimulation || !isSimulationRunning) {
|
|
807
|
+
this.trackedPositions = tracked
|
|
808
|
+
this.isPositionsUpToDate = true
|
|
809
|
+
}
|
|
810
|
+
|
|
703
811
|
return tracked
|
|
704
812
|
}
|
|
705
813
|
|
|
@@ -29,13 +29,14 @@ export class Store {
|
|
|
29
29
|
public adjustedSpaceSize = defaultConfigValues.spaceSize
|
|
30
30
|
public isSpaceKeyPressed = false
|
|
31
31
|
public div: HTMLDivElement | undefined
|
|
32
|
+
public webglMaxTextureSize = 16384 // Default fallback value
|
|
32
33
|
|
|
33
34
|
public hoveredPointRingColor = [1, 1, 1, hoveredPointRingOpacity]
|
|
34
35
|
public focusedPointRingColor = [1, 1, 1, focusedPointRingOpacity]
|
|
35
36
|
// -1 means that the color is not set
|
|
36
37
|
public greyoutPointColor = [-1, -1, -1, -1]
|
|
37
|
-
// If backgroundColor is dark,
|
|
38
|
-
public
|
|
38
|
+
// If backgroundColor is dark, isDarkenGreyout is true
|
|
39
|
+
public isDarkenGreyout = false
|
|
39
40
|
private alphaTarget = 0
|
|
40
41
|
private scalePointX = scaleLinear()
|
|
41
42
|
private scalePointY = scaleLinear()
|
|
@@ -53,7 +54,7 @@ export class Store {
|
|
|
53
54
|
document.documentElement.style.setProperty('--cosmosgl-error-message-color', brightness > 0.65 ? 'black' : 'white')
|
|
54
55
|
if (this.div) this.div.style.backgroundColor = `rgba(${color[0] * 255}, ${color[1] * 255}, ${color[2] * 255}, ${color[3]})`
|
|
55
56
|
|
|
56
|
-
this.
|
|
57
|
+
this.isDarkenGreyout = brightness < 0.65
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
public addRandomSeed (seed: number | string): void {
|
|
@@ -75,6 +76,13 @@ export class Store {
|
|
|
75
76
|
} else this.adjustedSpaceSize = configSpaceSize
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Sets the WebGL texture size limit for use in atlas creation and other texture operations.
|
|
81
|
+
*/
|
|
82
|
+
public setWebGLMaxTextureSize (webglMaxTextureSize: number): void {
|
|
83
|
+
this.webglMaxTextureSize = webglMaxTextureSize
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
public updateScreenSize (width: number, height: number): void {
|
|
79
87
|
const { adjustedSpaceSize } = this
|
|
80
88
|
this.screenSize = [width, height]
|
|
@@ -91,8 +91,11 @@ This method sets the shapes for the graph points.
|
|
|
91
91
|
| 5 | Hexagon |
|
|
92
92
|
| 6 | Star |
|
|
93
93
|
| 7 | Cross |
|
|
94
|
+
| 8 | None |
|
|
94
95
|
|
|
95
|
-
Each shape value in the array specifies the shape of a point using the same order as the points in the graph data. Invalid shape values (outside the range 0-
|
|
96
|
+
Each shape value in the array specifies the shape of a point using the same order as the points in the graph data. Invalid shape values (outside the range 0-8) will default to Circle (0).
|
|
97
|
+
|
|
98
|
+
Images are rendered above shapes.
|
|
96
99
|
|
|
97
100
|
**Example:**
|
|
98
101
|
```javascript
|
|
@@ -108,6 +111,50 @@ graph.setPointShapes(new Float32Array([
|
|
|
108
111
|
graph.render();
|
|
109
112
|
```
|
|
110
113
|
|
|
114
|
+
### <a name="set_image_data" href="#set_image_data">#</a> graph.<b>setImageData</b>(<i>imageDataArray</i>)
|
|
115
|
+
|
|
116
|
+
This method sets the images for the graph points using [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) objects. Images are rendered above shapes. To use images, provide image indices via `setPointImageIndices()`.
|
|
117
|
+
|
|
118
|
+
Images are rendered above shapes.
|
|
119
|
+
|
|
120
|
+
* **`imageDataArray`** (ImageData[]): Array of ImageData objects to use as point images.
|
|
121
|
+
|
|
122
|
+
**Example:**
|
|
123
|
+
```javascript
|
|
124
|
+
// Create ImageData objects from canvas or other sources
|
|
125
|
+
const imageData1 = canvas1.getContext('2d').getImageData(0, 0, 32, 32);
|
|
126
|
+
const imageData2 = canvas2.getContext('2d').getImageData(0, 0, 32, 32);
|
|
127
|
+
|
|
128
|
+
// Set the images for the graph
|
|
129
|
+
graph.setImageData([imageData1, imageData2]);
|
|
130
|
+
|
|
131
|
+
// Set which image each point should use (0 = imageData1, 1 = imageData2)
|
|
132
|
+
graph.setPointImageIndices(new Float32Array([0, 1, 0, 1]));
|
|
133
|
+
|
|
134
|
+
// To show shapes with images
|
|
135
|
+
graph.setPointShapes(new Float32Array([0, 1, 2, 3])); // Circle, Square, Triangle, Diamond
|
|
136
|
+
|
|
137
|
+
graph.render();
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This example sets up two images and assigns them to four points: the first and third points use imageData1, while the second and fourth points use imageData2. The shapes are also set to show both shapes and images together.
|
|
141
|
+
|
|
142
|
+
### <a name="set_point_image_indices" href="#set_point_image_indices">#</a> graph.<b>setPointImageIndices</b>(<i>imageIndices</i>)
|
|
143
|
+
|
|
144
|
+
This method sets which image each point should use from the images array provided to `setImageData()`. Images are rendered above shapes.
|
|
145
|
+
|
|
146
|
+
* **`imageIndices`** (Float32Array): A Float32Array representing which image each point uses in the format `[index1, index2, ..., indexN]`, where each value is an index into the images array provided to `setImageData()`.
|
|
147
|
+
* **Valid indices**: Use 0 for the first image, 1 for the second image, etc. Invalid or negative indices will default to -1 (no image).
|
|
148
|
+
* **Default behavior**: When no image indices are provided, all points default to -1 (no image).
|
|
149
|
+
|
|
150
|
+
### <a name="set_point_image_sizes" href="#set_point_image_sizes">#</a> graph.<b>setPointImageSizes</b>(<i>imageSizes</i>)
|
|
151
|
+
|
|
152
|
+
This method sets the sizes for the point images in the graph.
|
|
153
|
+
|
|
154
|
+
* **`imageSizes`** (Float32Array): A Float32Array representing the sizes of point images in the format `[size1, size2, ..., sizeN]`, where each `size` value corresponds to the size of the image for the point at the same index in the graph's data.
|
|
155
|
+
|
|
156
|
+
Each size value in the array specifies the size of a point image using the same order as the points in the graph data. The sizes are applied to images that have been set using `setImageData()` and referenced via `setPointImageIndices()`.
|
|
157
|
+
|
|
111
158
|
### <a name="set_links" href="#set_links">#</a> graph.<b>setLinks</b>(<i>links</i>)
|
|
112
159
|
|
|
113
160
|
This method sets the links (connections) between points in a cosmos.gl graph.
|
|
@@ -8,7 +8,11 @@ export const createStory: (storyFunction: () => {
|
|
|
8
8
|
graph: Graph;
|
|
9
9
|
div: HTMLDivElement;
|
|
10
10
|
destroy?: () => void;
|
|
11
|
-
}
|
|
11
|
+
} | Promise<{
|
|
12
|
+
graph: Graph;
|
|
13
|
+
div: HTMLDivElement;
|
|
14
|
+
destroy?: () => void;
|
|
15
|
+
}>) => Story = (storyFunction) => ({
|
|
12
16
|
async beforeEach (d): Promise<() => void> {
|
|
13
17
|
return (): void => {
|
|
14
18
|
d.args.destroy?.()
|
|
@@ -16,9 +20,32 @@ export const createStory: (storyFunction: () => {
|
|
|
16
20
|
}
|
|
17
21
|
},
|
|
18
22
|
render: (args): HTMLDivElement => {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const result = storyFunction()
|
|
24
|
+
|
|
25
|
+
if (result instanceof Promise) {
|
|
26
|
+
// For async story functions, create a simple div and update it when ready
|
|
27
|
+
const div = document.createElement('div')
|
|
28
|
+
div.style.height = '100vh'
|
|
29
|
+
div.style.width = '100%'
|
|
30
|
+
div.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #666;">Loading story...</div>'
|
|
31
|
+
|
|
32
|
+
result.then((story) => {
|
|
33
|
+
args.graph = story.graph
|
|
34
|
+
args.destroy = story.destroy
|
|
35
|
+
// Replace the content with the actual story div
|
|
36
|
+
div.innerHTML = ''
|
|
37
|
+
div.appendChild(story.div)
|
|
38
|
+
}).catch((error) => {
|
|
39
|
+
console.error('Failed to load story:', error)
|
|
40
|
+
div.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #ff0000;">Failed to load story</div>'
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return div
|
|
44
|
+
} else {
|
|
45
|
+
// Synchronous story function
|
|
46
|
+
args.graph = result.graph
|
|
47
|
+
args.destroy = result.destroy
|
|
48
|
+
return result.div
|
|
49
|
+
}
|
|
23
50
|
},
|
|
24
51
|
})
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { Graph, PointShape } from '@cosmos.gl/graph'
|
|
2
|
+
|
|
3
|
+
// Import all PNG icons
|
|
4
|
+
import boxUrl from './icons/box.png'
|
|
5
|
+
import toolboxUrl from './icons/toolbox.png'
|
|
6
|
+
import swiftUrl from './icons/swift.png'
|
|
7
|
+
import legoUrl from './icons/lego.png'
|
|
8
|
+
import sUrl from './icons/s.png'
|
|
9
|
+
|
|
10
|
+
// Helper function to convert PNG URL to ImageData
|
|
11
|
+
const pngUrlToImageData = (pngUrl: string): Promise<ImageData> => {
|
|
12
|
+
return new Promise<ImageData>((resolve, reject) => {
|
|
13
|
+
const img = new Image()
|
|
14
|
+
|
|
15
|
+
img.onload = (): void => {
|
|
16
|
+
const canvas = document.createElement('canvas')
|
|
17
|
+
const ctx = canvas.getContext('2d')
|
|
18
|
+
if (!ctx) {
|
|
19
|
+
reject(new Error('Could not get 2D context'))
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
canvas.width = img.width
|
|
24
|
+
canvas.height = img.height
|
|
25
|
+
ctx.drawImage(img, 0, 0)
|
|
26
|
+
|
|
27
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
28
|
+
resolve(imageData)
|
|
29
|
+
}
|
|
30
|
+
img.onerror = (): void => reject(new Error(`Failed to load image: ${pngUrl}`))
|
|
31
|
+
img.src = pngUrl
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const loadPngImages = (pngUrls: string[]): Promise<ImageData[]> => {
|
|
36
|
+
return Promise.all(pngUrls.map(pngUrlToImageData))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Define node types for Xcode dependency graph
|
|
40
|
+
enum NodeType {
|
|
41
|
+
App = 0, // Main app target (swift icon)
|
|
42
|
+
Framework = 1, // Framework (box icon)
|
|
43
|
+
Library = 2, // Static library (toolbox icon)
|
|
44
|
+
Bundle = 3, // Bundle (lego icon)
|
|
45
|
+
Target = 4 // Build target (s icon)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface DependencyNode {
|
|
49
|
+
id: number;
|
|
50
|
+
name: string;
|
|
51
|
+
type: NodeType;
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
dependencies: number[];
|
|
55
|
+
size: number;
|
|
56
|
+
color: [number, number, number, number];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const imageExample = async (): Promise<{div: HTMLDivElement; graph: Graph }> => {
|
|
60
|
+
// Create container div
|
|
61
|
+
const div = document.createElement('div')
|
|
62
|
+
div.style.height = '100vh'
|
|
63
|
+
div.style.width = '100%'
|
|
64
|
+
div.style.display = 'flex'
|
|
65
|
+
div.style.flexDirection = 'column'
|
|
66
|
+
|
|
67
|
+
// Create main graph container
|
|
68
|
+
const graphContainer = document.createElement('div')
|
|
69
|
+
graphContainer.style.height = '100vh'
|
|
70
|
+
graphContainer.style.width = '100%'
|
|
71
|
+
graphContainer.style.position = 'absolute'
|
|
72
|
+
graphContainer.style.overflow = 'hidden'
|
|
73
|
+
div.appendChild(graphContainer)
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const spaceSize = 4096
|
|
77
|
+
|
|
78
|
+
const nodes: DependencyNode[] = [
|
|
79
|
+
// Main app target (center)
|
|
80
|
+
{ id: 0, name: 'MyApp', type: NodeType.App, x: 2048, y: 2048, dependencies: [1, 2, 3, 14], size: 60, color: [0.2, 0.6, 1.0, 1.0] },
|
|
81
|
+
|
|
82
|
+
// Frameworks (first ring around center)
|
|
83
|
+
{ id: 1, name: 'CoreData', type: NodeType.Framework, x: 1024, y: 2048, dependencies: [4, 5], size: 50, color: [0.8, 0.4, 0.2, 1.0] },
|
|
84
|
+
{ id: 2, name: 'UIKit', type: NodeType.Framework, x: 2048, y: 1024, dependencies: [6, 15], size: 50, color: [0.8, 0.4, 0.2, 1.0] },
|
|
85
|
+
{ id: 3, name: 'Network', type: NodeType.Framework, x: 3072, y: 2048, dependencies: [7, 8], size: 50, color: [0.8, 0.4, 0.2, 1.0] },
|
|
86
|
+
|
|
87
|
+
// Libraries (second ring)
|
|
88
|
+
{ id: 4, name: 'SQLite', type: NodeType.Library, x: 512, y: 2048, dependencies: [], size: 45, color: [0.6, 0.8, 0.4, 1.0] },
|
|
89
|
+
{ id: 5, name: 'Foundation', type: NodeType.Library, x: 1024, y: 1024, dependencies: [16], size: 45, color: [0.6, 0.8, 0.4, 1.0] },
|
|
90
|
+
{ id: 6, name: 'CoreGraphics', type: NodeType.Library, x: 2048, y: 512, dependencies: [], size: 45, color: [0.6, 0.8, 0.4, 1.0] },
|
|
91
|
+
{ id: 7, name: 'Security', type: NodeType.Library, x: 3072, y: 1024, dependencies: [], size: 45, color: [0.6, 0.8, 0.4, 1.0] },
|
|
92
|
+
{ id: 8, name: 'CFNetwork', type: NodeType.Library, x: 3584, y: 2048, dependencies: [], size: 45, color: [0.6, 0.8, 0.4, 1.0] },
|
|
93
|
+
|
|
94
|
+
// Additional frameworks (first ring)
|
|
95
|
+
{ id: 9, name: 'Analytics', type: NodeType.Framework, x: 2048, y: 3072, dependencies: [10, 17], size: 50, color: [0.8, 0.4, 0.2, 1.0] },
|
|
96
|
+
{ id: 10, name: 'Firebase', type: NodeType.Library, x: 2048, y: 3840, dependencies: [], size: 45, color: [0.6, 0.8, 0.4, 1.0] },
|
|
97
|
+
|
|
98
|
+
// Test targets (outer ring)
|
|
99
|
+
{ id: 11, name: 'Tests', type: NodeType.Target, x: 512, y: 1024, dependencies: [0], size: 50, color: [0.4, 0.6, 1.0, 1.0] },
|
|
100
|
+
{ id: 12, name: 'UITests', type: NodeType.Target, x: 3584, y: 1024, dependencies: [0, 2], size: 45, color: [0.4, 0.6, 1.0, 1.0] },
|
|
101
|
+
{ id: 13, name: 'Widget', type: NodeType.Target, x: 3584, y: 3072, dependencies: [1, 2], size: 45, color: [0.4, 0.6, 1.0, 1.0] },
|
|
102
|
+
|
|
103
|
+
// Additional components
|
|
104
|
+
{ id: 14, name: 'Localization', type: NodeType.Framework, x: 1536, y: 3072, dependencies: [18], size: 50, color: [0.8, 0.4, 0.2, 1.0] },
|
|
105
|
+
{ id: 15, name: 'CoreAnimation', type: NodeType.Library, x: 2560, y: 512, dependencies: [], size: 45, color: [0.6, 0.8, 0.4, 1.0] },
|
|
106
|
+
{ id: 16, name: 'CoreFoundation', type: NodeType.Library, x: 1024, y: 512, dependencies: [], size: 45, color: [0.6, 0.8, 0.4, 1.0] },
|
|
107
|
+
{ id: 17, name: 'Crashlytics', type: NodeType.Library, x: 1536, y: 3584, dependencies: [], size: 45, color: [0.6, 0.8, 0.4, 1.0] },
|
|
108
|
+
{ id: 18, name: 'LocalizationBundle', type: NodeType.Bundle, x: 1792, y: 3584, dependencies: [], size: 45, color: [1.0, 0.4, 1.0, 1.0] },
|
|
109
|
+
|
|
110
|
+
// More test targets
|
|
111
|
+
{ id: 19, name: 'UnitTests', type: NodeType.Target, x: 512, y: 3072, dependencies: [0, 1], size: 45, color: [0.4, 0.6, 1.0, 1.0] },
|
|
112
|
+
{ id: 20, name: 'IntegrationTests', type: NodeType.Target, x: 512, y: 3584, dependencies: [0, 3], size: 45, color: [0.4, 0.6, 1.0, 1.0] },
|
|
113
|
+
|
|
114
|
+
// Bundle resources
|
|
115
|
+
{ id: 21, name: 'Resources', type: NodeType.Bundle, x: 2304, y: 3072, dependencies: [0], size: 50, color: [1.0, 0.4, 1.0, 1.0] },
|
|
116
|
+
{ id: 22, name: 'Assets', type: NodeType.Bundle, x: 2560, y: 3584, dependencies: [0, 21], size: 50, color: [1.0, 0.4, 1.0, 1.0] },
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
const pointCount = nodes.length
|
|
120
|
+
const pointPositions = new Float32Array(pointCount * 2)
|
|
121
|
+
const pointColors = new Float32Array(pointCount * 4)
|
|
122
|
+
const pointShapes = new Float32Array(pointCount)
|
|
123
|
+
const pointSizes = new Float32Array(pointCount)
|
|
124
|
+
const imageIndices = new Float32Array(pointCount)
|
|
125
|
+
|
|
126
|
+
// Create links array for dependencies
|
|
127
|
+
const links: number[] = []
|
|
128
|
+
const linkArrows: boolean[] = []
|
|
129
|
+
const linkColors: number[] = []
|
|
130
|
+
|
|
131
|
+
// Set up nodes based on the dependency structure
|
|
132
|
+
for (const node of nodes) {
|
|
133
|
+
const i = node.id
|
|
134
|
+
|
|
135
|
+
// Set positions
|
|
136
|
+
pointPositions[i * 2] = node.x
|
|
137
|
+
pointPositions[i * 2 + 1] = node.y
|
|
138
|
+
|
|
139
|
+
// Set node properties - use None shape for all images except targets (s icon)
|
|
140
|
+
pointShapes[i] = node.type === NodeType.Target ? PointShape.Hexagon : PointShape.None
|
|
141
|
+
pointSizes[i] = node.size
|
|
142
|
+
imageIndices[i] = node.type
|
|
143
|
+
|
|
144
|
+
// Set colors
|
|
145
|
+
pointColors[i * 4] = node.color[0]
|
|
146
|
+
pointColors[i * 4 + 1] = node.color[1]
|
|
147
|
+
pointColors[i * 4 + 2] = node.color[2]
|
|
148
|
+
pointColors[i * 4 + 3] = node.color[3]
|
|
149
|
+
|
|
150
|
+
// Add dependency links
|
|
151
|
+
for (const depId of node.dependencies) {
|
|
152
|
+
links.push(i, depId)
|
|
153
|
+
linkArrows.push(true)
|
|
154
|
+
|
|
155
|
+
// Add colorful link colors based on source node type
|
|
156
|
+
const sourceType = node.type
|
|
157
|
+
let linkColor: [number, number, number, number]
|
|
158
|
+
|
|
159
|
+
switch (sourceType) {
|
|
160
|
+
case NodeType.App:
|
|
161
|
+
linkColor = [0.2, 0.8, 1.0, 0.8] // Bright blue
|
|
162
|
+
break
|
|
163
|
+
case NodeType.Framework:
|
|
164
|
+
linkColor = [1.0, 0.6, 0.2, 0.8] // Orange
|
|
165
|
+
break
|
|
166
|
+
case NodeType.Library:
|
|
167
|
+
linkColor = [0.4, 1.0, 0.4, 0.8] // Green
|
|
168
|
+
break
|
|
169
|
+
case NodeType.Bundle:
|
|
170
|
+
linkColor = [1.0, 0.4, 1.0, 0.8] // Magenta
|
|
171
|
+
break
|
|
172
|
+
case NodeType.Target:
|
|
173
|
+
linkColor = [0.8, 0.4, 1.0, 0.8] // Purple
|
|
174
|
+
break
|
|
175
|
+
default:
|
|
176
|
+
linkColor = [0.7, 0.7, 0.7, 0.8] // Gray
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Add RGBA values for this link
|
|
180
|
+
linkColors.push(linkColor[0], linkColor[1], linkColor[2], linkColor[3])
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Create graph with static positioning
|
|
185
|
+
const graph = new Graph(graphContainer, {
|
|
186
|
+
spaceSize,
|
|
187
|
+
enableSimulation: false,
|
|
188
|
+
enableDrag: false,
|
|
189
|
+
linkArrows: true,
|
|
190
|
+
curvedLinks: true,
|
|
191
|
+
pointSize: 50,
|
|
192
|
+
linkWidth: 3,
|
|
193
|
+
hoveredPointRingColor: 'white',
|
|
194
|
+
renderHoveredPointRing: true,
|
|
195
|
+
|
|
196
|
+
// Add click handler for point and background selection
|
|
197
|
+
onClick: (pointIndex: number | undefined): void => {
|
|
198
|
+
if (pointIndex !== undefined) {
|
|
199
|
+
// Use built-in functionality to select the clicked point and its neighbors
|
|
200
|
+
graph.selectPointByIndex(pointIndex, true)
|
|
201
|
+
} else {
|
|
202
|
+
// Clear selection when clicking on background
|
|
203
|
+
graph.unselectPoints()
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const imageDataArray = await loadPngImages([swiftUrl, boxUrl, toolboxUrl, legoUrl, sUrl])
|
|
209
|
+
|
|
210
|
+
// Set images and their indices
|
|
211
|
+
graph.setImageData(imageDataArray)
|
|
212
|
+
graph.setPointImageIndices(imageIndices)
|
|
213
|
+
|
|
214
|
+
// Set all data
|
|
215
|
+
graph.setPointPositions(pointPositions)
|
|
216
|
+
graph.setPointColors(pointColors)
|
|
217
|
+
graph.setPointShapes(pointShapes)
|
|
218
|
+
graph.setPointSizes(pointSizes)
|
|
219
|
+
|
|
220
|
+
// Set links if we have any dependencies
|
|
221
|
+
if (links.length > 0) {
|
|
222
|
+
graph.setLinks(new Float32Array(links))
|
|
223
|
+
graph.setLinkArrows(linkArrows)
|
|
224
|
+
graph.setLinkColors(new Float32Array(linkColors))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
graph.render()
|
|
228
|
+
|
|
229
|
+
return { div, graph }
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('Error creating Xcode dependency graph:', error)
|
|
232
|
+
div.innerHTML = `
|
|
233
|
+
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #ff0000; font-size: 18px;">
|
|
234
|
+
Error loading Xcode dependency graph: ${error instanceof Error ? error.message : 'Unknown error'}
|
|
235
|
+
</div>
|
|
236
|
+
`
|
|
237
|
+
throw error
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -3,8 +3,10 @@ import type { Meta } from '@storybook/html'
|
|
|
3
3
|
import { createStory, Story } from '@/graph/stories/create-story'
|
|
4
4
|
import { CosmosStoryProps } from './create-cosmos'
|
|
5
5
|
import { allShapes } from './shapes/all-shapes'
|
|
6
|
+
import { imageExample } from './shapes/image-example'
|
|
6
7
|
|
|
7
8
|
import allShapesStoryRaw from './shapes/all-shapes/index?raw'
|
|
9
|
+
import imageExampleStoryRaw from './shapes/image-example/index?raw'
|
|
8
10
|
|
|
9
11
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
10
12
|
const meta: Meta<CosmosStoryProps> = {
|
|
@@ -21,5 +23,15 @@ export const AllShapes: Story = {
|
|
|
21
23
|
},
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
export const ImageExample: Story = {
|
|
27
|
+
...createStory(imageExample),
|
|
28
|
+
name: 'Image Points Example',
|
|
29
|
+
parameters: {
|
|
30
|
+
sourceCode: [
|
|
31
|
+
{ name: 'Story', code: imageExampleStoryRaw },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
// eslint-disable-next-line import/no-default-export
|
|
25
37
|
export default meta
|