@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.
Files changed (36) hide show
  1. package/.eslintrc +61 -0
  2. package/CHARTER.md +69 -0
  3. package/GOVERNANCE.md +21 -0
  4. package/dist/index.d.ts +46 -15
  5. package/dist/index.js +2986 -2701
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.min.js +180 -62
  8. package/dist/index.min.js.map +1 -1
  9. package/dist/modules/GraphData/index.d.ts +18 -2
  10. package/dist/modules/Points/atlas-utils.d.ts +24 -0
  11. package/dist/modules/Points/index.d.ts +21 -2
  12. package/dist/modules/Store/index.d.ts +6 -1
  13. package/dist/stories/create-story.d.ts +5 -1
  14. package/dist/stories/shapes/image-example/index.d.ts +5 -0
  15. package/dist/stories/shapes.stories.d.ts +1 -0
  16. package/package.json +4 -4
  17. package/src/config.ts +1 -0
  18. package/src/declaration.d.ts +5 -0
  19. package/src/index.ts +119 -67
  20. package/src/modules/GraphData/index.ts +68 -6
  21. package/src/modules/Points/atlas-utils.ts +137 -0
  22. package/src/modules/Points/draw-highlighted.vert +3 -3
  23. package/src/modules/Points/draw-points.frag +106 -14
  24. package/src/modules/Points/draw-points.vert +51 -25
  25. package/src/modules/Points/find-points-on-area-selection.frag +6 -5
  26. package/src/modules/Points/index.ts +121 -13
  27. package/src/modules/Store/index.ts +11 -3
  28. package/src/stories/3. api-reference.mdx +48 -1
  29. package/src/stories/create-story.ts +32 -5
  30. package/src/stories/shapes/image-example/icons/box.png +0 -0
  31. package/src/stories/shapes/image-example/icons/lego.png +0 -0
  32. package/src/stories/shapes/image-example/icons/s.png +0 -0
  33. package/src/stories/shapes/image-example/icons/swift.png +0 -0
  34. package/src/stories/shapes/image-example/icons/toolbox.png +0 -0
  35. package/src/stories/shapes/image-example/index.ts +239 -0
  36. 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 dontRescale: boolean | undefined
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 `dontRescale` flag is set (allowing one-time skip of rescaling)
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.dontRescale) shouldRescale = false
87
+ if (this.shouldSkipRescale) shouldRescale = false
79
88
 
80
89
  if (shouldRescale) {
81
90
  this.rescaleInitialNodePositions()
82
- } else if (!this.dontRescale) {
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.dontRescale = undefined
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
- darkenGreyout: () => store.darkenGreyout,
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
- 'selection[0]': () => store.selectedArea[0],
289
- 'selection[1]': () => store.selectedArea[1],
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
- darkenGreyout: () => store.darkenGreyout,
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
- if (!indices?.length) return
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
- public getTrackedPositionsMap (): Map<number, [number, number]> {
692
- const tracked = new Map<number, [number, number]>()
693
- if (!this.trackedIndices) return tracked
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, darkenGreyout is true
38
- public darkenGreyout = false
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.darkenGreyout = brightness < 0.65
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-7) will default to Circle (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
- }) => Story = (storyFunction) => ({
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 story = storyFunction()
20
- args.graph = story.graph
21
- args.destroy = story.destroy
22
- return story.div
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
  })
@@ -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