package d2graph import ( "math" "sort" "strings" "oss.terrastruct.com/d2/d2target" "oss.terrastruct.com/d2/lib/geo" "oss.terrastruct.com/d2/lib/label" "oss.terrastruct.com/d2/lib/shape" ) const MIN_SEGMENT_LEN = 10 func (obj *Object) MoveWithDescendants(dx, dy float64) { obj.TopLeft.X += dx obj.TopLeft.Y += dy for _, child := range obj.ChildrenArray { child.MoveWithDescendants(dx, dy) } } func (obj *Object) MoveWithDescendantsTo(x, y float64) { dx := x - obj.TopLeft.X dy := y - obj.TopLeft.Y obj.MoveWithDescendants(dx, dy) } func (parent *Object) RemoveChild(child *Object) { delete(parent.Children, strings.ToLower(child.ID)) for i := 0; i < len(parent.ChildrenArray); i++ { if parent.ChildrenArray[i] == child { parent.ChildrenArray = append(parent.ChildrenArray[:i], parent.ChildrenArray[i+1:]...) break } } } // remove obj and all descendants from graph, as a new Graph func (g *Graph) ExtractAsNestedGraph(obj *Object) *Graph { descendantObjects, edges := pluckObjAndEdges(g, obj) tempGraph := NewGraph() tempGraph.RootLevel = int(obj.Level()) - 1 tempGraph.Root.ChildrenArray = []*Object{obj} tempGraph.Root.Children[strings.ToLower(obj.ID)] = obj for _, descendantObj := range descendantObjects { descendantObj.Graph = tempGraph } tempGraph.Objects = descendantObjects tempGraph.Edges = edges obj.Parent.RemoveChild(obj) obj.Parent = tempGraph.Root return tempGraph } func pluckObjAndEdges(g *Graph, obj *Object) (descendantsObjects []*Object, edges []*Edge) { for i := 0; i < len(g.Edges); i++ { edge := g.Edges[i] if edge.Src.IsDescendantOf(obj) && edge.Dst.IsDescendantOf(obj) { edges = append(edges, edge) g.Edges = append(g.Edges[:i], g.Edges[i+1:]...) i-- } } for i := 0; i < len(g.Objects); i++ { temp := g.Objects[i] if temp.IsDescendantOf(obj) { descendantsObjects = append(descendantsObjects, temp) g.Objects = append(g.Objects[:i], g.Objects[i+1:]...) i-- } } return descendantsObjects, edges } func (g *Graph) InjectNestedGraph(tempGraph *Graph, parent *Object) { obj := tempGraph.Root.ChildrenArray[0] dx := 0 - obj.TopLeft.X dy := 0 - obj.TopLeft.Y obj.MoveWithDescendants(dx, dy) for _, e := range tempGraph.Edges { e.Move(dx, dy) } obj.Parent = parent for _, obj := range tempGraph.Objects { obj.Graph = g } g.Objects = append(g.Objects, tempGraph.Objects...) parent.Children[strings.ToLower(obj.ID)] = obj parent.ChildrenArray = append(parent.ChildrenArray, obj) g.Edges = append(g.Edges, tempGraph.Edges...) } // ShiftDescendants moves Object's descendants (not including itself) // descendants' edges are also moved by the same dx and dy (the whole route is moved if both ends are a descendant) func (obj *Object) ShiftDescendants(dx, dy float64) { // also need to shift edges of descendants that are shifted movedEdges := make(map[*Edge]struct{}) for _, e := range obj.Graph.Edges { isSrcDesc := e.Src.IsDescendantOf(obj) isDstDesc := e.Dst.IsDescendantOf(obj) if isSrcDesc && isDstDesc { movedEdges[e] = struct{}{} for _, p := range e.Route { p.X += dx p.Y += dy } } } obj.IterDescendants(func(_, curr *Object) { curr.TopLeft.X += dx curr.TopLeft.Y += dy for _, e := range obj.Graph.Edges { if _, ok := movedEdges[e]; ok { continue } isSrc := e.Src == curr isDst := e.Dst == curr if isSrc && isDst { for _, p := range e.Route { p.X += dx p.Y += dy } } else if isSrc { if dx == 0 { e.ShiftStart(dy, false) } else if dy == 0 { e.ShiftStart(dx, true) } else { e.Route[0].X += dx e.Route[0].Y += dy } } else if isDst { if dx == 0 { e.ShiftEnd(dy, false) } else if dy == 0 { e.ShiftEnd(dx, true) } else { e.Route[len(e.Route)-1].X += dx e.Route[len(e.Route)-1].Y += dy } } if isSrc || isDst { movedEdges[e] = struct{}{} } } }) } // 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() { if obj.Shape.Value == d2target.ShapeHexagon { dy = d2target.THREE_DEE_OFFSET / 2 } else { dy = d2target.THREE_DEE_OFFSET } dx = d2target.THREE_DEE_OFFSET } else if obj.IsMultiple() { dy = d2target.MULTIPLE_OFFSET dx = d2target.MULTIPLE_OFFSET } return dx, dy } func (obj *Object) GetMargin() geo.Spacing { margin := geo.Spacing{} if obj.HasLabel() && obj.LabelPosition != nil { position := label.FromString(*obj.LabelPosition) labelWidth := float64(obj.LabelDimensions.Width + label.PADDING) labelHeight := float64(obj.LabelDimensions.Height + label.PADDING) switch position { case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight: margin.Top = labelHeight case label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight: margin.Bottom = labelHeight case label.OutsideLeftTop, label.OutsideLeftMiddle, label.OutsideLeftBottom: margin.Left = labelWidth case label.OutsideRightTop, label.OutsideRightMiddle, label.OutsideRightBottom: margin.Right = labelWidth } // if an outside label is larger than the object add margin accordingly if labelWidth > obj.Width { dx := labelWidth - obj.Width switch position { case label.OutsideTopLeft, label.OutsideBottomLeft: // label fixed at left will overflow on right margin.Right = dx case label.OutsideTopCenter, label.OutsideBottomCenter: margin.Left = math.Ceil(dx / 2) margin.Right = math.Ceil(dx / 2) case label.OutsideTopRight, label.OutsideBottomRight: margin.Left = dx } } if labelHeight > obj.Height { dy := labelHeight - obj.Height switch position { case label.OutsideLeftTop, label.OutsideRightTop: margin.Bottom = dy case label.OutsideLeftMiddle, label.OutsideRightMiddle: margin.Top = math.Ceil(dy / 2) margin.Bottom = math.Ceil(dy / 2) case label.OutsideLeftBottom, label.OutsideRightBottom: margin.Top = dy } } } if obj.HasIcon() && obj.IconPosition != nil { position := label.FromString(*obj.IconPosition) iconSize := float64(d2target.MAX_ICON_SIZE + label.PADDING) switch position { case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight: margin.Top = math.Max(margin.Top, iconSize) case label.OutsideBottomLeft, label.OutsideBottomCenter, label.OutsideBottomRight: margin.Bottom = math.Max(margin.Bottom, iconSize) case label.OutsideLeftTop, label.OutsideLeftMiddle, label.OutsideLeftBottom: margin.Left = math.Max(margin.Left, iconSize) case label.OutsideRightTop, label.OutsideRightMiddle, label.OutsideRightBottom: margin.Right = math.Max(margin.Right, iconSize) } } dx, dy := obj.GetModifierElementAdjustments() margin.Right += dx margin.Top += dy return margin } func (obj *Object) ToShape() shape.Shape { tl := obj.TopLeft if tl == nil { tl = geo.NewPoint(0, 0) } dslShape := strings.ToLower(obj.Shape.Value) shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape] contentBox := geo.NewBox(tl, obj.Width, obj.Height) s := shape.NewShape(shapeType, contentBox) if shapeType == shape.CLOUD_TYPE && obj.ContentAspectRatio != nil { s.SetInnerBoxAspectRatio(*obj.ContentAspectRatio) } return s } func (obj *Object) GetLabelTopLeft() *geo.Point { if obj.LabelPosition == nil { return nil } s := obj.ToShape() labelPosition := label.FromString(*obj.LabelPosition) var box *geo.Box if labelPosition.IsOutside() { box = s.GetBox() } else { box = s.GetInnerBox() } labelTL := labelPosition.GetPointOnBox(box, label.PADDING, float64(obj.LabelDimensions.Width), float64(obj.LabelDimensions.Height), ) return labelTL } func (obj *Object) GetIconTopLeft() *geo.Point { if obj.IconPosition == nil { return nil } s := obj.ToShape() iconPosition := label.FromString(*obj.IconPosition) var box *geo.Box if iconPosition.IsOutside() { box = s.GetBox() } else { box = s.GetInnerBox() } return iconPosition.GetPointOnBox(box, label.PADDING, d2target.MAX_ICON_SIZE, d2target.MAX_ICON_SIZE) } func (edge *Edge) TraceToShape(points []*geo.Point, startIndex, endIndex int) (newStart, newEnd int) { srcShape := edge.Src.ToShape() dstShape := edge.Dst.ToShape() startingSegment := geo.Segment{Start: points[startIndex+1], End: points[startIndex]} // if an edge runs into an outside label, stop the edge at the label instead var overlapsOutsideLabel, overlapsOutsideIcon bool if edge.Src.HasLabel() { // assumes LabelPosition, LabelWidth, LabelHeight are all set if there is a label labelPosition := label.FromString(*edge.Src.LabelPosition) if labelPosition.IsOutside() { labelWidth := float64(edge.Src.LabelDimensions.Width) labelHeight := float64(edge.Src.LabelDimensions.Height) labelTL := labelPosition.GetPointOnBox(edge.Src.Box, label.PADDING, labelWidth, labelHeight) labelBox := geo.NewBox(labelTL, labelWidth, labelHeight) // add left/right padding to box labelBox.TopLeft.X -= label.PADDING labelBox.Width += 2 * label.PADDING for labelBox.Contains(startingSegment.End) && startIndex+1 > endIndex { startingSegment.Start = startingSegment.End startingSegment.End = points[startIndex+2] startIndex++ } if intersections := labelBox.Intersections(startingSegment); len(intersections) > 0 { overlapsOutsideLabel = true p := intersections[0] if len(intersections) > 1 { p = findOuterIntersection(labelPosition, intersections) } // move starting segment to label intersection point points[startIndex] = p startingSegment.End = p // if the segment becomes too short, just merge it with the next segment if startIndex+1 < endIndex && startingSegment.Length() < MIN_SEGMENT_LEN { points[startIndex+1] = points[startIndex] startIndex++ } } } } if !overlapsOutsideLabel && edge.Src.HasIcon() { // assumes IconPosition is set if there is an Icon iconPosition := label.FromString(*edge.Src.IconPosition) if iconPosition.IsOutside() { iconWidth := float64(d2target.MAX_ICON_SIZE) iconHeight := float64(d2target.MAX_ICON_SIZE) iconTL := iconPosition.GetPointOnBox(edge.Src.Box, label.PADDING, iconWidth, iconHeight) iconBox := geo.NewBox(iconTL, iconWidth, iconHeight) for iconBox.Contains(startingSegment.End) && startIndex+1 > endIndex { startingSegment.Start = startingSegment.End startingSegment.End = points[startIndex+2] startIndex++ } if intersections := iconBox.Intersections(startingSegment); len(intersections) > 0 { overlapsOutsideIcon = true p := intersections[0] if len(intersections) > 1 { p = findOuterIntersection(iconPosition, intersections) } // move starting segment to icon intersection point points[startIndex] = p startingSegment.End = p // if the segment becomes too short, just merge it with the next segment if startIndex+1 < endIndex && startingSegment.Length() < MIN_SEGMENT_LEN { points[startIndex+1] = points[startIndex] startIndex++ } } } } if !overlapsOutsideLabel && !overlapsOutsideIcon { if intersections := edge.Src.Intersections(startingSegment); len(intersections) > 0 { // move starting segment to intersection point points[startIndex] = intersections[0] startingSegment.End = intersections[0] // if the segment becomes too short, just merge it with the next segment if startIndex+1 < endIndex && startingSegment.Length() < MIN_SEGMENT_LEN { points[startIndex+1] = points[startIndex] startIndex++ } } // trace the edge to the specific shape's border points[startIndex] = shape.TraceToShapeBorder(srcShape, points[startIndex], points[startIndex+1]) } endingSegment := geo.Segment{Start: points[endIndex-1], End: points[endIndex]} overlapsOutsideLabel = false overlapsOutsideIcon = false if edge.Dst.HasLabel() { // assumes LabelPosition, LabelWidth, LabelHeight are all set if there is a label labelPosition := label.FromString(*edge.Dst.LabelPosition) if labelPosition.IsOutside() { labelWidth := float64(edge.Dst.LabelDimensions.Width) labelHeight := float64(edge.Dst.LabelDimensions.Height) labelTL := labelPosition.GetPointOnBox(edge.Dst.Box, label.PADDING, labelWidth, labelHeight) labelBox := geo.NewBox(labelTL, labelWidth, labelHeight) // add left/right padding to box labelBox.TopLeft.X -= label.PADDING labelBox.Width += 2 * label.PADDING for labelBox.Contains(endingSegment.Start) && endIndex-1 > startIndex { endingSegment.End = endingSegment.Start endingSegment.Start = points[endIndex-2] endIndex-- } if intersections := labelBox.Intersections(endingSegment); len(intersections) > 0 { overlapsOutsideLabel = true p := intersections[0] if len(intersections) > 1 { p = findOuterIntersection(labelPosition, intersections) } // move ending segment to label intersection point points[endIndex] = p endingSegment.End = p // if the segment becomes too short, just merge it with the previous segment if endIndex-1 > startIndex && endingSegment.Length() < MIN_SEGMENT_LEN { points[endIndex-1] = points[endIndex] endIndex-- } } } } if !overlapsOutsideLabel && edge.Dst.HasIcon() { // assumes IconPosition is set if there is an Icon iconPosition := label.FromString(*edge.Dst.IconPosition) if iconPosition.IsOutside() { iconSize := d2target.GetIconSize(edge.Dst.Box, iconPosition.String()) iconWidth := float64(iconSize) iconHeight := float64(iconSize) labelTL := iconPosition.GetPointOnBox(edge.Dst.Box, label.PADDING, iconWidth, iconHeight) iconBox := geo.NewBox(labelTL, iconWidth, iconHeight) for iconBox.Contains(endingSegment.Start) && endIndex-1 > startIndex { endingSegment.End = endingSegment.Start endingSegment.Start = points[endIndex-2] endIndex-- } if intersections := iconBox.Intersections(endingSegment); len(intersections) > 0 { overlapsOutsideIcon = true p := intersections[0] if len(intersections) > 1 { p = findOuterIntersection(iconPosition, intersections) } // move ending segment to icon intersection point points[endIndex] = p endingSegment.End = p // if the segment becomes too short, just merge it with the previous segment if endIndex-1 > startIndex && endingSegment.Length() < MIN_SEGMENT_LEN { points[endIndex-1] = points[endIndex] endIndex-- } } } } if !overlapsOutsideLabel && !overlapsOutsideIcon { if intersections := edge.Dst.Intersections(endingSegment); len(intersections) > 0 { // move ending segment to intersection point points[endIndex] = intersections[0] endingSegment.End = intersections[0] // if the segment becomes too short, just merge it with the previous segment if endIndex-1 > startIndex && endingSegment.Length() < MIN_SEGMENT_LEN { points[endIndex-1] = points[endIndex] endIndex-- } } points[endIndex] = shape.TraceToShapeBorder(dstShape, points[endIndex], points[endIndex-1]) } return startIndex, endIndex } func findOuterIntersection(labelPosition label.Position, intersections []*geo.Point) *geo.Point { switch labelPosition { case label.OutsideTopLeft, label.OutsideTopRight, label.OutsideTopCenter: sort.Slice(intersections, func(i, j int) bool { return intersections[i].Y < intersections[j].Y }) case label.OutsideBottomLeft, label.OutsideBottomRight, label.OutsideBottomCenter: sort.Slice(intersections, func(i, j int) bool { return intersections[i].Y > intersections[j].Y }) case label.OutsideLeftTop, label.OutsideLeftMiddle, label.OutsideLeftBottom: sort.Slice(intersections, func(i, j int) bool { return intersections[i].X < intersections[j].X }) case label.OutsideRightTop, label.OutsideRightMiddle, label.OutsideRightBottom: sort.Slice(intersections, func(i, j int) bool { return intersections[i].X > intersections[j].X }) } return intersections[0] }