@@ -111,14 +111,28 @@ func (g *graph) drawArrow(from gridCoord, to gridCoord, e *edge) (*drawing, *dra
111111 }
112112 log .Debugf ("Drawing arrow from %v to %v with path %v" , from , to , e .path )
113113 dLabel := g .drawArrowLabel (e )
114- dPath , linesDrawn , lineDirs := g .drawPath (e .path )
115- dBoxStart := g .drawBoxStart (e .path , linesDrawn [0 ])
116- dArrowHead := g .drawArrowHead (linesDrawn [len (linesDrawn )- 1 ], lineDirs [len (lineDirs )- 1 ])
117- if e .isBidirectional && len (linesDrawn ) > 0 {
118- dStartArrowHead := g .drawArrowHead (reverseDrawingLine (linesDrawn [0 ]), lineDirs [0 ].getOpposite ())
114+ dPath , linesDrawn , lineDirs := g .drawPath (e .path , e .stroke )
115+ dBoxStart := g .drawBoxStart (e .path , linesDrawn [0 ], e .stroke )
116+ skipHead := e .head == headNone
117+ if skipHead && len (linesDrawn ) > 0 {
118+ // headNone (open links like ---) has no arrowhead glyph to mark the
119+ // connection at the destination, unlike every other head; touch up
120+ // the destination box's own border the same way drawBoxStart does
121+ // for the source, so the join is visible there too.
122+ dBoxEnd := g .drawBoxEnd (linesDrawn [len (linesDrawn )- 1 ], lineDirs [len (lineDirs )- 1 ], e .stroke )
123+ dBoxStart = g .mergeDrawings (dBoxStart , drawingCoord {0 , 0 }, dBoxEnd )
124+ }
125+ var dArrowHead * drawing
126+ if skipHead {
127+ dArrowHead = copyCanvas (g .drawing )
128+ } else {
129+ dArrowHead = g .drawArrowHead (linesDrawn [len (linesDrawn )- 1 ], lineDirs [len (lineDirs )- 1 ], e .head )
130+ }
131+ if e .isBidirectional && ! skipHead && len (linesDrawn ) > 0 {
132+ dStartArrowHead := g .drawArrowHead (reverseDrawingLine (linesDrawn [0 ]), lineDirs [0 ].getOpposite (), e .head )
119133 dArrowHead = g .mergeDrawings (dArrowHead , drawingCoord {0 , 0 }, dStartArrowHead )
120134 }
121- dCorners := g .drawCorners (e .path )
135+ dCorners := g .drawCorners (e .path , e . stroke )
122136 return dPath , dBoxStart , dArrowHead , dCorners , dLabel
123137}
124138
@@ -160,7 +174,7 @@ func mergePath(path []gridCoord) []gridCoord {
160174 return newPath
161175}
162176
163- func (g * graph ) drawPath (path []gridCoord ) (* drawing , [][]drawingCoord , []direction ) {
177+ func (g * graph ) drawPath (path []gridCoord , stroke edgeStroke ) (* drawing , [][]drawingCoord , []direction ) {
164178 d := copyCanvas (g .drawing )
165179 previousCoord := path [0 ]
166180 linesDrawn := make ([][]drawingCoord , 0 )
@@ -174,7 +188,7 @@ func (g *graph) drawPath(path []gridCoord) (*drawing, [][]drawingCoord, []direct
174188 continue
175189 }
176190 dir := determineDirection (genericCoord (previousCoord ), genericCoord (nextCoord ))
177- s := g .drawLine (d , previousDrawingCoord , nextDrawingCoord , 1 , - 1 )
191+ s := g .drawLine (d , previousDrawingCoord , nextDrawingCoord , 1 , - 1 , stroke )
178192 if len (s ) == 0 {
179193 // drawLine may return no coords if offsets collapse the line. Use at least one point so arrow and junction logic
180194 // can still infer a direction.
@@ -187,7 +201,7 @@ func (g *graph) drawPath(path []gridCoord) (*drawing, [][]drawingCoord, []direct
187201 return d , linesDrawn , lineDirs
188202}
189203
190- func (g * graph ) drawBoxStart (path []gridCoord , firstLine []drawingCoord ) * drawing {
204+ func (g * graph ) drawBoxStart (path []gridCoord , firstLine []drawingCoord , stroke edgeStroke ) * drawing {
191205 d := * (copyCanvas (g .drawing ))
192206 from := firstLine [0 ]
193207 dir := determineDirection (genericCoord (path [0 ]), genericCoord (path [1 ]))
@@ -197,6 +211,13 @@ func (g *graph) drawBoxStart(path []gridCoord, firstLine []drawingCoord) *drawin
197211 return & d
198212 }
199213
214+ // A heavy line meeting a light box border looks mismatched through a
215+ // light T-junction glyph (e.g. "├"); leave the box's own light border
216+ // character in place instead.
217+ if stroke == strokeThick {
218+ return & d
219+ }
220+
200221 switch dir {
201222 case Up :
202223 d [from .x ][from .y + 1 ] = "┴"
@@ -210,7 +231,32 @@ func (g *graph) drawBoxStart(path []gridCoord, firstLine []drawingCoord) *drawin
210231 return & d
211232}
212233
213- func (g * graph ) drawArrowHead (line []drawingCoord , fallback direction ) * drawing {
234+ // drawBoxEnd is drawBoxStart's mirror for the destination end of a path.
235+ func (g * graph ) drawBoxEnd (lastLine []drawingCoord , dir direction , stroke edgeStroke ) * drawing {
236+ d := * (copyCanvas (g .drawing ))
237+ if len (lastLine ) == 0 {
238+ return & d
239+ }
240+ to := lastLine [len (lastLine )- 1 ]
241+
242+ if g .useAscii || stroke == strokeThick {
243+ return & d
244+ }
245+
246+ switch dir {
247+ case Up :
248+ d [to .x ][to .y - 1 ] = "┬"
249+ case Down :
250+ d [to .x ][to .y + 1 ] = "┴"
251+ case Left :
252+ d [to .x - 1 ][to .y ] = "├"
253+ case Right :
254+ d [to .x + 1 ][to .y ] = "┤"
255+ }
256+ return & d
257+ }
258+
259+ func (g * graph ) drawArrowHead (line []drawingCoord , fallback direction , head edgeHead ) * drawing {
214260 d := * (copyCanvas (g .drawing ))
215261 if len (line ) == 0 {
216262 return & d
@@ -222,6 +268,22 @@ func (g *graph) drawArrowHead(line []drawingCoord, fallback direction) *drawing
222268 dir = fallback
223269 }
224270
271+ // Circle and cross heads are a single glyph regardless of travel direction.
272+ if head == headCircle || head == headCross {
273+ char := "o"
274+ if head == headCross {
275+ char = "x"
276+ }
277+ if ! g .useAscii {
278+ char = "●"
279+ if head == headCross {
280+ char = "✕"
281+ }
282+ }
283+ d [lastPos.x ][lastPos.y ] = char
284+ return & d
285+ }
286+
225287 var char string
226288 if ! g .useAscii {
227289 switch dir {
@@ -293,8 +355,12 @@ func (g *graph) drawArrowHead(line []drawingCoord, fallback direction) *drawing
293355 return & d
294356}
295357
296- func (g * graph ) drawCorners (path []gridCoord ) * drawing {
358+ func (g * graph ) drawCorners (path []gridCoord , stroke edgeStroke ) * drawing {
297359 d := copyCanvas (g .drawing )
360+ // Thick edges get the heavy box-drawing corner glyphs to match their
361+ // heavy line/arrowhead; ASCII has no distinct "thick corner" (still
362+ // "+"), and dotted corners are left as plain light corners.
363+ thick := ! g .useAscii && stroke == strokeThick
298364 for idx , coord := range path {
299365 // Skip the first and last step
300366 if idx == 0 || idx == len (path )- 1 {
@@ -310,12 +376,24 @@ func (g *graph) drawCorners(path []gridCoord) *drawing {
310376 switch {
311377 case (prevDir == Right && nextDir == Down ) || (prevDir == Up && nextDir == Left ):
312378 corner = "┐"
379+ if thick {
380+ corner = "┓"
381+ }
313382 case (prevDir == Right && nextDir == Up ) || (prevDir == Down && nextDir == Left ):
314383 corner = "┘"
384+ if thick {
385+ corner = "┛"
386+ }
315387 case (prevDir == Left && nextDir == Down ) || (prevDir == Up && nextDir == Right ):
316388 corner = "┌"
389+ if thick {
390+ corner = "┏"
391+ }
317392 case (prevDir == Left && nextDir == Up ) || (prevDir == Down && nextDir == Right ):
318393 corner = "└"
394+ if thick {
395+ corner = "┗"
396+ }
319397 default :
320398 corner = "+"
321399 }
@@ -353,7 +431,7 @@ func insetLine(line []drawingCoord, insetStart, insetEnd int) []drawingCoord {
353431 }
354432 endInset := insetEnd
355433 if insetEnd > 0 {
356- endInset = insetEnd - 1 // shift label right ~1 char so the start dash survives the centered placement
434+ endInset = insetEnd - 1 // shift label right ~1 char so the start dash survives the centered placement
357435 }
358436 dir := determineDirection (genericCoord (line [0 ]), genericCoord (line [1 ]))
359437 a , b := line [0 ], line [1 ]
0 commit comments