diff --git a/d2graph/layout.go b/d2graph/layout.go index 43342daa1..25b66795b 100644 --- a/d2graph/layout.go +++ b/d2graph/layout.go @@ -141,6 +141,118 @@ func (obj *Object) ShiftDescendants(dx, dy float64) { }) } +// ShiftStart moves the starting point of the route by delta either horizontally or vertically +// if subsequent points are in line with the movement, they will be removed (unless it is the last point) +// start end +// . ├────┼────┼───┼────┼───┤ before +// . ├──dx──► +// . ├──┼───┼────┼───┤ after +func (edge *Edge) ShiftStart(delta float64, isHorizontal bool) { + position := func(p *geo.Point) float64 { + if isHorizontal { + return p.X + } + return p.Y + } + + start := edge.Route[0] + next := edge.Route[1] + isIncreasing := position(start) < position(next) + if isHorizontal { + start.X += delta + } else { + start.Y += delta + } + + if isIncreasing == (delta < 0) { + // nothing more to do when moving away from the next point + return + } + + isAligned := func(p *geo.Point) bool { + if isHorizontal { + return p.Y == start.Y + } + return p.X == start.X + } + isPastStart := func(p *geo.Point) bool { + if delta > 0 { + return position(p) < position(start) + } else { + return position(p) > position(start) + } + } + + needsRemoval := false + toRemove := make([]bool, len(edge.Route)) + for i := 1; i < len(edge.Route)-1; i++ { + if !isAligned(edge.Route[i]) { + break + } + if isPastStart(edge.Route[i]) { + toRemove[i] = true + needsRemoval = true + } + } + if needsRemoval { + edge.Route = geo.RemovePoints(edge.Route, toRemove) + } +} + +// ShiftEnd moves the ending point of the route by delta either horizontally or vertically +// if prior points are in line with the movement, they will be removed (unless it is the first point) +func (edge *Edge) ShiftEnd(delta float64, isHorizontal bool) { + position := func(p *geo.Point) float64 { + if isHorizontal { + return p.X + } + return p.Y + } + + end := edge.Route[len(edge.Route)-1] + prev := edge.Route[len(edge.Route)-2] + isIncreasing := position(prev) < position(end) + if isHorizontal { + end.X += delta + } else { + end.Y += delta + } + + if isIncreasing == (delta > 0) { + // nothing more to do when moving away from the next point + return + } + + isAligned := func(p *geo.Point) bool { + if isHorizontal { + return p.Y == end.Y + } + return p.X == end.X + } + isPastEnd := func(p *geo.Point) bool { + if delta > 0 { + return position(p) < position(end) + } else { + return position(p) > position(end) + } + } + + needsRemoval := false + toRemove := make([]bool, len(edge.Route)) + for i := len(edge.Route) - 2; i > 0; i-- { + if !isAligned(edge.Route[i]) { + break + } + if isPastEnd(edge.Route[i]) { + toRemove[i] = true + needsRemoval = true + } + } + if needsRemoval { + edge.Route = geo.RemovePoints(edge.Route, toRemove) + } +} + // GetModifierElementAdjustments returns width/height adjustments to account for shapes with 3d or multiple func (obj *Object) GetModifierElementAdjustments() (dx, dy float64) { if obj.Is3D() { diff --git a/d2layouts/d2dagrelayout/layout.go b/d2layouts/d2dagrelayout/layout.go index e033c61e7..97734237d 100644 --- a/d2layouts/d2dagrelayout/layout.go +++ b/d2layouts/d2dagrelayout/layout.go @@ -384,7 +384,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err if isHorizontal && e.Src.Parent != g.Root && e.Dst.Parent != g.Root { moveWholeEdge = true } else { - e.Route[0].Y += stepSize + e.ShiftStart(stepSize, false) } } } @@ -393,7 +393,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err if isHorizontal && e.Dst.Parent != g.Root && e.Src.Parent != g.Root { moveWholeEdge = true } else { - e.Route[len(e.Route)-1].Y += stepSize + e.ShiftEnd(stepSize, false) } } } diff --git a/lib/geo/point.go b/lib/geo/point.go index 0b0a4ff51..ab8e034a0 100644 --- a/lib/geo/point.go +++ b/lib/geo/point.go @@ -305,3 +305,22 @@ func (p *Point) TruncateDecimals() { p.X = TruncateDecimals(p.X) p.Y = TruncateDecimals(p.Y) } + +// RemovePoints returns a new Points slice without the points in toRemove +func RemovePoints(points Points, toRemove []bool) Points { + newLen := len(points) + for _, should := range toRemove { + if should { + newLen-- + } + } + + without := make([]*Point, 0, newLen) + for i := 0; i < len(points); i++ { + if toRemove[i] { + continue + } + without = append(without, points[i]) + } + return without +}