package placement
import (
"context"
"math"
"github.com/pancsta/d2/d2layouts/d2talalayout/internal/layoutgraph"
"github.com/pancsta/d2/d2layouts/d2talalayout/internal/placementcost"
"github.com/pancsta/d2/lib/geo"
)
func alignAxes(ctx context .Context , g *layoutgraph .Graph , txn *layoutgraph .Transaction ) (bool , error ) {
if err := ctx .Err (); err != nil {
return false , err
}
changed := false
fixedNodes := g .FixedNodes ()
isTreeEdge := g .TreeEdgeMap ()
g .AddIsolatedTreeEdges (isTreeEdge )
excluded := make ([]*layoutgraph .Node , 0 , len (g .NodeToTree )+len (fixedNodes )+1 )
for _ , n := range g .Nodes {
if _ , has := g .NodeToTree [n ]; has {
excluded = append (excluded , n )
}
}
excluded = append (excluded , fixedNodes ...)
excluded = append (excluded , nil )
var edgeAbductions []*layoutgraph .EdgeAbduction
for _ , seq := range g .Sequences {
edgeAbductions = append (edgeAbductions , seq .EdgeAbductions ...)
}
hasTableColumns := false
initialLength , err := placementcost .EdgeLength (ctx , g , placementcost .EdgeLengthOptions {EdgeAbductions : edgeAbductions , IncludeNodeSizes : true , EnforceMinimumGap : false , PenalizeDirection : true })
if err != nil {
return false , err
}
nInitialAlignedTableColumns := 0
for _ , e := range g .Edges {
if e .HasTableColumn () {
hasTableColumns = true
if e .IsAxisAligned () {
nInitialAlignedTableColumns ++
}
}
}
for _ , e := range g .Edges {
if _ , is := isTreeEdge [e ]; is {
continue
}
if e .IsAxisAligned () {
continue
}
if e .From .Hierarchy != nil || e .To .Hierarchy != nil {
continue
}
txn .Clear ()
if err := txn .UpdateState (); err != nil {
return false , err
}
xDiff , yDiff := alignmentDeltas (e , g )
var dxBest , dyBest float64
var bestNodes []*layoutgraph .Node
bestEdgeLength , err := placementcost .EdgeLength (ctx , g , placementcost .EdgeLengthOptions {EdgeAbductions : edgeAbductions , IncludeNodeSizes : true , EnforceMinimumGap : false , PenalizeDirection : true })
if err != nil {
return false , err
}
containerAlignment , err := placementcost .ContainerAlignmentCost (ctx , g )
if err != nil {
return false , err
}
bestEdgeLength += containerAlignment
{
excluded [len (excluded )-1 ] = e .To
connectedNodes := e .From .ConnectedNodeSet (excluded , g )
edgeLength , dx , dy , err := tryMove (ctx , txn , g , e , connectedNodes , edgeAbductions , xDiff , yDiff )
if err != nil {
return false , err
}
if geo .PrecisionCompare (edgeLength , bestEdgeLength , geo .PRECISION ) < 0 {
bestEdgeLength = edgeLength
dxBest , dyBest = dx , dy
bestNodes = connectedNodes
} else if e .HasTableColumn () && geo .PrecisionCompare (edgeLength , bestEdgeLength , geo .PRECISION ) == 0 {
bestEdgeLength = edgeLength
dxBest , dyBest = dx , dy
bestNodes = connectedNodes
}
}
{
excluded [len (excluded )-1 ] = e .From
connectedNodes := e .To .ConnectedNodeSet (excluded , g )
edgeLength , dx , dy , err := tryMove (ctx , txn , g , e , connectedNodes , edgeAbductions , -xDiff , -yDiff )
if err != nil {
return false , err
}
if geo .PrecisionCompare (edgeLength , bestEdgeLength , geo .PRECISION ) < 0 {
dxBest , dyBest = dx , dy
bestNodes = connectedNodes
} else if e .HasTableColumn () && geo .PrecisionCompare (edgeLength , bestEdgeLength , geo .PRECISION ) == 0 {
dxBest , dyBest = dx , dy
bestNodes = connectedNodes
}
}
if len (bestNodes ) > 0 {
txn .AddOp (func () error {
changed = true
for _ , n := range bestNodes {
n .Translate (dxBest , dyBest )
}
return nil
})
if err := txn .Commit (ctx ); err != nil {
return false , err
}
}
}
if hasTableColumns && changed {
nAlignedTableColumns := 0
for _ , e := range g .Edges {
if e .HasTableColumn () && e .IsAxisAligned () {
nAlignedTableColumns ++
}
}
if nAlignedTableColumns <= nInitialAlignedTableColumns {
edgeLength , err := placementcost .EdgeLength (ctx , g , placementcost .EdgeLengthOptions {EdgeAbductions : edgeAbductions , IncludeNodeSizes : true , EnforceMinimumGap : false , PenalizeDirection : true })
if err != nil {
return false , err
}
containerAlignment , err := placementcost .ContainerAlignmentCost (ctx , g )
if err != nil {
return false , err
}
edgeLength += containerAlignment
changed = geo .PrecisionCompare (edgeLength , initialLength , geo .PRECISION ) == -1
}
}
return changed , nil
}
func alignmentDeltas(e *layoutgraph .Edge , g *layoutgraph .Graph ) (float64 , float64 ) {
if e .HasTableColumn () {
fromPort , toPort , hasFromPort , hasToPort , _ := e .FacingTablePortValues (nil , nil )
if hasFromPort && hasToPort {
return 0 , toPort .Y - fromPort .Y
} else if hasFromPort {
return 0 , (e .To .TopLeft .Y + e .To .Height /2 ) - fromPort .Y
} else if hasToPort {
return 0 , toPort .Y - (e .From .TopLeft .Y + e .From .Height /2 )
} else {
return e .To .TopLeft .X - e .From .TopLeft .X , 0
}
}
from := e .From
if seq , is := g .Sequences [from ]; is {
from = seq .AbductedNodeByEdge (e )
}
to := e .To
if seq , is := g .Sequences [to ]; is {
to = seq .AbductedNodeByEdge (e )
}
yDiff := math .Round ((to .TopLeft .Y + to .Height /2 ) - (from .TopLeft .Y + from .Height /2 ))
xDiff := math .Round ((to .TopLeft .X + to .Width /2 ) - (from .TopLeft .X + from .Width /2 ))
if e .From .Orientation (e .To ).IsHorizontal () {
xDiff = 0
} else if e .From .Orientation (e .To ).IsVertical () {
yDiff = 0
} else {
xDiff2 , yDiff2 := e .From .OrthogonalDistanceTo (e .To )
if xDiff2 < layoutgraph .ConnectedNodeGap {
yDiff = 0
}
if yDiff2 < layoutgraph .ConnectedNodeGap {
xDiff = 0
}
}
return xDiff , yDiff
}
func tryMove(ctx context .Context , txn *layoutgraph .Transaction , g *layoutgraph .Graph , e *layoutgraph .Edge , connectedNodes []*layoutgraph .Node , edgeAbductions []*layoutgraph .EdgeAbduction , xDiff , yDiff float64 ) (bestLen , dx , dy float64 , err error ) {
bestEdgeLength := math .Inf (1 )
var bestAttempt geo .Point
attempts := make ([]*geo .Point , 0 , 2 )
if geo .PrecisionCompare (yDiff , 0.0 , geo .PRECISION ) != 0 {
attempts = append (attempts , geo .NewPoint (0 , yDiff ))
}
if geo .PrecisionCompare (xDiff , 0.0 , geo .PRECISION ) != 0 {
attempts = append (attempts , geo .NewPoint (xDiff , 0 ))
}
for _ , attempt := range attempts {
txn .AddOp (func () error {
if !attemptShift (g , e , connectedNodes , attempt .X , attempt .Y ) {
return layoutgraph .ErrInvalidCandidate
}
return nil
})
if err := txn .Commit (ctx ); err == nil {
edgeLength , scoreErr := placementcost .EdgeLength (ctx , g , placementcost .EdgeLengthOptions {EdgeAbductions : edgeAbductions , IncludeNodeSizes : true , EnforceMinimumGap : false , PenalizeDirection : true })
if scoreErr != nil {
txn .Rollback ()
txn .Clear ()
return 0 , 0 , 0 , scoreErr
}
containerAlignment , scoreErr := placementcost .ContainerAlignmentCost (ctx , g )
if scoreErr != nil {
txn .Rollback ()
txn .Clear ()
return 0 , 0 , 0 , scoreErr
}
edgeLength += containerAlignment
if geo .PrecisionCompare (edgeLength , bestEdgeLength , geo .PRECISION ) < 1 {
bestEdgeLength = edgeLength
bestAttempt = *attempt
}
} else if !layoutgraph .IsCandidateRejection (err ) {
return 0 , 0 , 0 , err
}
txn .Clear ()
txn .Rollback ()
}
return bestEdgeLength , bestAttempt .X , bestAttempt .Y , nil
}
func attemptShift(g *layoutgraph .Graph , aligningEdge *layoutgraph .Edge , nodes []*layoutgraph .Node , x , y float64 ) bool {
for _ , n := range nodes {
n .Translate (x , y )
}
if !withinMaxSize (g ) {
return false
}
if intersectsOtherNode (g , aligningEdge .From , aligningEdge .To ) {
return false
}
return true
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .