d2/d2layouts/d2elklayout/layout.go

603 lines
16 KiB
Go
Raw Normal View History

2022-11-11 19:43:56 +00:00
// d2elklayout is a wrapper around the Javascript port of ELK.
//
// Coordinates are relative to parents.
// See https://www.eclipse.org/elk/documentation/tooldevelopers/graphdatastructure/coordinatesystem.html
package d2elklayout
import (
"context"
_ "embed"
"encoding/json"
2023-03-14 19:49:56 +00:00
"errors"
2022-11-11 19:43:56 +00:00
"fmt"
2023-02-19 05:51:55 +00:00
"math"
2022-12-18 04:40:56 +00:00
"strings"
2022-11-11 19:43:56 +00:00
2022-12-03 20:09:22 +00:00
"github.com/dop251/goja"
2022-12-08 07:22:20 +00:00
2022-12-01 19:32:57 +00:00
"oss.terrastruct.com/util-go/xdefer"
2022-11-11 19:43:56 +00:00
2022-12-01 18:48:01 +00:00
"oss.terrastruct.com/util-go/go2"
2022-11-11 19:43:56 +00:00
"oss.terrastruct.com/d2/d2graph"
2022-11-21 21:55:22 +00:00
"oss.terrastruct.com/d2/d2target"
2022-11-11 19:43:56 +00:00
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
2022-12-18 04:40:56 +00:00
"oss.terrastruct.com/d2/lib/shape"
2022-11-11 19:43:56 +00:00
)
//go:embed elk.js
var elkJS string
//go:embed setup.js
var setupJS string
type ELKNode struct {
2022-12-30 20:25:33 +00:00
ID string `json:"id"`
X float64 `json:"x"`
Y float64 `json:"y"`
Width float64 `json:"width"`
Height float64 `json:"height"`
Children []*ELKNode `json:"children,omitempty"`
Labels []*ELKLabel `json:"labels,omitempty"`
LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
2022-11-11 19:43:56 +00:00
}
type ELKLabel struct {
2022-12-30 20:25:33 +00:00
Text string `json:"text"`
X float64 `json:"x"`
Y float64 `json:"y"`
Width float64 `json:"width"`
Height float64 `json:"height"`
LayoutOptions *elkOpts `json:"layoutOptions,omitempty"`
2022-11-11 19:43:56 +00:00
}
type ELKPoint struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
type ELKEdgeSection struct {
Start ELKPoint `json:"startPoint"`
End ELKPoint `json:"endPoint"`
BendPoints []ELKPoint `json:"bendPoints,omitempty"`
}
type ELKEdge struct {
ID string `json:"id"`
Sources []string `json:"sources"`
Targets []string `json:"targets"`
Sections []ELKEdgeSection `json:"sections,omitempty"`
Labels []*ELKLabel `json:"labels,omitempty"`
Container string `json:"container"`
}
type ELKGraph struct {
2023-03-14 20:06:56 +00:00
ID string `json:"id"`
LayoutOptions *elkOpts `json:"layoutOptions"`
Children []*ELKNode `json:"children,omitempty"`
Edges []*ELKEdge `json:"edges,omitempty"`
2022-11-11 19:43:56 +00:00
}
2022-12-30 06:43:01 +00:00
type ConfigurableOpts struct {
Algorithm string `json:"elk.algorithm,omitempty"`
NodeSpacing int `json:"spacing.nodeNodeBetweenLayers,omitempty"`
Padding string `json:"elk.padding,omitempty"`
EdgeNodeSpacing int `json:"spacing.edgeNodeBetweenLayers,omitempty"`
SelfLoopSpacing int `json:"elk.spacing.nodeSelfLoop"`
2022-11-11 19:43:56 +00:00
}
2022-12-30 20:25:33 +00:00
var DefaultOpts = ConfigurableOpts{
Algorithm: "layered",
2023-02-10 08:57:15 +00:00
NodeSpacing: 70.0,
2023-02-10 03:49:53 +00:00
Padding: "[top=50,left=50,bottom=50,right=50]",
EdgeNodeSpacing: 40.0,
2022-12-30 20:25:33 +00:00
SelfLoopSpacing: 50.0,
}
2023-03-06 06:13:36 +00:00
var port_spacing = 40.
2023-03-14 21:11:51 +00:00
var edge_node_spacing = 40
2023-03-06 06:13:36 +00:00
2022-12-30 20:25:33 +00:00
type elkOpts struct {
2023-03-14 20:06:56 +00:00
EdgeNode int `json:"elk.spacing.edgeNode,omitempty"`
2023-03-14 18:08:29 +00:00
FixedAlignment string `json:"elk.layered.nodePlacement.bk.fixedAlignment,omitempty"`
2023-01-08 02:28:38 +00:00
Thoroughness int `json:"elk.layered.thoroughness,omitempty"`
EdgeEdgeBetweenLayersSpacing int `json:"elk.layered.spacing.edgeEdgeBetweenLayers,omitempty"`
Direction string `json:"elk.direction"`
HierarchyHandling string `json:"elk.hierarchyHandling,omitempty"`
InlineEdgeLabels bool `json:"elk.edgeLabels.inline,omitempty"`
ForceNodeModelOrder bool `json:"elk.layered.crossingMinimization.forceNodeModelOrder,omitempty"`
ConsiderModelOrder string `json:"elk.layered.considerModelOrder.strategy,omitempty"`
2022-12-30 06:43:01 +00:00
2023-02-19 05:51:55 +00:00
NodeSizeConstraints string `json:"elk.nodeSize.constraints,omitempty"`
NodeSizeMinimum string `json:"elk.nodeSize.minimum,omitempty"`
2022-12-30 06:43:01 +00:00
ConfigurableOpts
2022-11-11 19:43:56 +00:00
}
2022-12-30 21:36:49 +00:00
func DefaultLayout(ctx context.Context, g *d2graph.Graph) (err error) {
return Layout(ctx, g, nil)
}
2022-12-30 06:43:01 +00:00
func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err error) {
2022-12-30 05:09:53 +00:00
if opts == nil {
opts = &DefaultOpts
}
2022-11-11 19:43:56 +00:00
defer xdefer.Errorf(&err, "failed to ELK layout")
2022-12-03 20:09:22 +00:00
vm := goja.New()
2022-11-11 19:43:56 +00:00
2022-12-03 20:09:22 +00:00
console := vm.NewObject()
if err := vm.Set("console", console); err != nil {
2022-11-11 19:43:56 +00:00
return err
}
2022-12-03 20:09:22 +00:00
if _, err := vm.RunString(elkJS); err != nil {
return err
}
if _, err := vm.RunString(setupJS); err != nil {
2022-11-11 19:43:56 +00:00
return err
}
elkGraph := &ELKGraph{
ID: "root",
2022-12-30 20:25:33 +00:00
LayoutOptions: &elkOpts{
2023-02-09 06:11:52 +00:00
Thoroughness: 8,
2023-01-08 02:28:38 +00:00
EdgeEdgeBetweenLayersSpacing: 50,
2023-03-14 21:11:51 +00:00
EdgeNode: edge_node_spacing,
2023-01-08 02:28:38 +00:00
HierarchyHandling: "INCLUDE_CHILDREN",
2023-03-14 18:08:29 +00:00
FixedAlignment: "BALANCED",
2023-01-08 02:28:38 +00:00
ConsiderModelOrder: "NODES_AND_EDGES",
2022-12-30 22:28:52 +00:00
ConfigurableOpts: ConfigurableOpts{
Algorithm: opts.Algorithm,
NodeSpacing: opts.NodeSpacing,
EdgeNodeSpacing: opts.EdgeNodeSpacing,
SelfLoopSpacing: opts.SelfLoopSpacing,
},
2022-11-11 19:43:56 +00:00
},
}
2022-11-30 00:02:37 +00:00
switch g.Root.Attributes.Direction.Value {
case "down":
elkGraph.LayoutOptions.Direction = "DOWN"
case "up":
elkGraph.LayoutOptions.Direction = "UP"
case "right":
2022-11-29 05:39:36 +00:00
elkGraph.LayoutOptions.Direction = "RIGHT"
2022-11-30 00:02:37 +00:00
case "left":
elkGraph.LayoutOptions.Direction = "LEFT"
2022-11-30 01:57:17 +00:00
default:
elkGraph.LayoutOptions.Direction = "DOWN"
2022-11-29 05:39:36 +00:00
}
2022-11-11 19:43:56 +00:00
elkNodes := make(map[*d2graph.Object]*ELKNode)
elkEdges := make(map[*d2graph.Edge]*ELKEdge)
// BFS
var walk func(*d2graph.Object, *d2graph.Object, func(*d2graph.Object, *d2graph.Object))
walk = func(obj, parent *d2graph.Object, fn func(*d2graph.Object, *d2graph.Object)) {
if obj.Parent != nil {
fn(obj, parent)
}
for _, ch := range obj.ChildrenArray {
walk(ch, obj, fn)
}
}
walk(g.Root, nil, func(obj, parent *d2graph.Object) {
2023-03-06 06:13:36 +00:00
incoming := 0.
outgoing := 0.
for _, e := range g.Edges {
if e.Src == obj {
outgoing++
}
if e.Dst == obj {
incoming++
}
}
if incoming >= 2 || outgoing >= 2 {
switch g.Root.Attributes.Direction.Value {
case "right", "left":
obj.Height = math.Max(obj.Height, math.Max(incoming, outgoing)*port_spacing)
default:
obj.Width = math.Max(obj.Width, math.Max(incoming, outgoing)*port_spacing)
}
}
2022-12-20 04:15:35 +00:00
height := obj.Height
2023-02-19 05:51:55 +00:00
width := obj.Width
2022-12-29 07:53:37 +00:00
if obj.LabelWidth != nil && obj.LabelHeight != nil {
2023-03-02 22:52:30 +00:00
if obj.HasOutsideBottomLabel() || obj.Attributes.Icon != nil {
2022-12-29 07:53:37 +00:00
height += float64(*obj.LabelHeight) + label.PADDING
}
2023-02-19 05:51:55 +00:00
width = go2.Max(width, float64(*obj.LabelWidth))
2022-12-20 04:15:35 +00:00
}
2022-11-11 19:43:56 +00:00
n := &ELKNode{
ID: obj.AbsID(),
2023-02-19 05:51:55 +00:00
Width: width,
2022-12-20 04:15:35 +00:00
Height: height,
2022-11-11 19:43:56 +00:00
}
if len(obj.ChildrenArray) > 0 {
2022-12-30 20:25:33 +00:00
n.LayoutOptions = &elkOpts{
2023-01-08 02:28:38 +00:00
ForceNodeModelOrder: true,
2023-02-09 06:11:52 +00:00
Thoroughness: 8,
2023-01-08 02:28:38 +00:00
EdgeEdgeBetweenLayersSpacing: 50,
HierarchyHandling: "INCLUDE_CHILDREN",
2023-03-14 18:08:29 +00:00
FixedAlignment: "BALANCED",
2023-03-14 21:11:51 +00:00
EdgeNode: edge_node_spacing,
2023-01-08 02:28:38 +00:00
ConsiderModelOrder: "NODES_AND_EDGES",
2023-02-19 05:51:55 +00:00
// Why is it (height, width)? I have no clue, but it works.
NodeSizeMinimum: fmt.Sprintf("(%d, %d)", int(math.Ceil(height)), int(math.Ceil(width))),
2022-12-30 06:43:01 +00:00
ConfigurableOpts: ConfigurableOpts{
2023-01-08 02:28:38 +00:00
NodeSpacing: opts.NodeSpacing,
EdgeNodeSpacing: opts.EdgeNodeSpacing,
SelfLoopSpacing: opts.SelfLoopSpacing,
2023-01-08 02:28:38 +00:00
Padding: opts.Padding,
2022-12-30 06:43:01 +00:00
},
2022-11-11 19:43:56 +00:00
}
2023-02-19 05:51:55 +00:00
// Only set if specified.
// There's a bug where if it's the node label dimensions that set the NodeSizeMinimum,
// then suddenly it's reversed back to (width, height). I must be missing something
if obj.Attributes.Width != nil || obj.Attributes.Height != nil {
n.LayoutOptions.NodeSizeConstraints = "MINIMUM_SIZE"
}
2023-02-13 18:42:47 +00:00
if n.LayoutOptions.Padding == DefaultOpts.Padding {
// Default
paddingTop := 50
if obj.LabelHeight != nil {
paddingTop = go2.Max(paddingTop, *obj.LabelHeight+label.PADDING)
}
2023-02-13 19:23:54 +00:00
if obj.Attributes.Icon != nil && obj.Attributes.Shape.Value != d2target.ShapeImage {
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(n.Width), float64(n.Height))
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Attributes.Shape.Value]
s := shape.NewShape(shapeType, contentBox)
iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft))
2023-02-13 18:48:55 +00:00
paddingTop = go2.Max(paddingTop, iconSize+label.PADDING*2)
2023-02-13 18:42:47 +00:00
}
n.LayoutOptions.Padding = fmt.Sprintf("[top=%d,left=50,bottom=50,right=50]",
paddingTop,
)
}
2023-03-14 20:06:56 +00:00
} else {
n.LayoutOptions = &elkOpts{
// Margins: "[top=100,left=100,bottom=100,right=100]",
}
2022-11-11 19:43:56 +00:00
}
if obj.LabelWidth != nil && obj.LabelHeight != nil {
n.Labels = append(n.Labels, &ELKLabel{
Text: obj.Attributes.Label.Value,
Width: float64(*obj.LabelWidth),
Height: float64(*obj.LabelHeight),
})
}
if parent == g.Root {
elkGraph.Children = append(elkGraph.Children, n)
} else {
elkNodes[parent].Children = append(elkNodes[parent].Children, n)
}
elkNodes[obj] = n
})
for _, edge := range g.Edges {
e := &ELKEdge{
ID: edge.AbsID(),
Sources: []string{edge.Src.AbsID()},
Targets: []string{edge.Dst.AbsID()},
}
if edge.Attributes.Label.Value != "" {
e.Labels = append(e.Labels, &ELKLabel{
Text: edge.Attributes.Label.Value,
Width: float64(edge.LabelDimensions.Width),
Height: float64(edge.LabelDimensions.Height),
2022-12-30 20:25:33 +00:00
LayoutOptions: &elkOpts{
2022-12-20 23:31:13 +00:00
InlineEdgeLabels: true,
},
2022-11-11 19:43:56 +00:00
})
}
elkGraph.Edges = append(elkGraph.Edges, e)
elkEdges[edge] = e
}
raw, err := json.Marshal(elkGraph)
if err != nil {
return err
}
loadScript := fmt.Sprintf(`var graph = %s`, raw)
2022-12-03 20:09:22 +00:00
if _, err := vm.RunString(loadScript); err != nil {
2022-11-11 19:43:56 +00:00
return err
}
2022-12-03 20:09:22 +00:00
val, err := vm.RunString(`elk.layout(graph)
2022-11-11 19:43:56 +00:00
.then(s => s)
2023-03-14 19:49:56 +00:00
.catch(err => err.message)
2022-12-03 20:09:22 +00:00
`)
2022-11-11 19:43:56 +00:00
if err != nil {
return err
}
2022-12-03 20:09:22 +00:00
p := val.Export()
2022-11-11 19:43:56 +00:00
if err != nil {
return err
}
2022-12-03 20:09:22 +00:00
promise := p.(*goja.Promise)
for promise.State() == goja.PromiseStatePending {
2022-11-11 19:43:56 +00:00
if err := ctx.Err(); err != nil {
return err
}
continue
}
2023-03-14 19:49:56 +00:00
if promise.State() == goja.PromiseStateRejected {
return errors.New("ELK: something went wrong")
}
result := promise.Result().Export()
var jsonOut map[string]interface{}
switch out := result.(type) {
case string:
return fmt.Errorf("ELK layout error: %s", out)
case map[string]interface{}:
jsonOut = out
default:
return fmt.Errorf("ELK unexpected return: %v", out)
}
2022-12-03 20:09:22 +00:00
jsonBytes, err := json.Marshal(jsonOut)
2022-11-11 19:43:56 +00:00
if err != nil {
return err
}
2022-12-03 20:09:22 +00:00
err = json.Unmarshal(jsonBytes, &elkGraph)
2022-11-11 19:43:56 +00:00
if err != nil {
return err
}
byID := make(map[string]*d2graph.Object)
walk(g.Root, nil, func(obj, parent *d2graph.Object) {
n := elkNodes[obj]
parentX := 0.0
parentY := 0.0
if parent != nil && parent != g.Root {
parentX = parent.TopLeft.X
parentY = parent.TopLeft.Y
}
2022-12-24 21:39:45 +00:00
obj.TopLeft = geo.NewPoint(parentX+n.X, parentY+n.Y)
2022-11-11 19:43:56 +00:00
obj.Width = n.Width
obj.Height = n.Height
if obj.LabelWidth != nil && obj.LabelHeight != nil {
if len(obj.ChildrenArray) > 0 {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
2023-03-02 22:52:30 +00:00
} else if obj.HasOutsideBottomLabel() {
2022-12-20 04:15:35 +00:00
obj.LabelPosition = go2.Pointer(string(label.OutsideBottomCenter))
obj.Height -= float64(*obj.LabelHeight) + label.PADDING
} else if obj.Attributes.Icon != nil {
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
2022-11-11 19:43:56 +00:00
} else {
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
}
if obj.Attributes.Icon != nil {
2023-02-13 18:42:47 +00:00
if len(obj.ChildrenArray) > 0 {
obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
2023-02-13 19:23:54 +00:00
obj.LabelPosition = go2.Pointer(string(label.InsideTopRight))
2023-02-13 18:42:47 +00:00
} else {
obj.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
}
2022-11-11 19:43:56 +00:00
}
byID[obj.AbsID()] = obj
})
for _, edge := range g.Edges {
e := elkEdges[edge]
parentX := 0.0
parentY := 0.0
if e.Container != "root" {
parentX = byID[e.Container].TopLeft.X
parentY = byID[e.Container].TopLeft.Y
}
var points []*geo.Point
for _, s := range e.Sections {
points = append(points, &geo.Point{
X: parentX + s.Start.X,
Y: parentY + s.Start.Y,
})
for _, bp := range s.BendPoints {
points = append(points, &geo.Point{
X: parentX + bp.X,
Y: parentY + bp.Y,
})
}
points = append(points, &geo.Point{
X: parentX + s.End.X,
Y: parentY + s.End.Y,
})
}
2022-12-18 04:40:56 +00:00
startIndex, endIndex := 0, len(points)-1
srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Attributes.Shape.Value)], edge.Src.Box)
dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Attributes.Shape.Value)], edge.Dst.Box)
// trace the edge to the specific shape's border
points[startIndex] = shape.TraceToShapeBorder(srcShape, points[startIndex], points[startIndex+1])
points[endIndex] = shape.TraceToShapeBorder(dstShape, points[endIndex], points[endIndex-1])
2022-11-11 19:43:56 +00:00
if edge.Attributes.Label.Value != "" {
2022-12-20 23:31:13 +00:00
edge.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
2022-11-11 19:43:56 +00:00
}
edge.Route = points
}
2023-03-14 21:11:51 +00:00
deleteBends(g)
2022-11-11 19:43:56 +00:00
return nil
}
2023-03-14 21:11:51 +00:00
// deleteBends is a shim for ELK to delete unnecessary bends
// see https://github.com/terrastruct/d2/issues/1030
func deleteBends(g *d2graph.Graph) {
// Get rid of S-shapes at the source and the target
for _, isSource := range []bool{true, false} {
for ei, e := range g.Edges {
if len(e.Route) < 4 {
continue
}
2023-03-14 21:54:30 +00:00
if e.Src == e.Dst {
continue
}
2023-03-14 21:11:51 +00:00
var endpoint *d2graph.Object
var start *geo.Point
var corner *geo.Point
var end *geo.Point
if isSource {
start = e.Route[0]
corner = e.Route[1]
end = e.Route[2]
endpoint = e.Src
} else {
start = e.Route[len(e.Route)-1]
corner = e.Route[len(e.Route)-2]
end = e.Route[len(e.Route)-3]
endpoint = e.Dst
}
isHorizontal := start.Y == corner.Y
// Make sure it's still attached
if isHorizontal {
if end.Y <= endpoint.TopLeft.Y+10 {
continue
}
if end.Y >= endpoint.TopLeft.Y+endpoint.Height-10 {
continue
}
} else {
if end.X <= endpoint.TopLeft.X+10 {
continue
}
if end.X >= endpoint.TopLeft.X+endpoint.Width-10 {
continue
}
}
var newStart *geo.Point
if isHorizontal {
newStart = geo.NewPoint(start.X, end.Y)
} else {
newStart = geo.NewPoint(end.X, start.Y)
}
2023-03-14 21:54:30 +00:00
endpointShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(endpoint.Attributes.Shape.Value)], endpoint.Box)
newStart = shape.TraceToShapeBorder(endpointShape, newStart, end)
2023-03-14 21:11:51 +00:00
// Check that the new segment doesn't collide with anything new
oldSegment := geo.NewSegment(start, corner)
newSegment := geo.NewSegment(newStart, end)
oldIntersects := countObjectIntersects(g, *oldSegment)
newIntersects := countObjectIntersects(g, *newSegment)
if newIntersects > oldIntersects {
continue
}
2023-03-14 21:54:30 +00:00
oldCrossingsCount, oldOverlapsCount, oldCloseOverlapsCount := countEdgeIntersects(g, g.Edges[ei], *oldSegment)
newCrossingsCount, newOverlapsCount, newCloseOverlapsCount := countEdgeIntersects(g, g.Edges[ei], *newSegment)
2023-03-14 21:11:51 +00:00
2023-03-14 21:54:30 +00:00
if newCrossingsCount > oldCrossingsCount {
continue
}
if newOverlapsCount > oldOverlapsCount {
continue
}
if newCloseOverlapsCount > oldCloseOverlapsCount {
2023-03-14 21:11:51 +00:00
continue
}
// commit
if isSource {
g.Edges[ei].Route = append(
[]*geo.Point{newStart},
e.Route[3:]...,
)
} else {
g.Edges[ei].Route = append(
e.Route[:len(e.Route)-3],
newStart,
)
}
}
}
}
func countObjectIntersects(g *d2graph.Graph, s geo.Segment) int {
count := 0
for _, o := range g.Objects {
if o.Intersects(s, float64(edge_node_spacing)) {
count++
}
}
return count
}
// countEdgeIntersects counts both crossings AND getting too close to a parallel segment
2023-03-14 21:54:30 +00:00
func countEdgeIntersects(g *d2graph.Graph, sEdge *d2graph.Edge, s geo.Segment) (int, int, int) {
2023-03-14 21:11:51 +00:00
isHorizontal := s.Start.Y == s.End.Y
2023-03-14 21:54:30 +00:00
crossingsCount := 0
overlapsCount := 0
closeOverlapsCount := 0
2023-03-14 21:11:51 +00:00
for _, e := range g.Edges {
if e == sEdge {
continue
}
for i := 0; i < len(e.Route)-1; i++ {
otherS := geo.NewSegment(e.Route[i], e.Route[i+1])
otherIsHorizontal := otherS.Start.Y == otherS.End.Y
if isHorizontal == otherIsHorizontal {
2023-03-14 21:54:30 +00:00
if s.Overlaps(*otherS, !isHorizontal, 0.) {
if isHorizontal {
if math.Abs(s.Start.Y-otherS.Start.Y) < float64(edge_node_spacing)/2. {
overlapsCount++
if math.Abs(s.Start.Y-otherS.Start.Y) < float64(edge_node_spacing)/4. {
closeOverlapsCount++
}
}
} else {
if math.Abs(s.Start.X-otherS.Start.X) < float64(edge_node_spacing)/2. {
overlapsCount++
if math.Abs(s.Start.X-otherS.Start.X) < float64(edge_node_spacing)/4. {
closeOverlapsCount++
}
}
}
2023-03-14 21:11:51 +00:00
}
} else {
if s.Intersects(*otherS) {
2023-03-14 21:54:30 +00:00
crossingsCount++
2023-03-14 21:11:51 +00:00
}
}
}
}
2023-03-14 21:54:30 +00:00
return crossingsCount, overlapsCount, closeOverlapsCount
2023-03-14 21:11:51 +00:00
}