d2/d2layouts/d2cycle/layout.go
Mayank77maruti 527edda636 iteration 1
2025-02-21 17:05:31 +00:00

405 lines
No EOL
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// package d2cycle
// import (
// "context"
// "math"
// "oss.terrastruct.com/d2/d2graph"
// "oss.terrastruct.com/d2/lib/geo"
// "oss.terrastruct.com/d2/lib/label"
// "oss.terrastruct.com/util-go/go2"
// )
// const (
// MIN_RADIUS = 200
// PADDING = 20
// MIN_SEGMENT_LEN = 10
// ARC_STEPS = 30 // high resolution for smooth arcs
// )
// // Layout arranges nodes in a circle and routes edges with properly clipped arcs
// func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) error {
// objects := g.Root.ChildrenArray
// if len(objects) == 0 {
// return nil
// }
// // Position labels and icons first
// for _, obj := range g.Objects {
// positionLabelsIcons(obj)
// }
// // Calculate layout parameters
// nodeCircleRadius := calculateRadius(objects)
// maxNodeSize := 0.0
// for _, obj := range objects {
// size := math.Max(obj.Width, obj.Height)
// maxNodeSize = math.Max(maxNodeSize, size)
// }
// // Position nodes in circle
// positionObjects(objects, nodeCircleRadius)
// // Create properly clipped edge arcs
// for _, edge := range g.Edges {
// createCircularArc(edge, nodeCircleRadius, maxNodeSize)
// }
// return nil
// }
// func calculateRadius(objects []*d2graph.Object) float64 {
// numObjects := float64(len(objects))
// maxSize := 0.0
// for _, obj := range objects {
// size := math.Max(obj.Width, obj.Height)
// maxSize = math.Max(maxSize, size)
// }
// minRadius := (maxSize/2 + PADDING) / math.Sin(math.Pi/numObjects)
// return math.Max(minRadius, MIN_RADIUS)
// }
// func positionObjects(objects []*d2graph.Object, radius float64) {
// numObjects := float64(len(objects))
// angleOffset := -math.Pi / 2 // Start at top
// for i, obj := range objects {
// angle := angleOffset + (2*math.Pi*float64(i))/numObjects
// x := radius * math.Cos(angle)
// y := radius * math.Sin(angle)
// // Center object at calculated position
// obj.TopLeft = geo.NewPoint(
// x-obj.Width/2,
// y-obj.Height/2,
// )
// }
// }
// func createCircularArc(edge *d2graph.Edge, nodeCircleRadius, maxNodeSize float64) {
// if edge.Src == nil || edge.Dst == nil {
// return
// }
// srcCenter := edge.Src.Center()
// dstCenter := edge.Dst.Center()
// // Calculate arc radius outside node circle
// arcRadius := nodeCircleRadius + maxNodeSize/2 + PADDING
// // Calculate angles for arc endpoints
// srcAngle := math.Atan2(srcCenter.Y, srcCenter.X)
// dstAngle := math.Atan2(dstCenter.Y, dstCenter.X)
// if dstAngle < srcAngle {
// dstAngle += 2 * math.Pi
// }
// // Generate arc path points
// path := make([]*geo.Point, 0, ARC_STEPS+1)
// for i := 0; i <= ARC_STEPS; i++ {
// t := float64(i) / ARC_STEPS
// angle := srcAngle + t*(dstAngle-srcAngle)
// x := arcRadius * math.Cos(angle)
// y := arcRadius * math.Sin(angle)
// path = append(path, geo.NewPoint(x, y))
// }
// // Set exact endpoints (will be clipped later)
// path[0] = srcCenter
// path[len(path)-1] = dstCenter
// // Clip path to node borders
// edge.Route = path
// startIndex, endIndex := edge.TraceToShape(edge.Route, 0, len(edge.Route)-1)
// if startIndex < endIndex {
// edge.Route = edge.Route[startIndex : endIndex+1]
// }
// edge.IsCurve = true
// }
// // clampPointOutsideBox walks forward from 'startIdx' until the path segment
// // leaves the bounding box. Then it sets path[startIdx] to the intersection.
// // If we never find it, we return (startIdx, path[startIdx]) meaning we can't clamp.
// func clampPointOutsideBox(box *geo.Box, path []*geo.Point, startIdx int) (int, *geo.Point) {
// if startIdx >= len(path)-1 {
// return startIdx, path[startIdx]
// }
// // If path[startIdx] is outside, no clamp needed
// if !boxContains(box, path[startIdx]) {
// return startIdx, path[startIdx]
// }
// // Walk forward looking for outside
// for i := startIdx + 1; i < len(path); i++ {
// insideNext := boxContains(box, path[i])
// if insideNext {
// // still inside -> keep going
// continue
// }
// // crossing from inside to outside between path[i-1], path[i]
// seg := geo.NewSegment(path[i-1], path[i])
// inters := boxIntersections(box, *seg)
// if len(inters) > 0 {
// // use first intersection
// return i, inters[0]
// }
// // fallback => no intersection found
// return i, path[i]
// }
// // entire remainder is inside, so we can't clamp
// // Just return the end
// last := len(path) - 1
// return last, path[last]
// }
// // clampPointOutsideBoxReverse scans backward from endIdx while path[j] is in the box.
// // Once we find crossing (outside→inside), we return (j, intersection).
// func clampPointOutsideBoxReverse(box *geo.Box, path []*geo.Point, endIdx int) (int, *geo.Point) {
// if endIdx <= 0 {
// return endIdx, path[endIdx]
// }
// if !boxContains(box, path[endIdx]) {
// // already outside
// return endIdx, path[endIdx]
// }
// for j := endIdx - 1; j >= 0; j-- {
// if boxContains(box, path[j]) {
// continue
// }
// // crossing from outside -> inside between path[j], path[j+1]
// seg := geo.NewSegment(path[j], path[j+1])
// inters := boxIntersections(box, *seg)
// if len(inters) > 0 {
// return j, inters[0]
// }
// return j, path[j]
// }
// // entire path inside
// return 0, path[0]
// }
// // Helper if your geo.Box doesnt implement Contains()
// func boxContains(b *geo.Box, p *geo.Point) bool {
// // typical bounding-box check
// return p.X >= b.TopLeft.X &&
// p.X <= b.TopLeft.X+b.Width &&
// p.Y >= b.TopLeft.Y &&
// p.Y <= b.TopLeft.Y+b.Height
// }
// // Helper if your geo.Box doesnt implement Intersections(geo.Segment) yet
// func boxIntersections(b *geo.Box, seg geo.Segment) []*geo.Point {
// // We'll assume d2's standard geo.Box has a built-in Intersections(*Segment) method.
// // If not, implement manually. For example, checking each of the 4 edges:
// // left, right, top, bottom
// // For simplicity, if you do have b.Intersections(...) you can just do:
// // return b.Intersections(seg)
// return b.Intersections(seg)
// // If you don't have that, you'd code the line-rect intersection yourself.
// }
// // positionLabelsIcons is basically your logic that sets default label/icon positions if needed
// func positionLabelsIcons(obj *d2graph.Object) {
// // If there's an icon but no icon position, give it a default
// if obj.Icon != nil && obj.IconPosition == nil {
// if len(obj.ChildrenArray) > 0 {
// obj.IconPosition = go2.Pointer(label.OutsideTopLeft.String())
// if obj.LabelPosition == nil {
// obj.LabelPosition = go2.Pointer(label.OutsideTopRight.String())
// return
// }
// } else if obj.SQLTable != nil || obj.Class != nil || obj.Language != "" {
// obj.IconPosition = go2.Pointer(label.OutsideTopLeft.String())
// } else {
// obj.IconPosition = go2.Pointer(label.InsideMiddleCenter.String())
// }
// }
// // If there's a label but no label position, give it a default
// if obj.HasLabel() && obj.LabelPosition == nil {
// if len(obj.ChildrenArray) > 0 {
// obj.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
// } else if obj.HasOutsideBottomLabel() {
// obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
// } else if obj.Icon != nil {
// obj.LabelPosition = go2.Pointer(label.InsideTopCenter.String())
// } else {
// obj.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
// }
// // If the label is bigger than the shape, fallback to outside positions
// if float64(obj.LabelDimensions.Width) > obj.Width ||
// float64(obj.LabelDimensions.Height) > obj.Height {
// if len(obj.ChildrenArray) > 0 {
// obj.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
// } else {
// obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
// }
// }
// }
// }
package d2cycle
import (
"context"
"math"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/lib/geo"
"oss.terrastruct.com/d2/lib/label"
"oss.terrastruct.com/util-go/go2"
)
const (
MIN_RADIUS = 200
PADDING = 20
ARC_STEPS = 60 // High resolution for perfect circles
)
func Layout(ctx context.Context, g *d2graph.Graph, layout d2graph.LayoutGraph) error {
objects := g.Root.ChildrenArray
if len(objects) == 0 {
return nil
}
for _, obj := range g.Objects {
positionLabelsIcons(obj)
}
baseRadius := calculateBaseRadius(objects)
positionObjects(objects, baseRadius)
for _, edge := range g.Edges {
createPerfectArc(edge, baseRadius)
}
return nil
}
func calculateBaseRadius(objects []*d2graph.Object) float64 {
numNodes := float64(len(objects))
maxSize := 0.0
for _, obj := range objects {
size := math.Max(obj.Width, obj.Height)
maxSize = math.Max(maxSize, size)
}
radius := (maxSize + 2*PADDING) / (2 * math.Sin(math.Pi/numNodes))
return math.Max(radius, MIN_RADIUS)
}
func positionObjects(objects []*d2graph.Object, radius float64) {
numObjects := float64(len(objects))
angleOffset := -math.Pi / 2
for i, obj := range objects {
angle := angleOffset + (2*math.Pi*float64(i))/numObjects
x := radius * math.Cos(angle)
y := radius * math.Sin(angle)
obj.TopLeft = geo.NewPoint(
x-obj.Width/2,
y-obj.Height/2,
)
}
}
func createPerfectArc(edge *d2graph.Edge, baseRadius float64) {
if edge.Src == nil || edge.Dst == nil || edge.Src == edge.Dst {
return
}
srcCenter := edge.Src.Center()
dstCenter := edge.Dst.Center()
center := geo.NewPoint(0, 0) // Layout center
// Calculate angles with proper wrapping
startAngle := math.Atan2(srcCenter.Y-center.Y, srcCenter.X-center.X)
endAngle := math.Atan2(dstCenter.Y-center.Y, dstCenter.X-center.X)
// Handle angle wrapping for shortest path
angleDiff := endAngle - startAngle
if angleDiff < 0 {
angleDiff += 2 * math.Pi
}
if angleDiff > math.Pi {
angleDiff -= 2 * math.Pi
}
// Generate perfect circular arc
path := make([]*geo.Point, 0, ARC_STEPS+1)
for i := 0; i <= ARC_STEPS; i++ {
t := float64(i) / ARC_STEPS
currentAngle := startAngle + t*angleDiff
x := center.X + baseRadius*math.Cos(currentAngle)
y := center.Y + baseRadius*math.Sin(currentAngle)
path = append(path, geo.NewPoint(x, y))
}
// Clip to shape boundaries while preserving arc
edge.Route = path
startIdx, endIdx := edge.TraceToShape(edge.Route, 0, len(edge.Route)-1)
// Maintain smooth arc after clipping
if startIdx < endIdx {
edge.Route = edge.Route[startIdx : endIdx+1]
// Ensure minimum points for smooth rendering
if len(edge.Route) < 3 {
edge.Route = []*geo.Point{path[0], path[len(path)-1]}
}
}
edge.IsCurve = true
}
func positionLabelsIcons(obj *d2graph.Object) {
// If there's an icon but no icon position, give it a default
if obj.Icon != nil && obj.IconPosition == nil {
if len(obj.ChildrenArray) > 0 {
obj.IconPosition = go2.Pointer(label.OutsideTopLeft.String())
if obj.LabelPosition == nil {
obj.LabelPosition = go2.Pointer(label.OutsideTopRight.String())
return
}
} else if obj.SQLTable != nil || obj.Class != nil || obj.Language != "" {
obj.IconPosition = go2.Pointer(label.OutsideTopLeft.String())
} else {
obj.IconPosition = go2.Pointer(label.InsideMiddleCenter.String())
}
}
if obj.HasLabel() && obj.LabelPosition == nil {
if len(obj.ChildrenArray) > 0 {
obj.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
} else if obj.HasOutsideBottomLabel() {
obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
} else if obj.Icon != nil {
obj.LabelPosition = go2.Pointer(label.InsideTopCenter.String())
} else {
obj.LabelPosition = go2.Pointer(label.InsideMiddleCenter.String())
}
if float64(obj.LabelDimensions.Width) > obj.Width ||
float64(obj.LabelDimensions.Height) > obj.Height {
if len(obj.ChildrenArray) > 0 {
obj.LabelPosition = go2.Pointer(label.OutsideTopCenter.String())
} else {
obj.LabelPosition = go2.Pointer(label.OutsideBottomCenter.String())
}
}
}
}
func boxContains(b *geo.Box, p *geo.Point) bool {
return p.X >= b.TopLeft.X &&
p.X <= b.TopLeft.X+b.Width &&
p.Y >= b.TopLeft.Y &&
p.Y <= b.TopLeft.Y+b.Height
}
func boxIntersections(b *geo.Box, seg geo.Segment) []*geo.Point {
return b.Intersections(seg)
}