...

Source file src/oss.terrastruct.com/d2/lib/shape/shape.go

Documentation: oss.terrastruct.com/d2/lib/shape

     1  package shape
     2  
     3  import (
     4  	"math"
     5  
     6  	"oss.terrastruct.com/d2/lib/geo"
     7  	"oss.terrastruct.com/d2/lib/svg"
     8  	"oss.terrastruct.com/util-go/go2"
     9  )
    10  
    11  const (
    12  	SQUARE_TYPE        = "Square"
    13  	REAL_SQUARE_TYPE   = "RealSquare"
    14  	PARALLELOGRAM_TYPE = "Parallelogram"
    15  	DOCUMENT_TYPE      = "Document"
    16  	CYLINDER_TYPE      = "Cylinder"
    17  	QUEUE_TYPE         = "Queue"
    18  	PAGE_TYPE          = "Page"
    19  	PACKAGE_TYPE       = "Package"
    20  	STEP_TYPE          = "Step"
    21  	CALLOUT_TYPE       = "Callout"
    22  	STORED_DATA_TYPE   = "StoredData"
    23  	PERSON_TYPE        = "Person"
    24  	DIAMOND_TYPE       = "Diamond"
    25  	OVAL_TYPE          = "Oval"
    26  	CIRCLE_TYPE        = "Circle"
    27  	HEXAGON_TYPE       = "Hexagon"
    28  	CLOUD_TYPE         = "Cloud"
    29  
    30  	TABLE_TYPE = "Table"
    31  	CLASS_TYPE = "Class"
    32  	TEXT_TYPE  = "Text"
    33  	CODE_TYPE  = "Code"
    34  	IMAGE_TYPE = "Image"
    35  
    36  	defaultPadding = 40.
    37  )
    38  
    39  type Shape interface {
    40  	Is(shape string) bool
    41  	GetType() string
    42  
    43  	AspectRatio1() bool
    44  	IsRectangular() bool
    45  
    46  	GetBox() *geo.Box
    47  	GetInnerBox() *geo.Box
    48  	// cloud shape has different innerBoxes depending on content's aspect ratio
    49  	GetInnerBoxForContent(width, height float64) *geo.Box
    50  	SetInnerBoxAspectRatio(aspectRatio float64)
    51  
    52  	// placing a rectangle of the given size and padding inside the shape, return the position relative to the shape's TopLeft
    53  	GetInsidePlacement(width, height, paddingX, paddingY float64) geo.Point
    54  
    55  	GetDimensionsToFit(width, height, paddingX, paddingY float64) (float64, float64)
    56  	GetDefaultPadding() (paddingX, paddingY float64)
    57  
    58  	// Perimeter returns a slice of geo.Intersectables that together constitute the shape border
    59  	Perimeter() []geo.Intersectable
    60  
    61  	GetSVGPathData() []string
    62  }
    63  
    64  type baseShape struct {
    65  	Type      string
    66  	Box       *geo.Box
    67  	FullShape *Shape
    68  }
    69  
    70  func (s baseShape) Is(shapeType string) bool {
    71  	return s.Type == shapeType
    72  }
    73  
    74  func (s baseShape) GetType() string {
    75  	return s.Type
    76  }
    77  
    78  func (s baseShape) AspectRatio1() bool {
    79  	return false
    80  }
    81  
    82  func (s baseShape) IsRectangular() bool {
    83  	return false
    84  }
    85  
    86  func (s baseShape) GetBox() *geo.Box {
    87  	return s.Box
    88  }
    89  
    90  func (s baseShape) GetInnerBox() *geo.Box {
    91  	return s.Box
    92  }
    93  
    94  // only cloud shape needs this right now
    95  func (s baseShape) GetInnerBoxForContent(width, height float64) *geo.Box {
    96  	return nil
    97  }
    98  
    99  func (s baseShape) SetInnerBoxAspectRatio(aspectRatio float64) {
   100  	// only used for cloud
   101  }
   102  
   103  func (s baseShape) GetInsidePlacement(_, _, paddingX, paddingY float64) geo.Point {
   104  	innerTL := (*s.FullShape).GetInnerBox().TopLeft
   105  	return *geo.NewPoint(innerTL.X+paddingX/2, innerTL.Y+paddingY/2)
   106  }
   107  
   108  // return the minimum shape dimensions needed to fit content (width x height)
   109  // in the shape's innerBox with padding
   110  func (s baseShape) GetDimensionsToFit(width, height, paddingX, paddingY float64) (float64, float64) {
   111  	return math.Ceil(width + paddingX), math.Ceil(height + paddingY)
   112  }
   113  
   114  func (s baseShape) GetDefaultPadding() (paddingX, paddingY float64) {
   115  	return defaultPadding, defaultPadding
   116  }
   117  
   118  func (s baseShape) Perimeter() []geo.Intersectable {
   119  	return nil
   120  }
   121  
   122  func (s baseShape) GetSVGPathData() []string {
   123  	return nil
   124  }
   125  
   126  func NewShape(shapeType string, box *geo.Box) Shape {
   127  	switch shapeType {
   128  	case CALLOUT_TYPE:
   129  		return NewCallout(box)
   130  	case CIRCLE_TYPE:
   131  		return NewCircle(box)
   132  	case CLASS_TYPE:
   133  		return NewClass(box)
   134  	case CLOUD_TYPE:
   135  		return NewCloud(box)
   136  	case CODE_TYPE:
   137  		return NewCode(box)
   138  	case CYLINDER_TYPE:
   139  		return NewCylinder(box)
   140  	case DIAMOND_TYPE:
   141  		return NewDiamond(box)
   142  	case DOCUMENT_TYPE:
   143  		return NewDocument(box)
   144  	case HEXAGON_TYPE:
   145  		return NewHexagon(box)
   146  	case IMAGE_TYPE:
   147  		return NewImage(box)
   148  	case OVAL_TYPE:
   149  		return NewOval(box)
   150  	case PACKAGE_TYPE:
   151  		return NewPackage(box)
   152  	case PAGE_TYPE:
   153  		return NewPage(box)
   154  	case PARALLELOGRAM_TYPE:
   155  		return NewParallelogram(box)
   156  	case PERSON_TYPE:
   157  		return NewPerson(box)
   158  	case QUEUE_TYPE:
   159  		return NewQueue(box)
   160  	case REAL_SQUARE_TYPE:
   161  		return NewRealSquare(box)
   162  	case STEP_TYPE:
   163  		return NewStep(box)
   164  	case STORED_DATA_TYPE:
   165  		return NewStoredData(box)
   166  	case SQUARE_TYPE:
   167  		return NewSquare(box)
   168  	case TABLE_TYPE:
   169  		return NewTable(box)
   170  	case TEXT_TYPE:
   171  		return NewText(box)
   172  
   173  	default:
   174  		shape := shapeSquare{
   175  			baseShape: &baseShape{
   176  				Type: shapeType,
   177  				Box:  box,
   178  			},
   179  		}
   180  		shape.FullShape = go2.Pointer(Shape(shape))
   181  		return shape
   182  	}
   183  }
   184  
   185  // TraceToShapeBorder takes the point on the rectangular border
   186  // r here is the point on rectangular border
   187  // p is the prev point (used to calculate slope)
   188  // s is the point on the actual shape border that'll be returned
   189  //
   190  // .      p
   191  // .      │
   192  // .      │
   193  // .      ▼
   194  // . ┌────r─────────────────────────┐
   195  // . │                              │
   196  // . │    │                         │
   197  // . │    │      xxxxxxxx           │
   198  // . │    ▼  xxxxx       xxxx       │
   199  // . │    sxxx               xx     │
   200  // . │   x                    xx    │
   201  // . │  xx                     xx   │
   202  // . │  x                      xx   │
   203  // . │  xx                   xxx    │
   204  // . │   xxxx             xxxx      │
   205  // . └──────xxxxxxxxxxxxxx──────────┘
   206  func TraceToShapeBorder(shape Shape, rectBorderPoint, prevPoint *geo.Point) *geo.Point {
   207  	if shape.Is("") || shape.IsRectangular() {
   208  		return rectBorderPoint
   209  	}
   210  
   211  	// We want to extend the line all the way through to the other end of the shape to get the intersections
   212  	scaleSize := shape.GetBox().Width
   213  	if prevPoint.X == rectBorderPoint.X {
   214  		scaleSize = shape.GetBox().Height
   215  	}
   216  	vector := prevPoint.VectorTo(rectBorderPoint)
   217  	vector = vector.AddLength(scaleSize)
   218  	extendedSegment := geo.Segment{Start: prevPoint, End: prevPoint.AddVector(vector)}
   219  
   220  	closestD := math.Inf(1)
   221  	closestPoint := rectBorderPoint
   222  
   223  	for _, perimeterSegment := range shape.Perimeter() {
   224  		for _, intersectingPoint := range perimeterSegment.Intersections(extendedSegment) {
   225  			d := geo.EuclideanDistance(rectBorderPoint.X, rectBorderPoint.Y, intersectingPoint.X, intersectingPoint.Y)
   226  			if d < closestD {
   227  				closestD = d
   228  				closestPoint = intersectingPoint
   229  			}
   230  		}
   231  	}
   232  
   233  	closestPoint.TruncateFloat32()
   234  	return geo.NewPoint(math.Round(closestPoint.X), math.Round(closestPoint.Y))
   235  }
   236  
   237  func boxPath(box *geo.Box) *svg.SvgPathContext {
   238  	pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
   239  	pc.StartAt(pc.Absolute(0, 0))
   240  	pc.L(false, box.Width, 0)
   241  	pc.L(false, box.Width, box.Height)
   242  	pc.L(false, 0, box.Height)
   243  	pc.Z()
   244  	return pc
   245  }
   246  
   247  func LimitAR(width, height, aspectRatio float64) (float64, float64) {
   248  	if width > aspectRatio*height {
   249  		height = math.Round(width / aspectRatio)
   250  	} else if height > aspectRatio*width {
   251  		width = math.Round(height / aspectRatio)
   252  	}
   253  	return width, height
   254  }
   255  

View as plain text