package placementcost

import (
	
	
	

	
	
	
)

// EdgeLengthOptions controls which geometric penalties contribute to edge length.
type EdgeLengthOptions struct {
	// EdgeAbductions supplies temporary endpoint ownership during placement.
	EdgeAbductions []*layoutgraph.EdgeAbduction
	// IncludeNodeSizes measures distances between node boundaries instead of origins.
	IncludeNodeSizes bool
	// EnforceMinimumGap penalizes connected nodes that are too close together.
	EnforceMinimumGap bool
	// PenalizeDirection adds cost when an edge departs from its preferred direction.
	PenalizeDirection bool
}

// NodesEdgeLength sums the edge-length cost rooted at each node in order.
func ( context.Context,  layoutgraph.Nodes,  EdgeLengthOptions) (float64, error) {
	if  := .Err();  != nil {
		return 0, fmt.Errorf("EdgeLength: %w", )
	}
	 := 0.0
	for ,  := range  {
		,  := NodeEdgeLength(, , )
		if  != nil {
			return 0, 
		}
		 += 
	}
	if  := .Err();  != nil {
		return 0, fmt.Errorf("EdgeLength: %w", )
	}
	return , nil
}

type edgeScratch struct {
	used             []bool
	nRepl            []*layoutgraph.Node
	aRepl            []*layoutgraph.Node
	clusterOrder     []*layoutgraph.Node
	edgeAbductions   []*layoutgraph.EdgeAbduction
	obstructionSets  [][]*layoutgraph.Node
	obstructionAdded map[*layoutgraph.Node]struct{}
	labeledEdgeCount map[labeledEdgePair]int
}

type labeledEdgePair struct {
	from layoutgraph.EntityID
	to   layoutgraph.EntityID
}

var scratchPool = typedpool.New(func() *edgeScratch { return new(edgeScratch) })

const maxPooledEdgeScratchEntries = 4096

func putEdgeScratch( *edgeScratch) {
	clear(.used)
	.used = .used[:0]
	clear(.nRepl)
	.nRepl = .nRepl[:0]
	clear(.aRepl)
	.aRepl = .aRepl[:0]
	if cap(.clusterOrder) > maxPooledEdgeScratchEntries {
		.clusterOrder = nil
	} else {
		clear(.clusterOrder[:cap(.clusterOrder)])
		.clusterOrder = .clusterOrder[:0]
	}
	if cap(.edgeAbductions) > maxPooledEdgeScratchEntries {
		.edgeAbductions = nil
	} else {
		clear(.edgeAbductions[:cap(.edgeAbductions)])
		.edgeAbductions = .edgeAbductions[:0]
	}
	if cap(.obstructionSets) > maxPooledEdgeScratchEntries {
		.obstructionSets = nil
		.obstructionAdded = nil
	} else {
		clear(.obstructionSets[:cap(.obstructionSets)])
		.obstructionSets = .obstructionSets[:0]
		clear(.obstructionAdded)
	}
	if len(.labeledEdgeCount) > maxPooledEdgeScratchEntries {
		.labeledEdgeCount = nil
	} else {
		clear(.labeledEdgeCount)
	}
	scratchPool.Put()
}

func checkScoringSlice( context.Context,  int) error {
	for  := 0;  < ;  += scoringCancellationCheckInterval {
		if  := scoringCancellationError(, );  != nil {
			return 
		}
	}
	return nil
}

// Helper function to calculate midpoint and check semi-diagonal for horizontal directions
func calculateHorizontalMidpoint(,  *layoutgraph.Node) (float64, bool) {
	 := math.Min(.TopLeft.X+.Width, .TopLeft.X+.Width)
	 := math.Max(.TopLeft.X, .TopLeft.X)
	 := ( + ) / 2.
	 := math.Abs(.TopLeft.X-.TopLeft.X) > SideEdgeSpacing ||
		math.Abs(.TopLeft.X+.Width-(.TopLeft.X+.Width)) > SideEdgeSpacing
	return , 
}

// Helper function to calculate midpoint and check semi-diagonal for vertical directions
func calculateVerticalMidpoint(,  *layoutgraph.Node) (float64, bool) {
	 := math.Min(.TopLeft.Y+.Height, .TopLeft.Y+.Height)
	 := math.Max(.TopLeft.Y, .TopLeft.Y)
	 := ( + ) / 2.
	 := math.Abs(.TopLeft.Y-.TopLeft.Y) > SideEdgeSpacing ||
		math.Abs(.TopLeft.Y+.Height-(.TopLeft.Y+.Height)) > SideEdgeSpacing
	return , 
}

// NodeEdgeLength evaluates the incident-edge cost rooted at node.
func ( context.Context,  *layoutgraph.Node,  EdgeLengthOptions) (float64, error) {
	if  := .Err();  != nil {
		return 0, fmt.Errorf("EdgeLength: %w", )
	}
	 := scratchPool.Get()
	defer putEdgeScratch()
	,  := prepareNodeEdgeLength(, , , )
	if  != nil {
		return 0, 
	}
	return evaluateNodeEdgeLength(, , , , )
}

func prepareNodeEdgeLength( context.Context,  *layoutgraph.Node,  EdgeLengthOptions,  *edgeScratch) (edgeLengthStatic, error) {
	 := .IncludeNodeSizes
	 := .PenalizeDirection

	 := .EdgeAbductions
	if  == nil {
		.clusterOrder = .clusterOrder[:0]
		for  := range .Graph.Clusters {
			.clusterOrder = append(.clusterOrder, )
		}
		layoutgraph.SortNodesByID(.clusterOrder)

		.edgeAbductions = .edgeAbductions[:0]
		for ,  := range .clusterOrder {
			if  := scoringCancellationError(, );  != nil {
				return edgeLengthStatic{}, 
			}
			 := .Graph.Clusters[]
			.edgeAbductions = append(.edgeAbductions, .EdgeAbductions...)
		}
		 = .edgeAbductions
	}

	if cap(.used) < len() {
		.used = make([]bool, len())
	} else {
		.used = .used[:len()]
	}
	 := .used
	// putEdgeScratch clears retained storage before pooling it; this scan keeps
	// cancellation bounded without clearing the same memory twice.
	if  := checkScoringSlice(, len());  != nil {
		return edgeLengthStatic{}, 
	}

	if cap(.nRepl) < len(.Edges) {
		.nRepl = make([]*layoutgraph.Node, len(.Edges))
	} else {
		.nRepl = .nRepl[:len(.Edges)]
	}
	 := .nRepl
	if  := checkScoringSlice(, len());  != nil {
		return edgeLengthStatic{}, 
	}

	if cap(.aRepl) < len(.Edges) {
		.aRepl = make([]*layoutgraph.Node, len(.Edges))
	} else {
		.aRepl = .aRepl[:len(.Edges)]
	}
	 := .aRepl
	if  := checkScoringSlice(, len());  != nil {
		return edgeLengthStatic{}, 
	}

	for ,  := range .Edges {
		if  := scoringCancellationError(, );  != nil {
			return edgeLengthStatic{}, 
		}
		 := .Adjacent()
		// These are sometimes replaced by container children
		 := 
		 := 

		if  {
			for ,  := range  {
				if  := scoringCancellationError(, );  != nil {
					return edgeLengthStatic{}, 
				}
				if [] {
					continue
				}
				if (.CurrentFrom == ) && (.CurrentTo == ) {
					[] = true
					if .OriginallyTo != nil {
						 = .OriginallyTo
					}
					if .OriginallyFrom != nil {
						 = .OriginallyFrom
					}
					break
				}
				if (.CurrentFrom == ) && (.CurrentTo == ) {
					[] = true
					if .OriginallyFrom != nil {
						 = .OriginallyFrom
					}
					if .OriginallyTo != nil {
						 = .OriginallyTo
					}
					break
				}
			}
		}

		[] = 
		[] = 
	}

	 := .ContainerDirection()
	var  float64
	if  {
		if  != geo.NONE {
			// heavily weight specified direction with sizes
			if  {
				 = 6
			} else {
				 = 1.5
			}
		} else {
			if .IsTable() {
				 = geo.Right
				 = .5
			} else {
				// Check for nodes with more than 2 labeled edges to same node
				// Track both total edges and how many are "from" this node
				 := make(map[*layoutgraph.Node]int)
				 := make(map[*layoutgraph.Node]int)
				 := false
				var  *layoutgraph.Node

				if  {
					for ,  := range .Edges {
						if  := scoringCancellationError(, );  != nil {
							return edgeLengthStatic{}, 
						}
						 := []
						// If this node is a container and the replacement is a child, the edge isn't actually connected
						if  !=  {
							continue
						}
						 := []
						if  != .Adjacent() {
							continue
						}
						if .Label != nil {
							if  ==  {
								continue
							}
							[]++
							 := .From
							if , ,  := .DirectedEndpoints();  {
								 = 
							}
							if  ==  {
								[]++
							}
							if [] > 2 {
								 = true
								 = 
							}
						}
					}
				}

				if  {
					// If more edges are from this node to the target, use Left orientation
					// Otherwise use Right orientation
					if [] > []/2 {
						 = geo.Left
					} else {
						 = geo.Right
					}
					 = 10
				} else {
					// Default to bottom right if no multiple labeled edges
					 = geo.BottomRight
					 = .3
				}
			}

			if .IsClusterVessel() {
				 = .2
				switch .Graph.Clusters[].Arrangement {
				case layoutgraph.Row:
					 = geo.Bottom
				case layoutgraph.Column:
					 = geo.Right
				}
			}
		}
	}

	// get the edges between each nodeReplacement and adjacentNodeReplacement
	for ,  := range .Edges {
		if  := scoringCancellationError(, );  != nil {
			return edgeLengthStatic{}, 
		}
		if ! || .Label == nil {
			continue
		}
		if .labeledEdgeCount == nil {
			.labeledEdgeCount = make(map[labeledEdgePair]int)
		}
		 := []
		 := []
		 := labeledEdgePair{from: .ID, to: .ID}
		.labeledEdgeCount[]++
	}

	return edgeLengthStatic{direction: , directionFactor: }, nil
}

func evaluateNodeEdgeLength( context.Context,  *layoutgraph.Node,  EdgeLengthOptions,  *edgeScratch,  edgeLengthStatic) (float64, error) {
	 := .IncludeNodeSizes
	 := .PenalizeDirection
	 := 0.0
	if .IncludeNodeSizes && .EnforceMinimumGap {
		 = .Graph.CellSize
	}
	,  := .nRepl, .aRepl
	,  := .direction, .directionFactor
	 := directionCompass()
	 := 0.0
	 := .EffectiveContainer()
	for ,  := range .Edges {
		if  := scoringCancellationError(, );  != nil {
			return 0, 
		}
		 := .Adjacent()
		// Used during initialization and when adjacent is outer container
		if .TopLeft == nil {
			continue
		}
		 := []
		 := []

		// If another node obstructs the direct path between two nodes, a more accurate heuristic penalizes this
		//      +---+
		//      |   |
		// +--->+   |
		// |    +---+
		// |
		// |
		// |
		// |    +---+
		// |    |   |
		// |    |   |
		// |    +-+-+
		// |      ^
		// |      |
		// |    +-+-+
		// |    |   |
		// +----+   |
		//      +---+

		var  *geo.Point
		var  *geo.Point

		// Lazy calculation of centers and edge direction - only when needed
		var ,  *geo.Point
		var  geo.Orientation
		var  bool
		var  geo.Orientation

		 := func() {
			if  == nil {
				 = .Center()
				 = .Center()
			}
		}

		 := func() geo.Orientation {
			if ! {
				if  {
					 = .Orientation()
				} else {
					 = sizelessOrientation(, )
				}
				 = true
			}
			return 
		}

		 := false
		 := false
		if  {
			 = ()
			if  == geo.NONE {
				// This is okay
				continue
			}
			// When the direction from A to C is Right, B obstructs the ideal straight line
			// But A can still make a perfectly good L shape
			// ┌─────────┐
			// │         │
			// │         │
			// │         │
			// │         │
			// │         ├──────────────────────────────────┐
			// │         │                                  │
			// │         │                                  │
			// │    A    │                                  │
			// │         │        ┌─────────┐           ┌───▼────┐
			// │         │        │   B     │           │        │
			// │         ├────────┼─────────┼──────────►│   C    │
			// │         │        │         │           │        │
			// │         │        │         │           │        │
			// └─────────┘        └─────────┘           └────────┘
			// It should count as a turnCost for this B, but if B was covering the other L-shape too, it should count as 2
			// We call this a "semi-diagonal"
			 = ()
			if .IsDiagonal() {
				 = true
			}
			switch  {
			case geo.Top:
				,  := calculateHorizontalMidpoint(, )
				 = geo.NewPoint(, .TopLeft.Y+.Height)
				 = geo.NewPoint(, .TopLeft.Y)
				 = 
			case geo.Bottom:
				,  := calculateHorizontalMidpoint(, )
				 = geo.NewPoint(, .TopLeft.Y)
				 = geo.NewPoint(, .TopLeft.Y+.Height)
				 = 
			case geo.Left:
				,  := calculateVerticalMidpoint(, )
				 = geo.NewPoint(.TopLeft.X+.Width, )
				 = geo.NewPoint(.TopLeft.X, )
				 = 
			case geo.Right:
				,  := calculateVerticalMidpoint(, )
				 = geo.NewPoint(.TopLeft.X, )
				 = geo.NewPoint(.TopLeft.X+.Width, )
				 = 
			case geo.TopLeft, geo.TopRight, geo.BottomLeft, geo.BottomRight:
				()
				 = 
				 = 
			}
			if .Cluster != nil || .Cluster != nil {
				 = false
				 = false
				()
				// if it's diagonal, you still have to use the boundaries, or else a line passes through innocent siblings
				switch  {
				case geo.Top, geo.TopLeft, geo.TopRight:
					 = geo.NewPoint(.X, .TopLeft.Y+.Height)
					 = geo.NewPoint(.X, .TopLeft.Y)
				case geo.Bottom, geo.BottomLeft, geo.BottomRight:
					 = geo.NewPoint(.X, .TopLeft.Y)
					 = geo.NewPoint(.X, .TopLeft.Y+.Height)
				case geo.Left:
					 = geo.NewPoint(.TopLeft.X+.Width, .Y)
					 = geo.NewPoint(.TopLeft.X, .Y)
				case geo.Right:
					 = geo.NewPoint(.TopLeft.X, .Y)
					 = geo.NewPoint(.TopLeft.X+.Width, .Y)
				}
			}
		} else {
			 = .TopLeft
			 = .TopLeft
		}

		// ... and the distance is a straight line
		var  float64
		if  && .HasTableColumn() {
			 := 
			 := 
			if  != .From {
				,  = , 
			}
			 = distanceBetweenTableColumns(.Graph, , , )
		} else {
			if .Cluster != nil || .Cluster != nil {
				//  I want to use the vessel of the cluster, otherwise during node placement, a connected node will jump back and forth between optimizing each one.
				if .Cluster != nil {
					 = placementDistance(.Cluster.Vessel, , )
				} else {
					 = placementDistance(.Cluster.Vessel, , )
				}
			} else if  {
				// the distance is the sum of that L shape
				 = distanceToPoint(, geo.NewPoint(.X, .Y), ) +
					distanceToPoint(, geo.NewPoint(.X, .Y), )
				if  {
					 += .Graph.TurnCost()
				}
			} else {
				 = placementDistance(, , )
			}
		}

		if  && .IsContainer() && .IsContainer() &&  == .EffectiveContainer() {
			 := 0.
			// If the widths are roughly equal in size, we want alignment on either one of the sides or the center
			if math.Min(.Width, .Width) > math.Max(.Width, .Width)*0.75 {
				if (.TopLeft.X != .TopLeft.X) && (.TopLeft.X+.Width != .TopLeft.X+.Width) && ((.TopLeft.X+.Width)/2 != (.TopLeft.X+.Width)/2) {
					 = .Graph.TurnCost()
				}
			}

			if math.Min(.Height, .Height) > math.Max(.Height, .Height)*0.75 {
				if (.TopLeft.Y != .TopLeft.Y) && (.TopLeft.Y+.Height != .TopLeft.Y+.Height) && ((.TopLeft.Y+.Height)/2 != (.TopLeft.Y+.Height)/2) {
					 = .Graph.TurnCost()
				}
			}

			 += 
		}

		if  && !.IsBetweenTableColumns() {
			 := scoringNodeBounds().including(scoringNodeBounds())
			// for diagonals and semi-diagonals we want to see if both corners are covered by any obstructions
			 := false
			 := false

			 := func( *layoutgraph.Node) bool {
				// Obstructions are initialized as siblings of endpoints, but we still need to eliminate cases
				// If the obstruction is one of the end nodes (already checked)
				// If the obstruction is a child of one of the end nodes (applicable when child connected to its container)
				if .IsDescendantOf() || .IsDescendantOf() {
					return false
				}
				if  {
					if ! &&
						(.PassesThrough(, geo.NewPoint(.X, .Y)) ||
							.PassesThrough(, geo.NewPoint(.X, .Y))) {
						 = true
					}
					if ! &&
						(.PassesThrough(, geo.NewPoint(.X, .Y)) ||
							.PassesThrough(, geo.NewPoint(.X, .Y))) {
						 = true
					}
					if  &&  {
						return true
					}
				} else {
					if .PassesThrough(, ) {
						return true
					}
				}
				return false
			}

			// The L shape that goes clockwise
			 := false
			// The L shape that goes counterclockwise
			 := false

			if  {
				 = ()
				switch  {
				case geo.Left, geo.Right:
					if math.Abs(.TopLeft.Y-.TopLeft.Y) <= SideEdgeSpacing {
						 = true
					} else if math.Abs(.TopLeft.Y+.Height-(.TopLeft.Y+.Height)) <= SideEdgeSpacing {
						 = true
					}
				case geo.Top, geo.Bottom:
					if math.Abs(.TopLeft.X-.TopLeft.X) <= SideEdgeSpacing {
						 = true
					} else if math.Abs(.TopLeft.X+.Width-(.TopLeft.X+.Width)) <= SideEdgeSpacing {
						 = true
					}
				}
			}

			// Only used for semi-diagonals
			 := func( *layoutgraph.Node) bool {
				if .IsDescendantOf() || .IsDescendantOf() {
					return false
				}
				()
				 = ()
				switch  {
				case geo.Top:
					 := math.Max(.TopLeft.X, .TopLeft.X)
					 := math.Min(.TopLeft.X+.Width, .TopLeft.X+.Width)
					if ! {
						 := math.Min(.TopLeft.X, .TopLeft.X)
						 :=  + math.Abs(-)/2.
						// ┌────────────────┐
						// │                │
						// │                │
						// │                │
						// └────────────────┘
						//
						//
						//
						//          ┌──────┐
						//          │      │
						//          │      │
						//          └──────┘
						if .TopLeft.X < .TopLeft.X {
							if .PassesThrough(geo.NewPoint(, .Y), geo.NewPoint(, .Y)) ||
								.PassesThrough(geo.NewPoint(, .Y), ) {
								 = true
							}
						}
						//        ┌──────┐
						//        │      │
						//        │      │
						//        └──────┘
						//
						//
						//
						// ┌────────────────┐
						// │                │
						// │                │
						// │                │
						// └────────────────┘
						//
						if .TopLeft.X > .TopLeft.X {
							if .PassesThrough(, geo.NewPoint(, .Y)) ||
								.PassesThrough(geo.NewPoint(, .Y), geo.NewPoint(, .Y)) {
								 = true
							}
						}
					}
					if ! {
						 := math.Max(.TopLeft.X+.Width, .TopLeft.X+.Width)
						 :=  - math.Abs(-)/2.
						// ┌────────────────┐
						// │                │
						// │                │
						// │                │
						// └────────────────┘
						//
						//
						//
						//   ┌──────┐
						//   │      │
						//   │      │
						//   └──────┘
						if .TopLeft.X+.Width > .TopLeft.X+.Width {
							if .PassesThrough(geo.NewPoint(, .Y), geo.NewPoint(, .Y)) ||
								.PassesThrough(, geo.NewPoint(, .Y)) {
								 = true
							}
						}
						//   ┌──────┐
						//   │      │
						//   │      │
						//   └──────┘
						//
						//
						// ┌────────────────┐
						// │                │
						// │                │
						// │                │
						// └────────────────┘
						//
						if .TopLeft.X+.Width < .TopLeft.X+.Width {
							if .PassesThrough(, geo.NewPoint(, .Y)) ||
								.PassesThrough(geo.NewPoint(, .Y), geo.NewPoint(, .Y)) {
								 = true
							}
						}
					}
				case geo.Bottom:
					,  = , 
					,  = , 

					 := math.Max(.TopLeft.X, .TopLeft.X)
					 := math.Min(.TopLeft.X+.Width, .TopLeft.X+.Width)
					if ! {
						 := math.Min(.TopLeft.X, .TopLeft.X)
						 :=  + math.Abs(-)/2.
						if .TopLeft.X < .TopLeft.X {
							if .PassesThrough(geo.NewPoint(, .Y), geo.NewPoint(, .Y)) ||
								.PassesThrough(geo.NewPoint(, .Y), ) {
								 = true
							}
						}
						if .TopLeft.X > .TopLeft.X {
							if .PassesThrough(, geo.NewPoint(, .Y)) ||
								.PassesThrough(geo.NewPoint(, .Y), geo.NewPoint(, .Y)) {
								 = true
							}
						}
					}
					if ! {
						 := math.Max(.TopLeft.X+.Width, .TopLeft.X+.Width)
						 :=  - math.Abs(-)/2.
						if .TopLeft.X+.Width > .TopLeft.X+.Width {
							if .PassesThrough(geo.NewPoint(, .Y), geo.NewPoint(, .Y)) ||
								.PassesThrough(, geo.NewPoint(, .Y)) {
								 = true
							}
						}
						if .TopLeft.X+.Width < .TopLeft.X+.Width {
							if .PassesThrough(, geo.NewPoint(, .Y)) ||
								.PassesThrough(geo.NewPoint(, .Y), geo.NewPoint(, .Y)) {
								 = true
							}
						}
					}

					,  = , 
					,  = , 
				case geo.Left:
					 := math.Max(.TopLeft.Y, .TopLeft.Y)
					 := math.Min(.TopLeft.Y+.Height, .TopLeft.Y+.Height)
					if ! {
						 := math.Min(.TopLeft.Y, .TopLeft.Y)
						 :=  + math.Abs(-)/2.
						// ┌────────┐
						// │        │
						// │        │
						// │        │           ┌─────┐
						// │        │           │     │
						// │        │           │     │
						// │        │           │     │
						// │        │           └─────┘
						// └────────┘
						if .TopLeft.Y < .TopLeft.Y {
							if .PassesThrough(geo.NewPoint(.X, ), geo.NewPoint(.X, )) ||
								.PassesThrough(, geo.NewPoint(.X, )) {
								 = true
							}
						}
						//              ┌────────┐
						//              │        │
						//              │        │
						// ┌─────┐      │        │
						// │     │      │        │
						// │     │      │        │
						// │     │      │        │
						// └─────┘      │        │
						//              └────────┘
						if .TopLeft.Y > .TopLeft.Y {
							if .PassesThrough(, geo.NewPoint(.X, )) ||
								.PassesThrough(geo.NewPoint(.X, ), geo.NewPoint(.X, )) {
								 = true
							}
						}
					}
					if ! {
						 := math.Max(.TopLeft.Y+.Height, .TopLeft.Y+.Height)
						 :=  - math.Abs(-)/2.
						// ┌────────┐         ┌─────┐
						// │        │         │     │
						// │        │         │     │
						// │        │         │     │
						// │        │         └─────┘
						// │        │
						// │        │
						// │        │
						// └────────┘
						if .TopLeft.Y+.Height > .TopLeft.Y+.Height {
							if .PassesThrough(geo.NewPoint(.X, ), geo.NewPoint(.X, )) ||
								.PassesThrough(, geo.NewPoint(.X, )) {
								 = true
							}
						}
						// ┌─────┐       ┌────────┐
						// │     │       │        │
						// │     │       │        │
						// │     │       │        │
						// └─────┘       │        │
						//               │        │
						//               │        │
						//               │        │
						//               └────────┘
						if .TopLeft.Y+.Height < .TopLeft.Y+.Height {
							if .PassesThrough(, geo.NewPoint(.X, )) ||
								.PassesThrough(geo.NewPoint(.X, ), geo.NewPoint(.X, )) {
								 = true
							}
						}
					}
				case geo.Right:
					// Use same code as geo.Left but swap, then swap back after
					,  = , 
					,  = , 

					 := math.Max(.TopLeft.Y, .TopLeft.Y)
					 := math.Min(.TopLeft.Y+.Height, .TopLeft.Y+.Height)
					if ! {
						 := math.Min(.TopLeft.Y, .TopLeft.Y)
						 :=  + math.Abs(-)/2.
						if .TopLeft.Y < .TopLeft.Y {
							if .PassesThrough(geo.NewPoint(.X, ), geo.NewPoint(.X, )) ||
								.PassesThrough(, geo.NewPoint(.X, )) {
								 = true
							}
						}
						if .TopLeft.Y > .TopLeft.Y {
							if .PassesThrough(, geo.NewPoint(.X, )) ||
								.PassesThrough(geo.NewPoint(.X, ), geo.NewPoint(.X, )) {
								 = true
							}
						}
					}
					if ! {
						 := math.Max(.TopLeft.Y+.Height, .TopLeft.Y+.Height)
						 :=  - math.Abs(-)/2.
						if .TopLeft.Y+.Height > .TopLeft.Y+.Height {
							if .PassesThrough(geo.NewPoint(.X, ), geo.NewPoint(.X, )) ||
								.PassesThrough(, geo.NewPoint(.X, )) {
								 = true
							}
						}
						if .TopLeft.Y+.Height < .TopLeft.Y+.Height {
							if .PassesThrough(, geo.NewPoint(.X, )) ||
								.PassesThrough(geo.NewPoint(.X, ), geo.NewPoint(.X, )) {
								 = true
							}
						}
					}
					,  = , 
					,  = , 
				default:
					panic("did not expect diagonal")
				}

				if  &&  {
					return true
				}

				return false
			}

			 := false
			 := !
			clear(.obstructionSets)
			.obstructionSets = .obstructionSets[:0]
			if .obstructionAdded == nil {
				.obstructionAdded = make(map[*layoutgraph.Node]struct{})
			} else {
				clear(.obstructionAdded)
			}
			 := .NearestSharedAncestor()

			 := .EffectiveContainer()
			for {
				if ,  := .obstructionAdded[];  {
					break
				}
				.obstructionSets = append(.obstructionSets, .Graph.Containers[])
				.obstructionAdded[] = struct{}{}
				if  == nil {
					break
				}
				 = .EffectiveContainer()
				if  ==  {
					break
				}
			}

			 = .EffectiveContainer()
			for {
				if ,  := .obstructionAdded[];  {
					break
				}
				.obstructionSets = append(.obstructionSets, .Graph.Containers[])
				.obstructionAdded[] = struct{}{}
				if  == nil {
					break
				}
				 = .EffectiveContainer()
				if  ==  {
					break
				}
			}

			for ,  := range .obstructionSets {
				if  := scoringCancellationError(, );  != nil {
					return 0, 
				}
				for  := 0;  < len(); ++ {
					if  := scoringCancellationError(, );  != nil {
						return 0, 
					}
					 := []
					if .excludes() {
						continue
					}
					if .IsDescendantOf() || .IsDescendantOf() {
						continue
					}
					// Used during initialization, or sibling coming from subgraph that hasn't gone yet
					if .TopLeft == nil {
						continue
					}
					if .Graph != .Graph {
						// not in the same subgraph, can happen during placement
						continue
					}
					if  ==  ||  ==  {
						continue
					}
					if .IsClusterVessel() {
						if (.Cluster != nil && .Cluster.Vessel == ) || (.Cluster != nil && .Cluster.Vessel == ) {
							// Penalties are calculated elsewhere below for cluster nodes, so don't double count
							continue
						}
					}

					if ! && () {
						 = true
					}
					if  && ! && () {
						 = true
					}
					if  &&  {
						break
					}
				}
				if  &&  {
					break
				}
			}
			if  {
				var  float64
				// Penalize deeply for obstructing cluster
				if .Cluster != nil || .Cluster != nil {
					 = .Graph.TurnCost() * 2
				} else if  {
					// When it's diagonal, the shortest route would've been an L shaped, so to turn it in an S-shape is only one extra turn
					 = .Graph.TurnCost()
				} else if  {
					if  {
						 = .Graph.TurnCost() * 2
					} else {
						// Just an L-shape
						 = .Graph.TurnCost()
					}
				} else {
					// When it's straight, takes 2 extra turns to get around the obstruction
					 = .Graph.TurnCost() * 2
				}
				 += 
			}

			 := .Graph.TurnCost()

			if .Cluster != nil {
				switch .Cluster.Arrangement {
				case layoutgraph.Row:
					 = ()
					switch  {
					case geo.Left, geo.Right:
						 +=  * 2
					}
				case layoutgraph.Column:
					 = ()
					switch  {
					case geo.Top, geo.Bottom:
						 +=  * 2
					}
				}

				// This is a unique penalty because otherwise, two nodes could sit above the cluster (if it's a row)
				// and avoid all other penalties (including symmetry)
				 := .Cluster.Vessel.Orientation()
				, ,  := clusterExactlyTwoExternalConnectedNodes(.Cluster)
				if  {
					var  *layoutgraph.Node
					if  ==  {
						 = 
					} else {
						 = 
					}
					 := .Cluster.Vessel.Orientation()

					// Penalize being on the same side as other external
					switch .Cluster.Arrangement {
					case layoutgraph.Row:
						switch  {
						// The other node is below us
						case geo.TopLeft, geo.Top, geo.TopRight:
							switch  {
							// The other node is below us, so we don't want this node below us
							case geo.TopLeft, geo.Top, geo.TopRight:
								 += 
							}
						case geo.BottomLeft, geo.Bottom, geo.BottomRight:
							switch  {
							case geo.BottomLeft, geo.Bottom, geo.BottomRight:
								 += 
							}
						}
					case layoutgraph.Column:
						switch  {
						case geo.TopLeft, geo.Left, geo.BottomLeft:
							switch  {
							case geo.TopLeft, geo.Left, geo.BottomLeft:
								 += 
							}
						case geo.TopRight, geo.Right, geo.BottomRight:
							switch  {
							case geo.TopRight, geo.Right, geo.BottomRight:
								 += 
							}
						}
					}
				}
				// Reward being aligned with center when on the right side
				 := .Graph.TurnCost()
				switch .Cluster.Arrangement {
				case layoutgraph.Row:
					switch  {
					case geo.Top, geo.Bottom:
						 = math.Abs(.Cluster.Vessel.Center().X - .Center().X)
					}
				case layoutgraph.Column:
					switch  {
					case geo.Right, geo.Left:
						 = math.Abs(.Cluster.Vessel.Center().Y - .Center().Y)
					}
				}
				 += 
			}
			if .Cluster != nil {
				switch .Cluster.Arrangement {
				case layoutgraph.Row:
					 = ()
					switch  {
					case geo.Left, geo.Right:
						 +=  * 2
					}
				case layoutgraph.Column:
					 = ()
					switch  {
					case geo.Top, geo.Bottom:
						 +=  * 2
					}
				}

				 := .Cluster.Vessel.Orientation()
				, ,  := clusterExactlyTwoExternalConnectedNodes(.Cluster)
				if  {
					var  *layoutgraph.Node
					if  ==  {
						 = 
					} else {
						 = 
					}
					 := .Cluster.Vessel.Orientation()

					switch .Cluster.Arrangement {
					case layoutgraph.Row:
						switch  {
						case geo.TopLeft, geo.Top, geo.TopRight:
							switch  {
							case geo.TopLeft, geo.Top, geo.TopRight:
								 += 
							}
						case geo.BottomLeft, geo.Bottom, geo.BottomRight:
							switch  {
							case geo.BottomLeft, geo.Bottom, geo.BottomRight:
								 += 
							}
						}
					case layoutgraph.Column:
						switch  {
						case geo.TopLeft, geo.Left, geo.BottomLeft:
							switch  {
							case geo.TopLeft, geo.Left, geo.BottomLeft:
								 += 
							}
						case geo.TopRight, geo.Right, geo.BottomRight:
							switch  {
							case geo.TopRight, geo.Right, geo.BottomRight:
								 += 
							}
						}
					}
				}

				 := .Graph.TurnCost()
				switch .Cluster.Arrangement {
				case layoutgraph.Row:
					switch  {
					case geo.Top, geo.Bottom:
						 = math.Abs(.Cluster.Vessel.Center().X - .Center().X)
					}
				case layoutgraph.Column:
					switch  {
					case geo.Right, geo.Left:
						 = math.Abs(.Cluster.Vessel.Center().Y - .Center().Y)
					}
				}
				 += 
			}
		}

		if  && .IsBetweenTableColumns() {
			var  int
			if .From ==  {
				 = *.ToTableColumnIndex
			} else {
				 = *.FromTableColumnIndex
			}
			for ,  := range .Edges {
				if  := scoringCancellationError(, );  != nil {
					return 0, 
				}
				if !.IsBetweenTableColumns() {
					continue
				}
				 := .Adjacent()
				var  int
				if .From ==  {
					 = *.ToTableColumnIndex
				} else {
					 = *.FromTableColumnIndex
				}

				 := .Orientation()

				 := false
				// The other table has to be on the same side as us for this crossing calculation to matter
				 := .Orientation()
				 = ()
				switch  {
				case geo.BottomRight, geo.Right, geo.TopRight:
					switch  {
					case geo.BottomRight, geo.Right, geo.TopRight:
						 = true
					}
				case geo.BottomLeft, geo.Left, geo.TopLeft:
					switch  {
					case geo.BottomLeft, geo.Left, geo.TopLeft:
						 = true
					}
				}

				if  {
					switch  {
					// The other table is below us
					case geo.TopRight, geo.Top, geo.TopLeft:
						// The other table connects to column higher than ours
						if  <  {
							 += .Graph.CrossingCost()
						}
						// The other table is above us
					case geo.BottomRight, geo.Bottom, geo.BottomLeft:
						// The other table connects to column lower than ours
						if  >  {
							 += .Graph.CrossingCost()
						}
					}
				}
			}
		}

		// If the distance is less than the minGapSize (and it's enforced), we don't want to reward that
		 = math.Max(, )

		// Add a penalty according to edge direction
		 := 
		if .Container != .Container {
			if depth() > depth() {
				 = 
			}
		}
		if  && (.IsDirected() || .ContainerDirection() != geo.NONE) {
			 = ()
			 := .From
			if , ,  := .DirectedEndpoints();  {
				 = 
			}
			if  ==  {
				 = .GetOpposite()
			}

			 := 

			// For clusters, we care more about alignment in center than compass direction
			// So coalesce diagonal directions to the same direction
			if .Cluster != nil || .Cluster != nil {
				switch  {
				case geo.Left:
					switch  {
					case geo.TopLeft, geo.BottomLeft:
						 = geo.Left
					}
				case geo.Right:
					switch  {
					case geo.TopRight, geo.BottomRight:
						 = geo.Right
					}
				case geo.Top:
					switch  {
					case geo.TopLeft, geo.TopRight:
						 = geo.Top
					}
				case geo.Bottom:
					switch  {
					case geo.BottomLeft, geo.BottomRight:
						 = geo.Bottom
					}
				}
			}

			 := math.Abs(float64(compassDelta(, directionCompass())))
			if !.IsDirected() {
				// if there is a container direction set, also apply to bidirectional edges
				// using compassAxisDelta for most of the weight, and still slightly preferring the edge direction as defined
				// this way `direction:right; a <-> b` will weight layout [a]<->[b] slightly better than [b]<->[a]
				 = 0.1* + 0.9*math.Abs(float64(
					compassAxisDelta(, directionCompass()),
				))
			}
			 := .Graph.CellSize
			if ! {
				 = 1.
			}
			 +=  *  *  * sizelessDirectionDeltaFactor
		}

		if  {
			if .HasLargeArrowheadLabel() {
				 = ()
				if !.IsHorizontal() {
					 += 10 * .Graph.TurnCost()
				}
			} else if .Label != nil {
				if ! || !.IsVertical() {
					 := labeledEdgePair{from: .ID, to: .ID}
					 := .labeledEdgeCount[]
					// with 2 vertical edges a label can go on each side without obstructing each other
					if  > 2 {
						 = ()
						if !.IsHorizontal() {
							// penalize labeled edges with many duplicates if they are not horizontal
							 += float64() * .Graph.TurnCost()
						}
					}
				}
			}
		}

		 += 
	}

	// We don't care about getting as close as possible to all Nears, we just want to get close to one of them
	// Since Nears are symmetrical (if node A has node B and C as Nears, nodes B and C will have A too), they'll end up clustering together
	if len(.Nears) > 0 {
		 := math.Inf(1)
		 := 0
		for  := range .Nears {
			if  := scoringCancellationError(, );  != nil {
				return 0, 
			}
			++
			// Not initialized
			if .TopLeft == nil {
				 = 0
				continue
			}
			 := .DistanceTo(, )
			 = math.Min(, )
		}
		 += 
	}

	if .HerdAssignment != nil &&  {
		 := 0.0
		switch .HerdAssignment.Orientation {
		case geo.Bottom:
			 = .HerdAssignment.Val - (.TopLeft.Y + .Height)
		case geo.Top:
			 = .TopLeft.Y - .HerdAssignment.Val
		case geo.Left:
			 = .TopLeft.X - .HerdAssignment.Val
		case geo.Right:
			 = .HerdAssignment.Val - (.TopLeft.X + .Width)
		}

		if  >= 0 {
			 += 
		} else {
			// extra penalty since it makes all other sheep further from herd fence
			 -= ( - .Graph.CellSize)
		}
	}

	// add a penalty for common uncle siblings not being aligned
	if ,  := .Graph.CommonUncleSiblings[];  {
		 := AxisScore()
		 := .Graph.CellSize
		if ! {
			 = 1.
		}
		 +=  * (1. - ) * float64(len()-1)
	}

	if  := .Err();  != nil {
		return 0, fmt.Errorf("EdgeLength: %w", )
	}
	if  {
		 += flowContinuityCost(, )
	}
	return , nil
}