Merge pull request #1345 from gavin-ts/grid-only-place-label-if-unplaced
grid: don't overwrite nested graph label positions
This commit is contained in:
commit
ba3e43ad5d
16 changed files with 3311 additions and 393 deletions
|
|
@ -12,3 +12,4 @@
|
|||
#### Bugfixes ⛑️
|
||||
|
||||
- Shadow is cut off when `--pad` is 0. Thank you @LeonardsonCC ! [#1326](https://github.com/terrastruct/d2/pull/1326)
|
||||
- Fixes grid layout overwriting label placements for nested objects. [#1345](https://github.com/terrastruct/d2/pull/1345)
|
||||
|
|
|
|||
|
|
@ -1817,90 +1817,6 @@ func (g *Graph) ApplyTheme(themeID int64) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
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.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 == obj || edge.Dst == 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.AbsID() == obj.AbsID() {
|
||||
descendantsObjects = append(descendantsObjects, obj)
|
||||
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
|
||||
for _, child := range obj.ChildrenArray {
|
||||
subObjects, subEdges := pluckObjAndEdges(g, child)
|
||||
descendantsObjects = append(descendantsObjects, subObjects...)
|
||||
edges = append(edges, subEdges...)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return descendantsObjects, edges
|
||||
}
|
||||
|
||||
func (g *Graph) InjectNestedGraph(tempGraph *Graph, parent *Object) {
|
||||
obj := tempGraph.Root.ChildrenArray[0]
|
||||
obj.MoveWithDescendantsTo(0, 0)
|
||||
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...)
|
||||
}
|
||||
|
||||
func (g *Graph) PrintString() string {
|
||||
buf := &bytes.Buffer{}
|
||||
fmt.Fprint(buf, "Objects: [")
|
||||
|
|
@ -1918,54 +1834,6 @@ func (obj *Object) IterDescendants(apply func(parent, child *Object)) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
e.Route[0].X += dx
|
||||
e.Route[0].Y += dy
|
||||
} else if isDst {
|
||||
e.Route[len(e.Route)-1].X += dx
|
||||
e.Route[len(e.Route)-1].Y += dy
|
||||
}
|
||||
|
||||
if isSrc || isDst {
|
||||
movedEdges[e] = struct{}{}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (obj *Object) IsMultiple() bool {
|
||||
return obj.Style.Multiple != nil && obj.Style.Multiple.Value == "true"
|
||||
}
|
||||
|
|
@ -1973,19 +1841,3 @@ func (obj *Object) IsMultiple() bool {
|
|||
func (obj *Object) Is3D() bool {
|
||||
return obj.Style.ThreeDee != nil && obj.Style.ThreeDee.Value == "true"
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
191
d2graph/layout.go
Normal file
191
d2graph/layout.go
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
package d2graph
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/shape"
|
||||
)
|
||||
|
||||
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.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 == obj || edge.Dst == 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.AbsID() == obj.AbsID() {
|
||||
descendantsObjects = append(descendantsObjects, obj)
|
||||
g.Objects = append(g.Objects[:i], g.Objects[i+1:]...)
|
||||
for _, child := range obj.ChildrenArray {
|
||||
subObjects, subEdges := pluckObjAndEdges(g, child)
|
||||
descendantsObjects = append(descendantsObjects, subObjects...)
|
||||
edges = append(edges, subEdges...)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return descendantsObjects, edges
|
||||
}
|
||||
|
||||
func (g *Graph) InjectNestedGraph(tempGraph *Graph, parent *Object) {
|
||||
obj := tempGraph.Root.ChildrenArray[0]
|
||||
obj.MoveWithDescendantsTo(0, 0)
|
||||
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 {
|
||||
e.Route[0].X += dx
|
||||
e.Route[0].Y += dy
|
||||
} else if isDst {
|
||||
e.Route[len(e.Route)-1].X += dx
|
||||
e.Route[len(e.Route)-1].Y += dy
|
||||
}
|
||||
|
||||
if isSrc || isDst {
|
||||
movedEdges[e] = struct{}{}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 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) 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)
|
||||
return shape.NewShape(shapeType, contentBox)
|
||||
}
|
||||
|
||||
func (obj *Object) GetLabelTopLeft() *geo.Point {
|
||||
if obj.LabelPosition == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := obj.ToShape()
|
||||
labelPosition := label.Position(*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
|
||||
}
|
||||
|
|
@ -119,9 +119,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
}
|
||||
|
||||
if obj.Icon != nil && obj.Shape.Value != d2target.ShapeImage {
|
||||
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(obj.Width), float64(obj.Height))
|
||||
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[obj.Shape.Value]
|
||||
s := shape.NewShape(shapeType, contentBox)
|
||||
s := obj.ToShape()
|
||||
iconSize := d2target.GetIconSize(s.GetInnerBox(), string(label.InsideTopLeft))
|
||||
// Since dagre container labels are pushed up, we don't want a child container to collide
|
||||
maxContainerLabelHeight = go2.Max(maxContainerLabelHeight, (iconSize+label.PADDING*2)*2)
|
||||
|
|
@ -240,7 +238,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
obj.Width = math.Ceil(dn.Width)
|
||||
obj.Height = math.Ceil(dn.Height)
|
||||
|
||||
if obj.HasLabel() {
|
||||
if obj.HasLabel() && obj.LabelPosition == nil {
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
obj.LabelPosition = go2.Pointer(string(label.OutsideTopCenter))
|
||||
} else if obj.HasOutsideBottomLabel() {
|
||||
|
|
@ -253,7 +251,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||
}
|
||||
}
|
||||
if obj.Icon != nil {
|
||||
if obj.Icon != nil && obj.IconPosition == nil {
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
obj.IconPosition = go2.Pointer(string(label.OutsideTopLeft))
|
||||
obj.LabelPosition = go2.Pointer(string(label.OutsideTopRight))
|
||||
|
|
@ -491,8 +489,8 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
}
|
||||
}
|
||||
|
||||
srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Shape.Value)], edge.Src.Box)
|
||||
dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Shape.Value)], edge.Dst.Box)
|
||||
srcShape := edge.Src.ToShape()
|
||||
dstShape := edge.Dst.ToShape()
|
||||
|
||||
// trace the edge to the specific shape's border
|
||||
points[startIndex] = shape.TraceToShapeBorder(srcShape, start, points[startIndex+1])
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
obj.Width = math.Ceil(n.Width)
|
||||
obj.Height = math.Ceil(n.Height)
|
||||
|
||||
if obj.HasLabel() {
|
||||
if obj.HasLabel() && obj.LabelPosition == nil {
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
} else if obj.HasOutsideBottomLabel() {
|
||||
|
|
@ -419,7 +419,7 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
obj.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||
}
|
||||
}
|
||||
if obj.Icon != nil {
|
||||
if obj.Icon != nil && obj.IconPosition == nil {
|
||||
if len(obj.ChildrenArray) > 0 {
|
||||
obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopRight))
|
||||
|
|
@ -501,8 +501,8 @@ func Layout(ctx context.Context, g *d2graph.Graph, opts *ConfigurableOpts) (err
|
|||
}
|
||||
}
|
||||
|
||||
srcShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Src.Shape.Value)], edge.Src.Box)
|
||||
dstShape := shape.NewShape(d2target.DSL_SHAPE_TO_SHAPE_TYPE[strings.ToLower(edge.Dst.Shape.Value)], edge.Dst.Box)
|
||||
srcShape := edge.Src.ToShape()
|
||||
dstShape := edge.Dst.ToShape()
|
||||
|
||||
// trace the edge to the specific shape's border
|
||||
points[startIndex] = shape.TraceToShapeBorder(srcShape, points[startIndex], points[startIndex+1])
|
||||
|
|
|
|||
|
|
@ -6,13 +6,11 @@ import (
|
|||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"oss.terrastruct.com/d2/d2graph"
|
||||
"oss.terrastruct.com/d2/d2target"
|
||||
"oss.terrastruct.com/d2/lib/geo"
|
||||
"oss.terrastruct.com/d2/lib/label"
|
||||
"oss.terrastruct.com/d2/lib/shape"
|
||||
"oss.terrastruct.com/util-go/go2"
|
||||
)
|
||||
|
||||
|
|
@ -104,42 +102,99 @@ func withoutGridDiagrams(ctx context.Context, g *d2graph.Graph, layout d2graph.L
|
|||
if obj.GridGap != nil || obj.VerticalGap != nil {
|
||||
verticalPadding = gd.verticalGap
|
||||
}
|
||||
|
||||
// size shape according to grid
|
||||
obj.SizeToContent(float64(gd.width), float64(gd.height), float64(2*horizontalPadding), float64(2*verticalPadding))
|
||||
obj.SizeToContent(gd.width, gd.height, float64(2*horizontalPadding), float64(2*verticalPadding))
|
||||
|
||||
// compute where the grid should be placed inside shape
|
||||
dslShape := strings.ToLower(obj.Shape.Value)
|
||||
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape]
|
||||
s := shape.NewShape(shapeType, geo.NewBox(geo.NewPoint(0, 0), obj.Width, obj.Height))
|
||||
s := obj.ToShape()
|
||||
innerBox := s.GetInnerBox()
|
||||
if innerBox.TopLeft.X != 0 || innerBox.TopLeft.Y != 0 {
|
||||
gd.shift(innerBox.TopLeft.X, innerBox.TopLeft.Y)
|
||||
}
|
||||
|
||||
// compute how much space the label and icon occupy
|
||||
var occupiedWidth, occupiedHeight float64
|
||||
if obj.Icon != nil {
|
||||
iconSpace := float64(d2target.MAX_ICON_SIZE + 2*label.PADDING)
|
||||
occupiedWidth = iconSpace
|
||||
occupiedHeight = iconSpace
|
||||
}
|
||||
|
||||
var dx, dy float64
|
||||
if obj.LabelDimensions.Width != 0 {
|
||||
labelWidth := float64(obj.LabelDimensions.Width) + 2*label.PADDING
|
||||
if labelWidth > obj.Width {
|
||||
dx = (labelWidth - obj.Width) / 2
|
||||
obj.Width = labelWidth
|
||||
}
|
||||
}
|
||||
if obj.LabelDimensions.Height != 0 {
|
||||
labelHeight := float64(obj.LabelDimensions.Height) + 2*label.PADDING
|
||||
if labelHeight > float64(verticalPadding) {
|
||||
// if the label doesn't fit within the padding, we need to add more
|
||||
grow := labelHeight - float64(verticalPadding)
|
||||
dy = grow
|
||||
obj.Height += grow
|
||||
occupiedHeight = math.Max(
|
||||
occupiedHeight,
|
||||
float64(obj.LabelDimensions.Height)+2*label.PADDING,
|
||||
)
|
||||
}
|
||||
if obj.LabelDimensions.Width != 0 {
|
||||
// . ├────┤───────├────┤
|
||||
// . icon label icon
|
||||
// with an icon in top left we need 2x the space to fit the label in the center
|
||||
occupiedWidth *= 2
|
||||
occupiedWidth += float64(obj.LabelDimensions.Width) + 2*label.PADDING
|
||||
if occupiedWidth > obj.Width {
|
||||
dx = (occupiedWidth - obj.Width) / 2
|
||||
obj.Width = occupiedWidth
|
||||
}
|
||||
}
|
||||
|
||||
// also check for grid cells with outside top labels or icons
|
||||
// the first grid object is at the top (and always exists)
|
||||
topY := gd.objects[0].TopLeft.Y
|
||||
highestOutside := topY
|
||||
for _, o := range gd.objects {
|
||||
// we only want to compute label positions for objects at the top of the grid
|
||||
if o.TopLeft.Y > topY {
|
||||
if gd.rowDirected {
|
||||
// if the grid is rowDirected (row1, row2, etc) we can stop after finishing the first row
|
||||
break
|
||||
} else {
|
||||
// otherwise we continue until the next column
|
||||
continue
|
||||
}
|
||||
}
|
||||
if o.LabelPosition != nil {
|
||||
labelPosition := label.Position(*o.LabelPosition)
|
||||
if labelPosition.IsOutside() {
|
||||
labelTL := o.GetLabelTopLeft()
|
||||
if labelTL.Y < highestOutside {
|
||||
highestOutside = labelTL.Y
|
||||
}
|
||||
}
|
||||
}
|
||||
if o.IconPosition != nil {
|
||||
switch label.Position(*o.IconPosition) {
|
||||
case label.OutsideTopLeft, label.OutsideTopCenter, label.OutsideTopRight:
|
||||
iconSpace := float64(d2target.MAX_ICON_SIZE + label.PADDING)
|
||||
if topY-iconSpace < highestOutside {
|
||||
highestOutside = topY - iconSpace
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if highestOutside < topY {
|
||||
occupiedHeight += topY - highestOutside + 2*label.PADDING
|
||||
}
|
||||
if occupiedHeight > float64(verticalPadding) {
|
||||
// if the label doesn't fit within the padding, we need to add more
|
||||
dy = occupiedHeight - float64(verticalPadding)
|
||||
obj.Height += dy
|
||||
}
|
||||
|
||||
// we need to center children if we have to expand to fit the container label
|
||||
if dx != 0 || dy != 0 {
|
||||
gd.shift(dx, dy)
|
||||
}
|
||||
}
|
||||
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
if obj.HasLabel() {
|
||||
obj.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
}
|
||||
if obj.Icon != nil {
|
||||
obj.IconPosition = go2.Pointer(string(label.InsideTopLeft))
|
||||
}
|
||||
gridDiagrams[obj.AbsID()] = gd
|
||||
|
||||
for _, o := range gd.objects {
|
||||
|
|
@ -191,10 +246,17 @@ func layoutGrid(g *d2graph.Graph, obj *d2graph.Object) (*gridDiagram, error) {
|
|||
// position labels and icons
|
||||
for _, o := range gd.objects {
|
||||
if o.Icon != nil {
|
||||
o.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
o.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||
// don't overwrite position if nested graph layout positioned label/icon
|
||||
if o.LabelPosition == nil {
|
||||
o.LabelPosition = go2.Pointer(string(label.InsideTopCenter))
|
||||
}
|
||||
if o.IconPosition == nil {
|
||||
o.IconPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||
}
|
||||
} else {
|
||||
o.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||
if o.LabelPosition == nil {
|
||||
o.LabelPosition = go2.Pointer(string(label.InsideMiddleCenter))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2723,6 +2723,7 @@ scenarios: {
|
|||
loadFromFile(t, "grid_large_checkered"),
|
||||
loadFromFile(t, "grid_nested"),
|
||||
loadFromFile(t, "grid_nested_gap0"),
|
||||
loadFromFile(t, "grid_icon"),
|
||||
loadFromFile(t, "multiple_offset"),
|
||||
loadFromFile(t, "multiple_offset_left"),
|
||||
}
|
||||
|
|
|
|||
67
e2etests/testdata/files/grid_icon.d2
vendored
Normal file
67
e2etests/testdata/files/grid_icon.d2
vendored
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
classes: {
|
||||
2x2: {
|
||||
grid-rows: 2
|
||||
grid-columns: 2
|
||||
}
|
||||
}
|
||||
|
||||
grid w/ container + icon: {
|
||||
class: 2x2
|
||||
|
||||
a
|
||||
b: {
|
||||
b child
|
||||
|
||||
icon: https://icons.terrastruct.com/dev%2Fgithub.svg
|
||||
}
|
||||
c
|
||||
d
|
||||
}
|
||||
|
||||
grid + icon: {
|
||||
class: 2x2
|
||||
|
||||
icon: https://icons.terrastruct.com/dev%2Fgithub.svg
|
||||
|
||||
a
|
||||
b
|
||||
c
|
||||
d
|
||||
}
|
||||
|
||||
grid + icon w/ container: {
|
||||
class: 2x2
|
||||
|
||||
icon: https://icons.terrastruct.com/dev%2Fgithub.svg
|
||||
|
||||
a
|
||||
b: {
|
||||
b child
|
||||
}
|
||||
c
|
||||
d
|
||||
}
|
||||
|
||||
no label grid w/ container + icon: "" {
|
||||
class: 2x2
|
||||
|
||||
a
|
||||
b: {
|
||||
b child
|
||||
|
||||
icon: https://icons.terrastruct.com/dev%2Fgithub.svg
|
||||
}
|
||||
c
|
||||
d
|
||||
}
|
||||
|
||||
no label grid + icon: "" {
|
||||
class: 2x2
|
||||
|
||||
icon: https://icons.terrastruct.com/dev%2Fgithub.svg
|
||||
|
||||
a
|
||||
b
|
||||
c
|
||||
d
|
||||
}
|
||||
1271
e2etests/testdata/stable/grid_icon/dagre/board.exp.json
generated
vendored
Normal file
1271
e2etests/testdata/stable/grid_icon/dagre/board.exp.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
102
e2etests/testdata/stable/grid_icon/dagre/sketch.exp.svg
vendored
Normal file
102
e2etests/testdata/stable/grid_icon/dagre/sketch.exp.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 22 KiB |
1271
e2etests/testdata/stable/grid_icon/elk/board.exp.json
generated
vendored
Normal file
1271
e2etests/testdata/stable/grid_icon/elk/board.exp.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
102
e2etests/testdata/stable/grid_icon/elk/sketch.exp.svg
vendored
Normal file
102
e2etests/testdata/stable/grid_icon/elk/sketch.exp.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 22 KiB |
100
e2etests/testdata/stable/grid_nested/dagre/board.exp.json
generated
vendored
100
e2etests/testdata/stable/grid_nested/dagre/board.exp.json
generated
vendored
|
|
@ -14,7 +14,7 @@
|
|||
"y": 150
|
||||
},
|
||||
"width": 384,
|
||||
"height": 356,
|
||||
"height": 388,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
|
|
@ -52,7 +52,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 60,
|
||||
"y": 210
|
||||
"y": 242
|
||||
},
|
||||
"width": 53,
|
||||
"height": 130,
|
||||
|
|
@ -93,7 +93,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 153,
|
||||
"y": 210
|
||||
"y": 242
|
||||
},
|
||||
"width": 171,
|
||||
"height": 130,
|
||||
|
|
@ -125,7 +125,7 @@
|
|||
"underline": false,
|
||||
"labelWidth": 12,
|
||||
"labelHeight": 31,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPosition": "OUTSIDE_TOP_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
},
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 193,
|
||||
"y": 242
|
||||
"y": 274
|
||||
},
|
||||
"width": 91,
|
||||
"height": 66,
|
||||
|
|
@ -175,7 +175,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 60,
|
||||
"y": 380
|
||||
"y": 412
|
||||
},
|
||||
"width": 53,
|
||||
"height": 66,
|
||||
|
|
@ -216,7 +216,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 153,
|
||||
"y": 380
|
||||
"y": 412
|
||||
},
|
||||
"width": 171,
|
||||
"height": 66,
|
||||
|
|
@ -263,7 +263,7 @@
|
|||
"y": 0
|
||||
},
|
||||
"width": 692,
|
||||
"height": 656,
|
||||
"height": 688,
|
||||
"opacity": 1,
|
||||
"strokeDash": 0,
|
||||
"strokeWidth": 2,
|
||||
|
|
@ -301,7 +301,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 504,
|
||||
"y": 60
|
||||
"y": 92
|
||||
},
|
||||
"width": 53,
|
||||
"height": 430,
|
||||
|
|
@ -342,7 +342,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 597,
|
||||
"y": 60
|
||||
"y": 92
|
||||
},
|
||||
"width": 479,
|
||||
"height": 430,
|
||||
|
|
@ -374,7 +374,7 @@
|
|||
"underline": false,
|
||||
"labelWidth": 12,
|
||||
"labelHeight": 31,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPosition": "OUTSIDE_TOP_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
},
|
||||
|
|
@ -383,7 +383,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 617,
|
||||
"y": 123
|
||||
"y": 155
|
||||
},
|
||||
"width": 439,
|
||||
"height": 335,
|
||||
|
|
@ -424,7 +424,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 637,
|
||||
"y": 183
|
||||
"y": 215
|
||||
},
|
||||
"width": 186,
|
||||
"height": 240,
|
||||
|
|
@ -465,7 +465,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 657,
|
||||
"y": 246
|
||||
"y": 278
|
||||
},
|
||||
"width": 146,
|
||||
"height": 140,
|
||||
|
|
@ -506,7 +506,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 697,
|
||||
"y": 283
|
||||
"y": 315
|
||||
},
|
||||
"width": 66,
|
||||
"height": 66,
|
||||
|
|
@ -547,7 +547,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 843,
|
||||
"y": 183
|
||||
"y": 215
|
||||
},
|
||||
"width": 193,
|
||||
"height": 240,
|
||||
|
|
@ -588,7 +588,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 863,
|
||||
"y": 246
|
||||
"y": 278
|
||||
},
|
||||
"width": 153,
|
||||
"height": 140,
|
||||
|
|
@ -629,7 +629,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 903,
|
||||
"y": 283
|
||||
"y": 315
|
||||
},
|
||||
"width": 73,
|
||||
"height": 66,
|
||||
|
|
@ -670,7 +670,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 504,
|
||||
"y": 530
|
||||
"y": 562
|
||||
},
|
||||
"width": 53,
|
||||
"height": 66,
|
||||
|
|
@ -711,7 +711,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 597,
|
||||
"y": 530
|
||||
"y": 562
|
||||
},
|
||||
"width": 479,
|
||||
"height": 66,
|
||||
|
|
@ -755,7 +755,7 @@
|
|||
],
|
||||
"pos": {
|
||||
"x": 1196,
|
||||
"y": 69
|
||||
"y": 85
|
||||
},
|
||||
"width": 480,
|
||||
"height": 518,
|
||||
|
|
@ -796,7 +796,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1256,
|
||||
"y": 129
|
||||
"y": 145
|
||||
},
|
||||
"width": 53,
|
||||
"height": 292,
|
||||
|
|
@ -840,7 +840,7 @@
|
|||
],
|
||||
"pos": {
|
||||
"x": 1349,
|
||||
"y": 129
|
||||
"y": 145
|
||||
},
|
||||
"width": 267,
|
||||
"height": 292,
|
||||
|
|
@ -881,7 +881,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1409,
|
||||
"y": 189
|
||||
"y": 205
|
||||
},
|
||||
"width": 53,
|
||||
"height": 66,
|
||||
|
|
@ -922,7 +922,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1502,
|
||||
"y": 189
|
||||
"y": 205
|
||||
},
|
||||
"width": 54,
|
||||
"height": 66,
|
||||
|
|
@ -963,7 +963,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1409,
|
||||
"y": 295
|
||||
"y": 311
|
||||
},
|
||||
"width": 53,
|
||||
"height": 66,
|
||||
|
|
@ -1004,7 +1004,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1502,
|
||||
"y": 295
|
||||
"y": 311
|
||||
},
|
||||
"width": 54,
|
||||
"height": 66,
|
||||
|
|
@ -1045,7 +1045,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1256,
|
||||
"y": 461
|
||||
"y": 477
|
||||
},
|
||||
"width": 53,
|
||||
"height": 66,
|
||||
|
|
@ -1086,7 +1086,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1349,
|
||||
"y": 461
|
||||
"y": 477
|
||||
},
|
||||
"width": 267,
|
||||
"height": 66,
|
||||
|
|
@ -1127,7 +1127,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1736,
|
||||
"y": 38
|
||||
"y": 54
|
||||
},
|
||||
"width": 321,
|
||||
"height": 581,
|
||||
|
|
@ -1168,7 +1168,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1736,
|
||||
"y": 84
|
||||
"y": 100
|
||||
},
|
||||
"width": 53,
|
||||
"height": 469,
|
||||
|
|
@ -1209,7 +1209,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1789,
|
||||
"y": 84
|
||||
"y": 100
|
||||
},
|
||||
"width": 268,
|
||||
"height": 469,
|
||||
|
|
@ -1250,7 +1250,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1789,
|
||||
"y": 125
|
||||
"y": 141
|
||||
},
|
||||
"width": 214,
|
||||
"height": 66,
|
||||
|
|
@ -1291,7 +1291,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 2003,
|
||||
"y": 125
|
||||
"y": 141
|
||||
},
|
||||
"width": 54,
|
||||
"height": 66,
|
||||
|
|
@ -1332,7 +1332,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1789,
|
||||
"y": 191
|
||||
"y": 207
|
||||
},
|
||||
"width": 214,
|
||||
"height": 362,
|
||||
|
|
@ -1373,7 +1373,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1789,
|
||||
"y": 227
|
||||
"y": 243
|
||||
},
|
||||
"width": 53,
|
||||
"height": 66,
|
||||
|
|
@ -1414,7 +1414,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1842,
|
||||
"y": 227
|
||||
"y": 243
|
||||
},
|
||||
"width": 161,
|
||||
"height": 66,
|
||||
|
|
@ -1455,7 +1455,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1789,
|
||||
"y": 293
|
||||
"y": 309
|
||||
},
|
||||
"width": 53,
|
||||
"height": 260,
|
||||
|
|
@ -1496,7 +1496,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1842,
|
||||
"y": 293
|
||||
"y": 309
|
||||
},
|
||||
"width": 161,
|
||||
"height": 260,
|
||||
|
|
@ -1537,7 +1537,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1842,
|
||||
"y": 324
|
||||
"y": 340
|
||||
},
|
||||
"width": 107,
|
||||
"height": 163,
|
||||
|
|
@ -1578,7 +1578,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1842,
|
||||
"y": 355
|
||||
"y": 371
|
||||
},
|
||||
"width": 53,
|
||||
"height": 66,
|
||||
|
|
@ -1619,7 +1619,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1895,
|
||||
"y": 355
|
||||
"y": 371
|
||||
},
|
||||
"width": 54,
|
||||
"height": 66,
|
||||
|
|
@ -1660,7 +1660,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1842,
|
||||
"y": 421
|
||||
"y": 437
|
||||
},
|
||||
"width": 53,
|
||||
"height": 66,
|
||||
|
|
@ -1701,7 +1701,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1895,
|
||||
"y": 421
|
||||
"y": 437
|
||||
},
|
||||
"width": 54,
|
||||
"height": 66,
|
||||
|
|
@ -1742,7 +1742,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1949,
|
||||
"y": 324
|
||||
"y": 340
|
||||
},
|
||||
"width": 54,
|
||||
"height": 163,
|
||||
|
|
@ -1783,7 +1783,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1842,
|
||||
"y": 487
|
||||
"y": 503
|
||||
},
|
||||
"width": 107,
|
||||
"height": 66,
|
||||
|
|
@ -1824,7 +1824,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1949,
|
||||
"y": 487
|
||||
"y": 503
|
||||
},
|
||||
"width": 54,
|
||||
"height": 66,
|
||||
|
|
@ -1865,7 +1865,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 2003,
|
||||
"y": 191
|
||||
"y": 207
|
||||
},
|
||||
"width": 54,
|
||||
"height": 362,
|
||||
|
|
@ -1906,7 +1906,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1736,
|
||||
"y": 553
|
||||
"y": 569
|
||||
},
|
||||
"width": 53,
|
||||
"height": 66,
|
||||
|
|
@ -1947,7 +1947,7 @@
|
|||
"type": "rectangle",
|
||||
"pos": {
|
||||
"x": 1789,
|
||||
"y": 553
|
||||
"y": 569
|
||||
},
|
||||
"width": 268,
|
||||
"height": 66,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
4
e2etests/testdata/stable/grid_nested/elk/board.exp.json
generated
vendored
4
e2etests/testdata/stable/grid_nested/elk/board.exp.json
generated
vendored
|
|
@ -125,7 +125,7 @@
|
|||
"underline": false,
|
||||
"labelWidth": 12,
|
||||
"labelHeight": 31,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPosition": "INSIDE_TOP_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
},
|
||||
|
|
@ -374,7 +374,7 @@
|
|||
"underline": false,
|
||||
"labelWidth": 12,
|
||||
"labelHeight": 31,
|
||||
"labelPosition": "INSIDE_MIDDLE_CENTER",
|
||||
"labelPosition": "INSIDE_TOP_CENTER",
|
||||
"zIndex": 0,
|
||||
"level": 2
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Loading…
Reference in a new issue