1
2
3
4
5 package d2elklayout
6
7 import (
8 "context"
9 _ "embed"
10 "encoding/json"
11 "errors"
12 "fmt"
13 "math"
14 "regexp"
15 "strconv"
16 "strings"
17
18 "github.com/dop251/goja"
19
20 "oss.terrastruct.com/util-go/xdefer"
21
22 "oss.terrastruct.com/util-go/go2"
23
24 "oss.terrastruct.com/d2/d2graph"
25 "oss.terrastruct.com/d2/d2target"
26 "oss.terrastruct.com/d2/lib/geo"
27 "oss.terrastruct.com/d2/lib/label"
28 "oss.terrastruct.com/d2/lib/shape"
29 )
30
31
32 var elkJS string
33
34
35 var setupJS string
36
37 type ELKNode struct {
38 ID string `json:"id"`
39 X float64 `json:"x"`
40 Y float64 `json:"y"`
41 Width float64 `json:"width"`
42 Height float64 `json:"height"`
43 Children []*ELKNode `json:"children,omitempty"`
44 Ports []*ELKPort `json:"ports,omitempty"`
45 Labels []*ELKLabel `json:"labels,omitempty"`
46 LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
47 }
48
49 type PortSide string
50
51 const (
52 South PortSide = "SOUTH"
53 North PortSide = "NORTH"
54 East PortSide = "EAST"
55 West PortSide = "WEST"
56 )
57
58 type Direction string
59
60 const (
61 Down Direction = "DOWN"
62 Up Direction = "UP"
63 Right Direction = "RIGHT"
64 Left Direction = "LEFT"
65 )
66
67 type ELKPort struct {
68 ID string `json:"id"`
69 X float64 `json:"x"`
70 Y float64 `json:"y"`
71 Width float64 `json:"width"`
72 Height float64 `json:"height"`
73 LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
74 }
75
76 type ELKLabel struct {
77 Text string `json:"text"`
78 X float64 `json:"x"`
79 Y float64 `json:"y"`
80 Width float64 `json:"width"`
81 Height float64 `json:"height"`
82 LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
83 }
84
85 type ELKPoint struct {
86 X float64 `json:"x"`
87 Y float64 `json:"y"`
88 }
89
90 type ELKEdgeSection struct {
91 Start ELKPoint `json:"startPoint"`
92 End ELKPoint `json:"endPoint"`
93 BendPoints []ELKPoint `json:"bendPoints,omitempty"`
94 }
95
96 type ELKEdge struct {
97 ID string `json:"id"`
98 Sources []string `json:"sources"`
99 Targets []string `json:"targets"`
100 Sections []ELKEdgeSection `json:"sections,omitempty"`
101 Labels []*ELKLabel `json:"labels,omitempty"`
102 Container string `json:"container"`
103 }
104
105 type ELKGraph struct {
106 ID string `json:"id"`
107 LayoutOptions *elkOpts `json:"layoutOptions"`
108 Children []*ELKNode `json:"children,omitempty"`
109 Edges []*ELKEdge `json:"edges,omitempty"`
110 }
111
112 type ConfigurableOpts struct {
113 Algorithm string `json:"elk.algorithm,omitempty"`
114 NodeSpacing int `json:"spacing.nodeNodeBetweenLayers,omitempty"`
115 Padding string `json:"elk.padding,omitempty"`
116 EdgeNodeSpacing int `json:"spacing.edgeNodeBetweenLayers,omitempty"`
117 SelfLoopSpacing int `json:"elk.spacing.nodeSelfLoop"`
118 }
119
120 var DefaultOpts = ConfigurableOpts{
121 Algorithm: "layered",
122 NodeSpacing: 70.0,
123 Padding: "[top=50,left=50,bottom=50,right=50]",
124 EdgeNodeSpacing: 40.0,
125 SelfLoopSpacing: 50.0,
126 }
127
128 var port_spacing = 40.
129 var edge_node_spacing = 40
130
131 type elkOpts struct {
132 EdgeNode int `json:"elk.spacing.edgeNode,omitempty"`
133 FixedAlignment string `json:"elk.layered.nodePlacement.bk.fixedAlignment,omitempty"`
134 Thoroughness int `json:"elk.layered.thoroughness,omitempty"`
135 EdgeEdgeBetweenLayersSpacing int `json:"elk.layered.spacing.edgeEdgeBetweenLayers,omitempty"`
136 Direction Direction `json:"elk.direction"`
137 HierarchyHandling string `json:"elk.hierarchyHandling,omitempty"`
138 InlineEdgeLabels bool `json:"elk.edgeLabels.inline,omitempty"`
139 ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
140 ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"`
141 CycleBreakingStrategy string `json:"elk.layered.cycleBreaking.strategy,omitempty"`
142
143 SelfLoopDistribution string `json:"elk.layered.edgeRouting.selfLoopDistribution,omitempty"`
144
145 NodeSizeConstraints string `json:"elk.nodeSize.constraints,omitempty"`
146 ContentAlignment string `json:"elk.contentAlignment,omitempty"`
147 NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"`
148
149 PortSide PortSide `json:"elk.port.side,omitempty"`
150 PortConstraints string `json:"elk.portConstraints,omitempty"`
151
152 ConfigurableOpts
153 }
154
155 func DefaultLayout(ctx context.Context, g *d2graph.Graph) (err error) {
156 return Layout(ctx, g, nil)
157 }
158
159 func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err error) {
160 if opts == nil {
161 opts = &DefaultOpts
162 }
163 defer xdefer.Errorf(&err, "failed to ELK layout")
164
165 vm := goja.New()
166
167 console := vm.NewObject()
168 if err := vm.Set("console", console); err != nil {
169 return err
170 }
171
172 if _, err := vm.RunString(elkJS); err != nil {
173 return err
174 }
175 if _, err := vm.RunString(setupJS); err != nil {
176 return err
177 }
178
179 elkGraph := &ELKGraph{
180 ID: "",
181 LayoutOptions: &elkOpts{
182 Thoroughness: 8,
183 EdgeEdgeBetweenLayersSpacing: 50,
184 EdgeNode: edge_node_spacing,
185 HierarchyHandling: "INCLUDE_CHILDREN",
186 FixedAlignment: "BALANCED",
187 ConsiderModelOrder: "NODES_AND_EDGES",
188 CycleBreakingStrategy: "GREEDY_MODEL_ORDER",
189 NodeSizeConstraints: "MINIMUM_SIZE",
190 ContentAlignment: "H_CENTER V_CENTER",
191 ConfigurableOpts: ConfigurableOpts{
192 Algorithm: opts.Algorithm,
193 NodeSpacing: opts.NodeSpacing,
194 EdgeNodeSpacing: opts.EdgeNodeSpacing,
195 SelfLoopSpacing: opts.SelfLoopSpacing,
196 },
197 },
198 }
199 if elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing {
200
201 elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(elkGraph.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, childrenMaxSelfLoop(g.Root, g.Root.Direction.Value == "down" || g.Root.Direction.Value == "" || g.Root.Direction.Value == "up")/2+5)
202 }
203 switch g.Root.Direction.Value {
204 case "down":
205 elkGraph.LayoutOptions.Direction = Down
206 case "up":
207 elkGraph.LayoutOptions.Direction = Up
208 case "right":
209 elkGraph.LayoutOptions.Direction = Right
210 case "left":
211 elkGraph.LayoutOptions.Direction = Left
212 default:
213 elkGraph.LayoutOptions.Direction = Down
214 }
215
216
217 for _, obj := range g.Objects {
218 positionLabelsIcons(obj)
219 }
220
221 adjustments := make(map[*d2graph.Object]geo.Spacing)
222 elkNodes := make(map[*d2graph.Object]*ELKNode)
223 elkEdges := make(map[*d2graph.Edge]*ELKEdge)
224
225
226 var walk func(*d2graph.Object, *d2graph.Object, func(*d2graph.Object, *d2graph.Object))
227 walk = func(obj, parent *d2graph.Object, fn func(*d2graph.Object, *d2graph.Object)) {
228 if obj.Parent != nil {
229 fn(obj, parent)
230 }
231 for _, ch := range obj.ChildrenArray {
232 walk(ch, obj, fn)
233 }
234 }
235
236 walk(g.Root, nil, func(obj, parent *d2graph.Object) {
237 incoming := 0.
238 outgoing := 0.
239 for _, e := range g.Edges {
240 if e.Src == obj {
241 outgoing++
242 }
243 if e.Dst == obj {
244 incoming++
245 }
246 }
247 if incoming >= 2 || outgoing >= 2 {
248 switch g.Root.Direction.Value {
249 case "right", "left":
250 if obj.Attributes.HeightAttr == nil {
251 obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing)
252 }
253 default:
254 if obj.Attributes.WidthAttr == nil {
255 obj.Width = math.Max(obj.Width, math.Max(incoming, outgoing)*port_spacing)
256 }
257 }
258 }
259
260 if obj.HasLabel() && obj.HasIcon() {
261
262 obj.Height += float64(obj.LabelDimensions.Height + label.PADDING)
263 }
264
265 margin, _ := obj.SpacingOpt(label.PADDING, label.PADDING, false)
266 width := margin.Left + obj.Width + margin.Right
267 height := margin.Top + obj.Height + margin.Bottom
268 adjustments[obj] = margin
269
270 n := &ELKNode{
271 ID: obj.AbsID(),
272 Width: width,
273 Height: height,
274 }
275
276 if len(obj.ChildrenArray) > 0 {
277 n.LayoutOptions = &elkOpts{
278 ForceNodeModelOrder: true,
279 Thoroughness: 8,
280 EdgeEdgeBetweenLayersSpacing: 50,
281 HierarchyHandling: "INCLUDE_CHILDREN",
282 FixedAlignment: "BALANCED",
283 EdgeNode: edge_node_spacing,
284 ConsiderModelOrder: "NODES_AND_EDGES",
285 CycleBreakingStrategy: "GREEDY_MODEL_ORDER",
286 NodeSizeConstraints: "MINIMUM_SIZE",
287 ContentAlignment: "H_CENTER V_CENTER",
288 ConfigurableOpts: ConfigurableOpts{
289 NodeSpacing: opts.NodeSpacing,
290 EdgeNodeSpacing: opts.EdgeNodeSpacing,
291 SelfLoopSpacing: opts.SelfLoopSpacing,
292 Padding: opts.Padding,
293 },
294 }
295 if n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing == DefaultOpts.SelfLoopSpacing {
296 n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing = go2.Max(n.LayoutOptions.ConfigurableOpts.SelfLoopSpacing, childrenMaxSelfLoop(obj, g.Root.Direction.Value == "down" || g.Root.Direction.Value == "" || g.Root.Direction.Value == "up")/2+5)
297 }
298
299 switch elkGraph.LayoutOptions.Direction {
300 case Down, Up:
301 n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width)))
302 case Right, Left:
303 n.LayoutOptions.NodeSizeMinimum = fmt.Sprintf("(%d, %d)", int(math.Ceil(width)), int(math.Ceil(height)))
304 }
305 } else {
306 n.LayoutOptions = &elkOpts{
307 SelfLoopDistribution: "EQUALLY",
308 }
309 }
310
311 if obj.IsContainer() {
312 padding := parsePadding(opts.Padding)
313 padding = adjustPadding(obj, width, height, padding)
314 n.LayoutOptions.Padding = padding.String()
315 }
316
317 if obj.HasLabel() {
318 n.Labels = append(n.Labels, &ELKLabel{
319 Text: obj.Label.Value,
320 Width: float64(obj.LabelDimensions.Width),
321 Height: float64(obj.LabelDimensions.Height),
322 })
323 }
324
325 if parent == g.Root {
326 elkGraph.Children = append(elkGraph.Children, n)
327 } else {
328 elkNodes[parent].Children = append(elkNodes[parent].Children, n)
329 }
330
331 if obj.SQLTable != nil {
332 n.LayoutOptions.PortConstraints = "FIXED_POS"
333 columns := obj.SQLTable.Columns
334 colHeight := n.Height / float64(len(columns)+1)
335 n.Ports = make([]*ELKPort, 0, len(columns)*2)
336 var srcSide, dstSide PortSide
337 switch elkGraph.LayoutOptions.Direction {
338 case Left:
339 srcSide, dstSide = West, East
340 default:
341 srcSide, dstSide = East, West
342 }
343 for i, col := range columns {
344 n.Ports = append(n.Ports, &ELKPort{
345 ID: srcPortID(obj, col.Name.Label),
346 Y: float64(i+1)*colHeight + colHeight/2,
347 LayoutOptions: &elkOpts{PortSide: srcSide},
348 })
349 n.Ports = append(n.Ports, &ELKPort{
350 ID: dstPortID(obj, col.Name.Label),
351 Y: float64(i+1)*colHeight + colHeight/2,
352 LayoutOptions: &elkOpts{PortSide: dstSide},
353 })
354 }
355 }
356
357 elkNodes[obj] = n
358 })
359
360 var srcSide, dstSide PortSide
361 switch elkGraph.LayoutOptions.Direction {
362 case Up:
363 srcSide, dstSide = North, South
364 default:
365 srcSide, dstSide = South, North
366 }
367
368 ports := map[struct {
369 obj *d2graph.Object
370 side PortSide
371 }][]*ELKPort{}
372
373 for ei, edge := range g.Edges {
374 var src, dst string
375
376 switch {
377 case edge.SrcTableColumnIndex != nil:
378 src = srcPortID(edge.Src, edge.Src.SQLTable.Columns[*edge.SrcTableColumnIndex].Name.Label)
379 case edge.Src.SQLTable != nil:
380 p := &ELKPort{
381 ID: fmt.Sprintf("%s.%d", srcPortID(edge.Src, "__root__"), ei),
382 LayoutOptions: &elkOpts{PortSide: srcSide},
383 }
384 src = p.ID
385 elkNodes[edge.Src].Ports = append(elkNodes[edge.Src].Ports, p)
386 k := struct {
387 obj *d2graph.Object
388 side PortSide
389 }{edge.Src, srcSide}
390 ports[k] = append(ports[k], p)
391 default:
392 src = edge.Src.AbsID()
393 }
394
395 switch {
396 case edge.DstTableColumnIndex != nil:
397 dst = dstPortID(edge.Dst, edge.Dst.SQLTable.Columns[*edge.DstTableColumnIndex].Name.Label)
398 case edge.Dst.SQLTable != nil:
399 p := &ELKPort{
400 ID: fmt.Sprintf("%s.%d", dstPortID(edge.Dst, "__root__"), ei),
401 LayoutOptions: &elkOpts{PortSide: dstSide},
402 }
403 dst = p.ID
404 elkNodes[edge.Dst].Ports = append(elkNodes[edge.Dst].Ports, p)
405 k := struct {
406 obj *d2graph.Object
407 side PortSide
408 }{edge.Dst, dstSide}
409 ports[k] = append(ports[k], p)
410 default:
411 dst = edge.Dst.AbsID()
412 }
413
414 e := &ELKEdge{
415 ID: edge.AbsID(),
416 Sources: []string{src},
417 Targets: []string{dst},
418 }
419 if edge.Label.Value != "" {
420 e.Labels = append(e.Labels, &ELKLabel{
421 Text: edge.Label.Value,
422 Width: float64(edge.LabelDimensions.Width),
423 Height: float64(edge.LabelDimensions.Height),
424 LayoutOptions: &elkOpts{
425 InlineEdgeLabels: true,
426 },
427 })
428 }
429 elkGraph.Edges = append(elkGraph.Edges, e)
430 elkEdges[edge] = e
431 }
432
433 for k, ports := range ports {
434 width := elkNodes[k.obj].Width
435 spacing := width / float64(len(ports)+1)
436 for i, p := range ports {
437 p.X = float64(i+1) * spacing
438 }
439 }
440
441 raw, err := json.Marshal(elkGraph)
442 if err != nil {
443 return err
444 }
445
446 loadScript := fmt.Sprintf(`var graph = %s`, raw)
447
448 if _, err := vm.RunString(loadScript); err != nil {
449 return err
450 }
451
452 val, err := vm.RunString(`elk.layout(graph)
453 .then(s => s)
454 .catch(err => err.message)
455 `)
456
457 if err != nil {
458 return err
459 }
460
461 p := val.Export()
462 if err != nil {
463 return err
464 }
465
466 promise := p.(*goja.Promise)
467
468 for promise.State() == goja.PromiseStatePending {
469 if err := ctx.Err(); err != nil {
470 return err
471 }
472 continue
473 }
474
475 if promise.State() == goja.PromiseStateRejected {
476 return errors.New("ELK: something went wrong")
477 }
478
479 result := promise.Result().Export()
480
481 var jsonOut map[string]interface{}
482 switch out := result.(type) {
483 case string:
484 return fmt.Errorf("ELK layout error: %s", out)
485 case map[string]interface{}:
486 jsonOut = out
487 default:
488 return fmt.Errorf("ELK unexpected return: %v", out)
489 }
490
491 jsonBytes, err := json.Marshal(jsonOut)
492 if err != nil {
493 return err
494 }
495
496 err = json.Unmarshal(jsonBytes, &elkGraph)
497 if err != nil {
498 return err
499 }
500
501 byID := make(map[string]*d2graph.Object)
502 walk(g.Root, nil, func(obj, parent *d2graph.Object) {
503 n := elkNodes[obj]
504
505 parentX := 0.0
506 parentY := 0.0
507 if parent != nil && parent != g.Root {
508 parentX = parent.TopLeft.X
509 parentY = parent.TopLeft.Y
510 }
511 obj.TopLeft = geo.NewPoint(parentX+n.X, parentY+n.Y)
512 obj.Width = math.Ceil(n.Width)
513 obj.Height = math.Ceil(n.Height)
514
515 byID[obj.AbsID()] = obj
516 })
517
518 for _, edge := range g.Edges {
519 e := elkEdges[edge]
520
521 parentX := 0.0
522 parentY := 0.0
523 if e.Container != "" {
524 parentX = byID[e.Container].TopLeft.X
525 parentY = byID[e.Container].TopLeft.Y
526 }
527
528 var points []*geo.Point
529 for _, s := range e.Sections {
530 points = append(points, &geo.Point{
531 X: parentX + s.Start.X,
532 Y: parentY + s.Start.Y,
533 })
534 for _, bp := range s.BendPoints {
535 points = append(points, &geo.Point{
536 X: parentX + bp.X,
537 Y: parentY + bp.Y,
538 })
539 }
540 points = append(points, &geo.Point{
541 X: parentX + s.End.X,
542 Y: parentY + s.End.Y,
543 })
544 }
545 edge.Route = points
546 }
547
548 objEdges := make(map[*d2graph.Object][]*d2graph.Edge)
549 for _, e := range g.Edges {
550 objEdges[e.Src] = append(objEdges[e.Src], e)
551 if e.Dst != e.Src {
552 objEdges[e.Dst] = append(objEdges[e.Dst], e)
553 }
554 }
555
556 for _, obj := range g.Objects {
557 if margin, has := adjustments[obj]; has {
558 edges := objEdges[obj]
559
560 if margin.Left > 0 {
561 for _, e := range edges {
562 l := len(e.Route)
563 if e.Src == obj && e.Route[0].X == obj.TopLeft.X {
564 e.Route[0].X += margin.Left
565 }
566 if e.Dst == obj && e.Route[l-1].X == obj.TopLeft.X {
567 e.Route[l-1].X += margin.Left
568 }
569 }
570 obj.TopLeft.X += margin.Left
571 obj.ShiftDescendants(margin.Left/2, 0)
572 obj.Width -= margin.Left
573 }
574 if margin.Right > 0 {
575 for _, e := range edges {
576 l := len(e.Route)
577 if e.Src == obj && e.Route[0].X == obj.TopLeft.X+obj.Width {
578 e.Route[0].X -= margin.Right
579 }
580 if e.Dst == obj && e.Route[l-1].X == obj.TopLeft.X+obj.Width {
581 e.Route[l-1].X -= margin.Right
582 }
583 }
584 obj.ShiftDescendants(-margin.Right/2, 0)
585 obj.Width -= margin.Right
586 }
587 if margin.Top > 0 {
588 for _, e := range edges {
589 l := len(e.Route)
590 if e.Src == obj && e.Route[0].Y == obj.TopLeft.Y {
591 e.Route[0].Y += margin.Top
592 }
593 if e.Dst == obj && e.Route[l-1].Y == obj.TopLeft.Y {
594 e.Route[l-1].Y += margin.Top
595 }
596 }
597 obj.TopLeft.Y += margin.Top
598 obj.ShiftDescendants(0, margin.Top/2)
599 obj.Height -= margin.Top
600 }
601 if margin.Bottom > 0 {
602 for _, e := range edges {
603 l := len(e.Route)
604 if e.Src == obj && e.Route[0].Y == obj.TopLeft.Y+obj.Height {
605 e.Route[0].Y -= margin.Bottom
606 }
607 if e.Dst == obj && e.Route[l-1].Y == obj.TopLeft.Y+obj.Height {
608 e.Route[l-1].Y -= margin.Bottom
609 }
610 }
611 obj.ShiftDescendants(0, -margin.Bottom/2)
612 obj.Height -= margin.Bottom
613 }
614 }
615 }
616
617 for _, edge := range g.Edges {
618 points := edge.Route
619
620 startIndex, endIndex := 0, len(points)-1
621 start := points[startIndex]
622 end := points[endIndex]
623
624 var originalSrcTL, originalDstTL *geo.Point
625
626 if srcDx, srcDy := edge.Src.GetModifierElementAdjustments(); srcDx != 0 || srcDy != 0 {
627 if start.X > edge.Src.TopLeft.X+srcDx &&
628 start.Y < edge.Src.TopLeft.Y+edge.Src.Height-srcDy {
629 originalSrcTL = edge.Src.TopLeft.Copy()
630 edge.Src.TopLeft.X += srcDx
631 edge.Src.TopLeft.Y -= srcDy
632 }
633 }
634 if dstDx, dstDy := edge.Dst.GetModifierElementAdjustments(); dstDx != 0 || dstDy != 0 {
635 if end.X > edge.Dst.TopLeft.X+dstDx &&
636 end.Y < edge.Dst.TopLeft.Y+edge.Dst.Height-dstDy {
637 originalDstTL = edge.Dst.TopLeft.Copy()
638 edge.Dst.TopLeft.X += dstDx
639 edge.Dst.TopLeft.Y -= dstDy
640 }
641 }
642
643 startIndex, endIndex = edge.TraceToShape(points, startIndex, endIndex)
644 points = points[startIndex : endIndex+1]
645
646 if edge.Label.Value != "" {
647 edge.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
648 }
649
650 edge.Route = points
651
652
653 if originalSrcTL != nil {
654 edge.Src.TopLeft.X = originalSrcTL.X
655 edge.Src.TopLeft.Y = originalSrcTL.Y
656 }
657 if originalDstTL != nil {
658 edge.Dst.TopLeft.X = originalDstTL.X
659 edge.Dst.TopLeft.Y = originalDstTL.Y
660 }
661 }
662
663 deleteBends(g)
664
665 return nil
666 }
667
668 func srcPortID(obj *d2graph.Object, column string) string {
669 return fmt.Sprintf("%s.%s.src", obj.AbsID(), column)
670 }
671
672 func dstPortID(obj *d2graph.Object, column string) string {
673 return fmt.Sprintf("%s.%s.dst", obj.AbsID(), column)
674 }
675
676
677
678 func deleteBends(g *d2graph.Graph) {
679
680
681 for _, isSource := range []bool{true, false} {
682 for ei, e := range g.Edges {
683 if len(e.Route) < 4 {
684 continue
685 }
686 if e.Src == e.Dst {
687 continue
688 }
689 var endpoint *d2graph.Object
690 var start *geo.Point
691 var corner *geo.Point
692 var end *geo.Point
693
694 var columnIndex *int
695 if isSource {
696 start = e.Route[0]
697 corner = e.Route[1]
698 end = e.Route[2]
699 endpoint = e.Src
700 columnIndex = e.SrcTableColumnIndex
701 } else {
702 start = e.Route[len(e.Route)-1]
703 corner = e.Route[len(e.Route)-2]
704 end = e.Route[len(e.Route)-3]
705 endpoint = e.Dst
706 columnIndex = e.DstTableColumnIndex
707 }
708
709 isHorizontal := math.Ceil(start.Y) == math.Ceil(corner.Y)
710 dx, dy := endpoint.GetModifierElementAdjustments()
711
712
713 switch {
714 case columnIndex != nil:
715 rowHeight := endpoint.Height / float64(len(endpoint.SQLTable.Columns)+1)
716 rowCenter := endpoint.TopLeft.Y + rowHeight*float64(*columnIndex+1) + rowHeight/2
717
718
719 if math.Abs(end.Y-rowCenter) > rowHeight/3 {
720 continue
721 }
722 case isHorizontal:
723 if end.Y <= endpoint.TopLeft.Y+10-dy {
724 continue
725 }
726 if end.Y >= endpoint.TopLeft.Y+endpoint.Height-10 {
727 continue
728 }
729 default:
730 if end.X <= endpoint.TopLeft.X+10 {
731 continue
732 }
733 if end.X >= endpoint.TopLeft.X+endpoint.Width-10+dx {
734 continue
735 }
736 }
737
738 var newStart *geo.Point
739 if isHorizontal {
740 newStart = geo.NewPoint(start.X, end.Y)
741 } else {
742 newStart = geo.NewPoint(end.X, start.Y)
743 }
744
745 endpointShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(endpoint.Shape.Value)], endpoint.Box)
746 newStart = shape.TraceToShapeBorder(endpointShape, newStart, end)
747
748
749
750 oldSegment := geo.NewSegment(start, corner)
751 newSegment := geo.NewSegment(newStart, end)
752
753 oldIntersects := countObjectIntersects(g, e.Src, e.Dst, *oldSegment)
754 newIntersects := countObjectIntersects(g, e.Src, e.Dst, *newSegment)
755
756 if newIntersects > oldIntersects {
757 continue
758 }
759
760 oldCrossingsCount, oldOverlapsCount, oldCloseOverlapsCount, oldTouchingCount := countEdgeIntersects(g, g.Edges[ei], *oldSegment)
761 newCrossingsCount, newOverlapsCount, newCloseOverlapsCount, newTouchingCount := countEdgeIntersects(g, g.Edges[ei], *newSegment)
762
763 if newCrossingsCount > oldCrossingsCount {
764 continue
765 }
766 if newOverlapsCount > oldOverlapsCount {
767 continue
768 }
769
770 if newCloseOverlapsCount > oldCloseOverlapsCount {
771 continue
772 }
773 if newTouchingCount > oldTouchingCount {
774 continue
775 }
776
777
778 if isSource {
779 g.Edges[ei].Route = append(
780 []*geo.Point{newStart},
781 e.Route[3:]...,
782 )
783 } else {
784 g.Edges[ei].Route = append(
785 e.Route[:len(e.Route)-3],
786 newStart,
787 )
788 }
789 }
790 }
791
792
793
794
795
796
797
798
799 points := map[geo.Point]int{}
800 for _, e := range g.Edges {
801 for _, p := range e.Route {
802 points[*p]++
803 }
804 }
805
806 for ei, e := range g.Edges {
807 if len(e.Route) < 6 {
808 continue
809 }
810 if e.Src == e.Dst {
811 continue
812 }
813
814 for i := 1; i < len(e.Route)-3; i++ {
815 before := e.Route[i-1]
816 start := e.Route[i]
817 corner := e.Route[i+1]
818 end := e.Route[i+2]
819 after := e.Route[i+3]
820
821 if c, _ := points[*corner]; c > 1 {
822
823 continue
824 }
825
826
827
828
829 var newCorner *geo.Point
830 if math.Ceil(start.X) == math.Ceil(corner.X) {
831 newCorner = geo.NewPoint(end.X, start.Y)
832
833 if (end.X > start.X) != (start.X > before.X) {
834 continue
835 }
836 if (end.Y > start.Y) != (after.Y > end.Y) {
837 continue
838 }
839 } else {
840 newCorner = geo.NewPoint(start.X, end.Y)
841 if (end.Y > start.Y) != (start.Y > before.Y) {
842 continue
843 }
844 if (end.X > start.X) != (after.X > end.X) {
845 continue
846 }
847 }
848
849 oldS1 := geo.NewSegment(start, corner)
850 oldS2 := geo.NewSegment(corner, end)
851
852 newS1 := geo.NewSegment(start, newCorner)
853 newS2 := geo.NewSegment(newCorner, end)
854
855
856 oldIntersects := countObjectIntersects(g, e.Src, e.Dst, *oldS1) + countObjectIntersects(g, e.Src, e.Dst, *oldS2)
857 newIntersects := countObjectIntersects(g, e.Src, e.Dst, *newS1) + countObjectIntersects(g, e.Src, e.Dst, *newS2)
858
859 if newIntersects > oldIntersects {
860 continue
861 }
862
863 oldCrossingsCount1, oldOverlapsCount1, oldCloseOverlapsCount1, oldTouchingCount1 := countEdgeIntersects(g, g.Edges[ei], *oldS1)
864 oldCrossingsCount2, oldOverlapsCount2, oldCloseOverlapsCount2, oldTouchingCount2 := countEdgeIntersects(g, g.Edges[ei], *oldS2)
865 oldCrossingsCount := oldCrossingsCount1 + oldCrossingsCount2
866 oldOverlapsCount := oldOverlapsCount1 + oldOverlapsCount2
867 oldCloseOverlapsCount := oldCloseOverlapsCount1 + oldCloseOverlapsCount2
868 oldTouchingCount := oldTouchingCount1 + oldTouchingCount2
869
870 newCrossingsCount1, newOverlapsCount1, newCloseOverlapsCount1, newTouchingCount1 := countEdgeIntersects(g, g.Edges[ei], *newS1)
871 newCrossingsCount2, newOverlapsCount2, newCloseOverlapsCount2, newTouchingCount2 := countEdgeIntersects(g, g.Edges[ei], *newS2)
872 newCrossingsCount := newCrossingsCount1 + newCrossingsCount2
873 newOverlapsCount := newOverlapsCount1 + newOverlapsCount2
874 newCloseOverlapsCount := newCloseOverlapsCount1 + newCloseOverlapsCount2
875 newTouchingCount := newTouchingCount1 + newTouchingCount2
876
877 if newCrossingsCount > oldCrossingsCount {
878 continue
879 }
880 if newOverlapsCount > oldOverlapsCount {
881 continue
882 }
883
884 if newCloseOverlapsCount > oldCloseOverlapsCount {
885 continue
886 }
887 if newTouchingCount > oldTouchingCount {
888 continue
889 }
890
891
892 g.Edges[ei].Route = append(append(
893 e.Route[:i],
894 newCorner,
895 ),
896 e.Route[i+3:]...,
897 )
898 break
899 }
900 }
901 }
902
903 func countObjectIntersects(g *d2graph.Graph, src, dst *d2graph.Object, s geo.Segment) int {
904 count := 0
905 for i, o := range g.Objects {
906 if g.Objects[i] == src || g.Objects[i] == dst {
907 continue
908 }
909 if o.Intersects(s, float64(edge_node_spacing)-1) {
910 count++
911 }
912 }
913 return count
914 }
915
916
917 func countEdgeIntersects(g *d2graph.Graph, sEdge *d2graph.Edge, s geo.Segment) (int, int, int, int) {
918 isHorizontal := math.Ceil(s.Start.Y) == math.Ceil(s.End.Y)
919 crossingsCount := 0
920 overlapsCount := 0
921 closeOverlapsCount := 0
922 touchingCount := 0
923 for i, e := range g.Edges {
924 if g.Edges[i] == sEdge {
925 continue
926 }
927
928 for i := 0; i < len(e.Route)-1; i++ {
929 otherS := geo.NewSegment(e.Route[i], e.Route[i+1])
930 otherIsHorizontal := math.Ceil(otherS.Start.Y) == math.Ceil(otherS.End.Y)
931 if isHorizontal == otherIsHorizontal {
932 if s.Overlaps(*otherS, !isHorizontal, 0.) {
933 if isHorizontal {
934 if math.Abs(s.Start.Y-otherS.Start.Y) < float64(edge_node_spacing)/2. {
935 overlapsCount++
936 if math.Abs(s.Start.Y-otherS.Start.Y) < float64(edge_node_spacing)/4. {
937 closeOverlapsCount++
938 if math.Abs(s.Start.Y-otherS.Start.Y) < 1. {
939 touchingCount++
940 }
941 }
942 }
943 } else {
944 if math.Abs(s.Start.X-otherS.Start.X) < float64(edge_node_spacing)/2. {
945 overlapsCount++
946 if math.Abs(s.Start.X-otherS.Start.X) < float64(edge_node_spacing)/4. {
947 closeOverlapsCount++
948 if math.Abs(s.Start.Y-otherS.Start.Y) < 1. {
949 touchingCount++
950 }
951 }
952 }
953 }
954 }
955 } else {
956 if s.Intersects(*otherS) {
957 crossingsCount++
958 }
959 }
960 }
961
962 }
963 return crossingsCount, overlapsCount, closeOverlapsCount, touchingCount
964 }
965
966 func childrenMaxSelfLoop(parent *d2graph.Object, isWidth bool) int {
967 max := 0
968 for _, ch := range parent.Children {
969 for _, e := range parent.Graph.Edges {
970 if e.Src == e.Dst && e.Src == ch && e.Label.Value != "" {
971 if isWidth {
972 max = go2.Max(max, e.LabelDimensions.Width)
973 } else {
974 max = go2.Max(max, e.LabelDimensions.Height)
975 }
976 }
977 }
978 }
979
980 return max
981 }
982
983 type shapePadding struct {
984 top, left, bottom, right int
985 }
986
987
988 func parsePadding(in string) shapePadding {
989 reTop := regexp.MustCompile(`top=(\d+)`)
990 reLeft := regexp.MustCompile(`left=(\d+)`)
991 reBottom := regexp.MustCompile(`bottom=(\d+)`)
992 reRight := regexp.MustCompile(`right=(\d+)`)
993
994 padding := shapePadding{}
995
996 submatches := reTop.FindStringSubmatch(in)
997 if len(submatches) == 2 {
998 i, err := strconv.ParseInt(submatches[1], 10, 64)
999 if err == nil {
1000 padding.top = int(i)
1001 }
1002 }
1003
1004 submatches = reLeft.FindStringSubmatch(in)
1005 if len(submatches) == 2 {
1006 i, err := strconv.ParseInt(submatches[1], 10, 64)
1007 if err == nil {
1008 padding.left = int(i)
1009 }
1010 }
1011
1012 submatches = reBottom.FindStringSubmatch(in)
1013 if len(submatches) == 2 {
1014 i, err := strconv.ParseInt(submatches[1], 10, 64)
1015 if err == nil {
1016 padding.bottom = int(i)
1017 }
1018 }
1019
1020 submatches = reRight.FindStringSubmatch(in)
1021 i, err := strconv.ParseInt(submatches[1], 10, 64)
1022 if len(submatches) == 2 {
1023 if err == nil {
1024 padding.right = int(i)
1025 }
1026 }
1027
1028 return padding
1029 }
1030
1031 func (padding shapePadding) String() string {
1032 return fmt.Sprintf("[top=%d,left=%d,bottom=%d,right=%d]", padding.top, padding.left, padding.bottom, padding.right)
1033 }
1034
1035 func adjustPadding(obj *d2graph.Object, width, height float64, padding shapePadding) shapePadding {
1036 if !obj.IsContainer() {
1037 return padding
1038 }
1039
1040
1041 var extraTop, extraBottom, extraLeft, extraRight int
1042 if obj.HasLabel() && obj.LabelPosition != nil {
1043 labelHeight := obj.LabelDimensions.Height + 2*label.PADDING
1044 labelWidth := obj.LabelDimensions.Width + 2*label.PADDING
1045 switch label.FromString(*obj.LabelPosition) {
1046 case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight:
1047
1048 extraTop = labelHeight
1049 case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight:
1050 extraBottom = labelHeight
1051 case label.InsideMiddleLeft:
1052 extraLeft = labelWidth
1053 case label.InsideMiddleRight:
1054 extraRight = labelWidth
1055 }
1056 }
1057 if obj.HasIcon() && obj.IconPosition != nil {
1058 iconSize := d2target.MAX_ICON_SIZE + 2*label.PADDING
1059 switch label.FromString(*obj.IconPosition) {
1060 case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight:
1061 extraTop = go2.Max(extraTop, iconSize)
1062 case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight:
1063 extraBottom = go2.Max(extraBottom, iconSize)
1064 case label.InsideMiddleLeft:
1065 extraLeft = go2.Max(extraLeft, iconSize)
1066 case label.InsideMiddleRight:
1067 extraRight = go2.Max(extraRight, iconSize)
1068 }
1069 }
1070
1071 maxChildWidth, maxChildHeight := math.Inf(-1), math.Inf(-1)
1072 for _, c := range obj.ChildrenArray {
1073 if c.Width > maxChildWidth {
1074 maxChildWidth = c.Width
1075 }
1076 if c.Height > maxChildHeight {
1077 maxChildHeight = c.Height
1078 }
1079 }
1080
1081
1082 width += maxChildWidth + float64(extraLeft+extraRight)
1083 height += maxChildHeight + float64(extraTop+extraBottom)
1084 contentBox := geo.NewBox(geo.NewPoint(0, 0), width, height)
1085 shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Shape.Value]
1086 s := shape.NewShape(shapeType, contentBox)
1087 innerBox := s.GetInnerBox()
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114 innerTop := int(math.Ceil(innerBox.TopLeft.Y))
1115 innerBottom := int(math.Ceil(height - (innerBox.TopLeft.Y + innerBox.Height)))
1116 innerLeft := int(math.Ceil(innerBox.TopLeft.X))
1117 innerRight := int(math.Ceil(width - (innerBox.TopLeft.X + innerBox.Width)))
1118
1119 padding.top = go2.Max(padding.top, innerTop+extraTop)
1120 padding.bottom = go2.Max(padding.bottom, innerBottom+extraBottom)
1121 padding.left = go2.Max(padding.left, innerLeft+extraLeft)
1122 padding.right = go2.Max(padding.right, innerRight+extraRight)
1123
1124 return padding
1125 }
1126
1127 func positionLabelsIcons(obj *d2graph.Object) {
1128 if obj.Icon != nil && obj.IconPosition == nil {
1129 if len(obj.ChildrenArray) > 0 {
1130 obj.IconPosition = go2.Pointer(label.InsideTopLeft.String())
1131 if obj.LabelPosition == nil {
1132 obj.LabelPosition = go2.Pointer(label.InsideTopRight.String())
1133 return
1134 }
1135 } else if obj.SQLTable != nil || obj.Class != nil || obj.Language != "" {
1136 obj.IconPosition = go2.Pointer(label.OutsideTopLeft.String())
1137 } else {
1138 obj.IconPosition = go2.Pointer(label.InsideMiddleCenter.String())
1139 }
1140 }
1141 if obj.HasLabel() && obj.LabelPosition == nil {
1142 if len(obj.ChildrenArray) > 0 {
1143 obj.LabelPosition = go2.Pointer(label.InsideTopCenter.String())
1144 } else if obj.HasOutsideBottomLabel() {
1145 obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
1146 } else if obj.Icon != nil {
1147 obj.LabelPosition = go2.Pointer(label.InsideTopCenter.String())
1148 } else {
1149 obj.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
1150 }
1151 }
1152 }
1153
View as plain text