11
22import { ObsidianRenderer , ObsidianLink , LinkPair , GltLink , DataviewLinkType } from 'src/types' ;
33import { getAPI } from 'obsidian-dataview' ;
4- import { Text , TextStyle } from 'pixi.js' ;
4+ import { Text , TextStyle , Graphics } from 'pixi.js' ;
55// @ts -ignore
66import extractLinks from 'markdown-link-extractor' ;
77
@@ -11,9 +11,42 @@ export class LinkManager {
1111 api = getAPI ( ) ;
1212 currentTheme : string ;
1313 textColor : string ;
14+ tagColors : Map < string , number > ;
15+ categoricalColors : number [ ] = [
16+ 0xF44336 , // Red
17+ 0x03A9F4 , // Light Blue
18+ 0xFF9800 , // Orange
19+ 0x9C27B0 , // Purple
20+ 0xCDDC39 , // Lime
21+ 0x3F51B5 , // Indigo
22+ 0xFFC107 , // Amber
23+ 0x00BCD4 , // Cyan
24+ 0xE91E63 , // Pink
25+ 0x4CAF50 , // Green
26+ 0xFF5722 , // Deep Orange
27+ 0x673AB7 , // Deep Purple
28+ 0x9E9E9E , // Grey
29+ 0x2196F3 , // Blue
30+ 0x8BC34A , // Light Green
31+ 0x795548 , // Brown
32+ 0x009688 , // Teal
33+ 0x607D8B , // Blue Grey
34+ 0xFFEB3B , // Yellow
35+ 0x000000 // Black for contrast
36+ ]
37+
38+
39+ currentTagColorIndex = 0 ;
40+ yOffset = 5 ; // To increment the y position for each legend item
41+ xOffset = 20 ;
42+ lineHeight = 17 ; // Height of each line in the legend
43+ lineLength = 40 ; // Width of the color line
44+ spaceBetweenTextAndLine = 1 ; // Space between the text and the start of the line
45+
1446
1547 constructor ( ) {
1648 this . linksMap = new Map < string , GltLink > ( ) ;
49+ this . tagColors = new Map < string , number > ( ) ;
1750
1851 // Detect changes to the theme.
1952 this . detectThemeChange ( ) ;
@@ -73,14 +106,15 @@ export class LinkManager {
73106 }
74107 }
75108
76- addLink ( renderer : ObsidianRenderer , obLink : ObsidianLink ) : void {
109+ addLink ( renderer : ObsidianRenderer , obLink : ObsidianLink , tagColors : boolean ) : void {
77110 const key = this . generateKey ( obLink . source . id , obLink . target . id ) ;
78111 const reverseKey = this . generateKey ( obLink . target . id , obLink . source . id ) ;
79112 const pairStatus = ( obLink . source . id !== obLink . target . id ) && this . linksMap . has ( reverseKey ) ? LinkPair . Second : LinkPair . None ;
80113 const newLink : GltLink = {
81114 obsidianLink : obLink ,
82115 pairStatus : pairStatus ,
83- pixiText : this . createTextForLink ( renderer , obLink , pairStatus )
116+ pixiText : this . initializeLinkText ( renderer , obLink , pairStatus ) ,
117+ pixiGraphics : tagColors ? this . initializeLinkGraphics ( renderer , obLink , pairStatus ) : null ,
84118 } ;
85119
86120 this . linksMap . set ( key , newLink ) ;
@@ -99,11 +133,27 @@ export class LinkManager {
99133
100134 const gltLink = this . linksMap . get ( key ) ;
101135
136+ let text ;
102137 if ( gltLink && gltLink . pixiText && renderer . px && renderer . px . stage && renderer . px . stage . children && renderer . px . stage . children . includes ( gltLink . pixiText ) ) {
138+ text = gltLink . pixiText . text ;
103139 renderer . px . stage . removeChild ( gltLink . pixiText ) ;
104140 gltLink . pixiText . destroy ( ) ;
105141 }
106142
143+ if ( gltLink && gltLink . pixiGraphics && renderer . px && renderer . px . stage && renderer . px . stage . children && renderer . px . stage . children . includes ( gltLink . pixiGraphics ) ) {
144+ renderer . px . stage . removeChild ( gltLink . pixiGraphics ) ;
145+ gltLink . pixiGraphics . destroy ( ) ;
146+ }
147+
148+ let colorKey = text ?. replace ( / \r ? \n / g, "" ) ;
149+ if ( colorKey ) {
150+ if ( this . tagColors . has ( colorKey ) ) {
151+ this . tagColors . delete ( colorKey ) ;
152+ this . yOffset -= this . lineHeight ;
153+ this . currentTagColorIndex -= 1 ;
154+ }
155+ }
156+
107157 this . linksMap . delete ( key ) ;
108158
109159 const reverseLink = this . linksMap . get ( reverseKey ) ;
@@ -131,7 +181,7 @@ export class LinkManager {
131181 }
132182
133183 // Update the position of the text on the graph
134- updateTextPosition ( renderer : ObsidianRenderer , link : ObsidianLink ) : void {
184+ updateLinkText ( renderer : ObsidianRenderer , link : ObsidianLink ) : void {
135185 if ( ! renderer || ! link || ! link . source || ! link . target ) {
136186 // If any of these are null, exit the function
137187 return ;
@@ -160,8 +210,73 @@ export class LinkManager {
160210 }
161211 }
162212
213+ // Update the position of the text on the graph
214+ updateLinkGraphics ( renderer : ObsidianRenderer , link : ObsidianLink ) : void {
215+ if ( ! renderer || ! link || ! link . source || ! link . target ) {
216+ // If any of these are null, exit the function
217+ return ;
218+ }
219+ const linkKey = this . generateKey ( link . source . id , link . target . id ) ;
220+ const gltLink = this . linksMap . get ( linkKey ) ;
221+ let graphics ;
222+ if ( gltLink ) {
223+ graphics = gltLink . pixiGraphics ;
224+ } else {
225+ return
226+ } ;
227+ let { nx, ny} = this . calculateNormal ( link . source . x , link . source . y , link . target . x , link . target . y ) ;
228+ let { px, py} = this . calculateParallel ( link . source . x , link . source . y , link . target . x , link . target . y ) ;
229+
230+ nx *= Math . sqrt ( renderer . scale ) ;
231+ ny *= Math . sqrt ( renderer . scale ) ;
232+
233+ px *= 8 * Math . sqrt ( renderer . scale ) ;
234+ py *= 8 * Math . sqrt ( renderer . scale ) ;
235+
236+
237+ let { x :x1 , y :y1 } = this . getLinkToTextCoordinates ( link . source . x , link . source . y , renderer . panX , renderer . panY , renderer . scale ) ;
238+ let { x :x2 , y :y2 } = this . getLinkToTextCoordinates ( link . target . x , link . target . y , renderer . panX , renderer . panY , renderer . scale ) ;
239+ x1 += nx + px ;
240+ x2 += nx - px ;
241+ y1 += ny + py ;
242+ y2 += ny - py ;
243+
244+
245+
246+ let color ;
247+
248+ // Get the text to display for the link
249+ let linkString : string | null = this . getMetadataKeyForLink ( link . source . id , link . target . id ) ;
250+ if ( linkString === null ) {
251+
252+ } else {
253+
254+
255+ if ( link . source . id === link . target . id ) {
256+ linkString = "" ;
257+ }
258+
259+
260+ if ( ! this . tagColors . has ( linkString ) ) {
261+ color = 0x000000 ;
262+ } else {
263+ color = this . tagColors . get ( linkString ) ;
264+ }
265+ }
266+
267+
268+ if ( graphics && renderer . px && renderer . px . stage && renderer . px . stage . children && renderer . px . stage . children . includes ( graphics ) ) {
269+ // Now, update the line whenever needed without creating a new graphics object each time
270+ graphics . clear ( ) ; // Clear the previous drawing to prepare for the update
271+ graphics . lineStyle ( 3 / Math . sqrt ( renderer . nodeScale ) , color ) ; // Set the line style (width: 2px, color: black, alpha: 1)
272+ graphics . alpha = .6 ;
273+ graphics . moveTo ( x1 , y1 ) ; // Move to the starting point of the line (source node)
274+ graphics . lineTo ( x2 , y2 ) ; // Draw the line to the ending point (target node)
275+ }
276+ }
277+
163278 // Create or update text for a given link
164- private createTextForLink ( renderer : ObsidianRenderer , link : ObsidianLink , pairStatus : LinkPair ) : Text | null {
279+ private initializeLinkText ( renderer : ObsidianRenderer , link : ObsidianLink , pairStatus : LinkPair ) : Text | null {
165280
166281 // Get the text to display for the link
167282 let linkString : string | null = this . getMetadataKeyForLink ( link . source . id , link . target . id ) ;
@@ -181,6 +296,8 @@ export class LinkManager {
181296 } else {
182297
183298 }
299+
300+
184301 // Define the style for the text
185302 const textStyle : TextStyle = new TextStyle ( {
186303 fontFamily : 'Arial' ,
@@ -189,15 +306,76 @@ export class LinkManager {
189306 } ) ;
190307 // Create new text node
191308 const text : Text = new Text ( linkString , textStyle ) ;
192- text . alpha = 0.7 ;
309+
310+ text . zIndex = 1 ;
311+ text . alpha = 0.9 ;
193312 text . anchor . set ( 0.5 , 0.5 ) ;
194313
195314
196- this . updateTextPosition ( renderer , link ) ;
315+
316+ this . updateLinkText ( renderer , link ) ;
197317 renderer . px . stage . addChild ( text ) ;
318+
198319 return text
199320 }
200321
322+ // Create or update text for a given link
323+ private initializeLinkGraphics ( renderer : ObsidianRenderer , link : ObsidianLink , pairStatus : LinkPair ) : Graphics | null {
324+
325+ // Get the text to display for the link
326+ let linkString : string | null = this . getMetadataKeyForLink ( link . source . id , link . target . id ) ;
327+ if ( linkString === null ) {
328+ return null ;
329+ } //doesn't add if link is null
330+
331+
332+ if ( link . source . id === link . target . id ) {
333+ linkString = "" ;
334+ } else {
335+
336+ let color ;
337+
338+
339+
340+
341+ if ( ! this . tagColors . has ( linkString ) ) {
342+ color = this . categoricalColors [ this . currentTagColorIndex ] ;
343+ this . tagColors . set ( linkString , color ) ;
344+ // Increment and wrap the index to cycle through colors
345+ this . currentTagColorIndex = ( this . currentTagColorIndex + 1 ) % this . categoricalColors . length ;
346+
347+ // Create and add the label
348+ const textL = new Text ( linkString , { fontFamily : 'Arial' , fontSize : 14 , fill : 0x000000 } ) ;
349+ textL . x = this . xOffset ;
350+ textL . y = this . yOffset ;
351+ renderer . px . stage . addChild ( textL ) ;
352+
353+ // Calculate the starting x-coordinate for the line, based on the text width
354+ const lineStartX = this . xOffset + textL . width + this . spaceBetweenTextAndLine ;
355+
356+ const graphicsL = new Graphics ( ) ;
357+ graphicsL . lineStyle ( 2 , color , 1 ) ; // Assuming 'color' is in a PIXI-compatible format
358+ graphicsL . moveTo ( lineStartX , this . yOffset + ( this . lineHeight / 2 ) ) ; // Start a little below the text
359+ graphicsL . lineTo ( lineStartX + this . lineLength , this . yOffset + ( this . lineHeight / 2 ) ) ; // 40 pixels wide line
360+ renderer . px . stage . addChild ( graphicsL ) ;
361+ this . yOffset += this . lineHeight ;
362+ } else {
363+ color = this . tagColors . get ( linkString ) ;
364+ }
365+ }
366+
367+
368+ const graphics = new Graphics ( ) ;
369+ graphics . zIndex = 0 ;
370+ renderer . px . stage . addChild ( graphics ) ; // Add the line to the stage
371+
372+
373+ this . updateLinkGraphics ( renderer , link ) ;
374+
375+
376+ return graphics
377+ }
378+
201379 // Utility function to extract the file path from a Markdown link
202380 private extractPathFromMarkdownLink ( markdownLink : string | unknown ) : string {
203381 const links = extractLinks ( markdownLink ) . links ;
@@ -223,11 +401,28 @@ export class LinkManager {
223401 // Remove all text nodes from the graph
224402 destroyMap ( renderer : ObsidianRenderer ) : void {
225403 if ( this . linksMap . size > 0 ) {
226- this . linksMap . forEach ( ( gltLink , linkKey ) => {
227- if ( gltLink . pixiText && renderer . px && renderer . px . stage && renderer . px . stage . children && renderer . px . stage . children . includes ( gltLink . pixiText ) ) {
404+ this . linksMap . forEach ( ( gltLink , linkKey ) => {
405+ let text ;
406+ if ( gltLink && gltLink . pixiText && renderer . px && renderer . px . stage && renderer . px . stage . children && renderer . px . stage . children . includes ( gltLink . pixiText ) ) {
407+ text = gltLink . pixiText . text ;
228408 renderer . px . stage . removeChild ( gltLink . pixiText ) ;
229409 gltLink . pixiText . destroy ( ) ;
230410 }
411+
412+ if ( gltLink && gltLink . pixiGraphics && renderer . px && renderer . px . stage && renderer . px . stage . children && renderer . px . stage . children . includes ( gltLink . pixiGraphics ) ) {
413+ renderer . px . stage . removeChild ( gltLink . pixiGraphics ) ;
414+ gltLink . pixiGraphics . destroy ( ) ;
415+ }
416+
417+ let colorKey = text ?. replace ( / \r ? \n / g, "" ) ;
418+ if ( colorKey ) {
419+ if ( this . tagColors . has ( colorKey ) ) {
420+ this . tagColors . delete ( colorKey ) ;
421+ this . yOffset -= this . lineHeight ;
422+ this . currentTagColorIndex -= 1 ;
423+ }
424+ }
425+
231426 this . linksMap . delete ( linkKey ) ;
232427 } ) ;
233428 }
@@ -277,5 +472,38 @@ export class LinkManager {
277472 // Apply scaling and panning to calculate the actual position.
278473 return { x : linkX * scale + panX , y : linkY * scale + panY } ;
279474 }
280-
475+
476+ private calculateNormal ( sourceX : number , sourceY : number , targetX : number , targetY : number ) : { nx : number ; ny : number ; } {
477+ // Calculate the direction vector D
478+ const dx = targetX - sourceX ;
479+ const dy = targetY - sourceY ;
480+
481+ // Calculate the normal vector N by rotating D by 90 degrees
482+ let nx = - dy ;
483+ let ny = dx ;
484+
485+ // Normalize the normal vector to get a unit vector
486+ const length = Math . sqrt ( nx * nx + ny * ny ) ;
487+ nx /= length ; // Normalize the x component
488+ ny /= length ; // Normalize the y component
489+
490+
491+ return { nx, ny } ;
492+ }
493+
494+ private calculateParallel ( sourceX : number , sourceY : number , targetX : number , targetY : number ) : { px : number ; py : number ; } {
495+ // Calculate the direction vector D from source to target
496+ const dx = targetX - sourceX ;
497+ const dy = targetY - sourceY ;
498+
499+ // No need to rotate the vector for a parallel vector
500+
501+ // Normalize the direction vector to get a unit vector
502+ const length = Math . sqrt ( dx * dx + dy * dy ) ;
503+ const px = dx / length ; // Normalize the x component
504+ const py = dy / length ; // Normalize the y component
505+
506+ return { px, py } ;
507+ }
508+
281509}
0 commit comments