2022-11-03 13:54:49 +00:00
|
|
|
package shape
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"math"
|
|
|
|
|
|
|
|
|
|
"oss.terrastruct.com/d2/lib/geo"
|
2023-01-21 04:04:59 +00:00
|
|
|
"oss.terrastruct.com/d2/lib/svg"
|
2023-02-10 19:03:07 +00:00
|
|
|
"oss.terrastruct.com/util-go/go2"
|
2022-11-03 13:54:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
SQUARE_TYPE = "Square"
|
|
|
|
|
REAL_SQUARE_TYPE = "RealSquare"
|
|
|
|
|
PARALLELOGRAM_TYPE = "Parallelogram"
|
|
|
|
|
DOCUMENT_TYPE = "Document"
|
|
|
|
|
CYLINDER_TYPE = "Cylinder"
|
|
|
|
|
QUEUE_TYPE = "Queue"
|
|
|
|
|
PAGE_TYPE = "Page"
|
|
|
|
|
PACKAGE_TYPE = "Package"
|
|
|
|
|
STEP_TYPE = "Step"
|
|
|
|
|
CALLOUT_TYPE = "Callout"
|
|
|
|
|
STORED_DATA_TYPE = "StoredData"
|
|
|
|
|
PERSON_TYPE = "Person"
|
|
|
|
|
DIAMOND_TYPE = "Diamond"
|
|
|
|
|
OVAL_TYPE = "Oval"
|
|
|
|
|
CIRCLE_TYPE = "Circle"
|
|
|
|
|
HEXAGON_TYPE = "Hexagon"
|
|
|
|
|
CLOUD_TYPE = "Cloud"
|
|
|
|
|
|
|
|
|
|
TABLE_TYPE = "Table"
|
|
|
|
|
CLASS_TYPE = "Class"
|
|
|
|
|
TEXT_TYPE = "Text"
|
|
|
|
|
CODE_TYPE = "Code"
|
|
|
|
|
IMAGE_TYPE = "Image"
|
2023-01-24 03:10:31 +00:00
|
|
|
|
|
|
|
|
defaultPadding = 40.
|
2022-11-03 13:54:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Shape interface {
|
|
|
|
|
Is(shape string) bool
|
|
|
|
|
GetType() string
|
|
|
|
|
|
|
|
|
|
AspectRatio1() bool
|
|
|
|
|
IsRectangular() bool
|
|
|
|
|
|
|
|
|
|
GetBox() *geo.Box
|
|
|
|
|
GetInnerBox() *geo.Box
|
|
|
|
|
|
|
|
|
|
// placing a rectangle of the given size and padding inside the shape, return the position relative to the shape's TopLeft
|
2023-02-11 00:19:19 +00:00
|
|
|
GetInsidePlacement(width, height, paddingX, paddingY float64) geo.Point
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2023-01-23 18:32:12 +00:00
|
|
|
GetDimensionsToFit(width, height, paddingX, paddingY float64) (float64, float64)
|
|
|
|
|
GetDefaultPadding() (paddingX, paddingY float64)
|
2022-11-03 13:54:49 +00:00
|
|
|
|
|
|
|
|
// Perimeter returns a slice of geo.Intersectables that together constitute the shape border
|
|
|
|
|
Perimeter() []geo.Intersectable
|
|
|
|
|
|
|
|
|
|
GetSVGPathData() []string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type baseShape struct {
|
2023-02-10 19:03:07 +00:00
|
|
|
Type string
|
|
|
|
|
Box *geo.Box
|
|
|
|
|
FullShape *Shape
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s baseShape) Is(shapeType string) bool {
|
|
|
|
|
return s.Type == shapeType
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s baseShape) GetType() string {
|
|
|
|
|
return s.Type
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s baseShape) AspectRatio1() bool {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s baseShape) IsRectangular() bool {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s baseShape) GetBox() *geo.Box {
|
|
|
|
|
return s.Box
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s baseShape) GetInnerBox() *geo.Box {
|
|
|
|
|
return s.Box
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-11 00:19:19 +00:00
|
|
|
func (s baseShape) GetInsidePlacement(_, _, paddingX, paddingY float64) geo.Point {
|
2023-02-10 19:03:07 +00:00
|
|
|
innerTL := (*s.FullShape).GetInnerBox().TopLeft
|
2023-02-11 00:19:19 +00:00
|
|
|
return *geo.NewPoint(innerTL.X+paddingX/2, innerTL.Y+paddingY/2)
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-21 04:04:59 +00:00
|
|
|
// return the minimum shape dimensions needed to fit content (width x height)
|
|
|
|
|
// in the shape's innerBox with padding
|
2023-01-23 18:32:12 +00:00
|
|
|
func (s baseShape) GetDimensionsToFit(width, height, paddingX, paddingY float64) (float64, float64) {
|
2023-01-25 01:32:42 +00:00
|
|
|
return math.Ceil(width + paddingX), math.Ceil(height + paddingY)
|
2023-01-23 18:32:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s baseShape) GetDefaultPadding() (paddingX, paddingY float64) {
|
2023-01-24 03:10:31 +00:00
|
|
|
return defaultPadding, defaultPadding
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s baseShape) Perimeter() []geo.Intersectable {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s baseShape) GetSVGPathData() []string {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewShape(shapeType string, box *geo.Box) Shape {
|
|
|
|
|
switch shapeType {
|
|
|
|
|
case CALLOUT_TYPE:
|
|
|
|
|
return NewCallout(box)
|
|
|
|
|
case CIRCLE_TYPE:
|
|
|
|
|
return NewCircle(box)
|
|
|
|
|
case CLASS_TYPE:
|
|
|
|
|
return NewClass(box)
|
|
|
|
|
case CLOUD_TYPE:
|
|
|
|
|
return NewCloud(box)
|
|
|
|
|
case CODE_TYPE:
|
|
|
|
|
return NewCode(box)
|
|
|
|
|
case CYLINDER_TYPE:
|
|
|
|
|
return NewCylinder(box)
|
|
|
|
|
case DIAMOND_TYPE:
|
|
|
|
|
return NewDiamond(box)
|
|
|
|
|
case DOCUMENT_TYPE:
|
|
|
|
|
return NewDocument(box)
|
|
|
|
|
case HEXAGON_TYPE:
|
|
|
|
|
return NewHexagon(box)
|
|
|
|
|
case IMAGE_TYPE:
|
|
|
|
|
return NewImage(box)
|
|
|
|
|
case OVAL_TYPE:
|
|
|
|
|
return NewOval(box)
|
|
|
|
|
case PACKAGE_TYPE:
|
|
|
|
|
return NewPackage(box)
|
|
|
|
|
case PAGE_TYPE:
|
|
|
|
|
return NewPage(box)
|
|
|
|
|
case PARALLELOGRAM_TYPE:
|
|
|
|
|
return NewParallelogram(box)
|
|
|
|
|
case PERSON_TYPE:
|
|
|
|
|
return NewPerson(box)
|
|
|
|
|
case QUEUE_TYPE:
|
|
|
|
|
return NewQueue(box)
|
|
|
|
|
case REAL_SQUARE_TYPE:
|
|
|
|
|
return NewRealSquare(box)
|
|
|
|
|
case STEP_TYPE:
|
|
|
|
|
return NewStep(box)
|
|
|
|
|
case STORED_DATA_TYPE:
|
|
|
|
|
return NewStoredData(box)
|
|
|
|
|
case SQUARE_TYPE:
|
|
|
|
|
return NewSquare(box)
|
|
|
|
|
case TABLE_TYPE:
|
|
|
|
|
return NewTable(box)
|
|
|
|
|
case TEXT_TYPE:
|
|
|
|
|
return NewText(box)
|
|
|
|
|
|
|
|
|
|
default:
|
2023-02-10 19:03:07 +00:00
|
|
|
shape := shapeSquare{
|
2022-11-03 13:54:49 +00:00
|
|
|
baseShape: &baseShape{
|
|
|
|
|
Type: shapeType,
|
|
|
|
|
Box: box,
|
|
|
|
|
},
|
|
|
|
|
}
|
2023-02-10 19:03:07 +00:00
|
|
|
shape.FullShape = go2.Pointer(Shape(shape))
|
|
|
|
|
return shape
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TraceToShapeBorder takes the point on the rectangular border
|
|
|
|
|
// r here is the point on rectangular border
|
|
|
|
|
// p is the prev point (used to calculate slope)
|
|
|
|
|
// s is the point on the actual shape border that'll be returned
|
|
|
|
|
//
|
|
|
|
|
// p
|
|
|
|
|
// │
|
|
|
|
|
// │
|
|
|
|
|
// ▼
|
|
|
|
|
// ┌────r─────────────────────────┐
|
|
|
|
|
// │ │
|
|
|
|
|
// │ │ │
|
|
|
|
|
// │ │ xxxxxxxx │
|
|
|
|
|
// │ ▼ xxxxx xxxx │
|
|
|
|
|
// │ sxxx xx │
|
|
|
|
|
// │ x xx │
|
|
|
|
|
// │ xx xx │
|
|
|
|
|
// │ x xx │
|
|
|
|
|
// │ xx xxx │
|
|
|
|
|
// │ xxxx xxxx │
|
|
|
|
|
// └──────xxxxxxxxxxxxxx──────────┘
|
|
|
|
|
func TraceToShapeBorder(shape Shape, rectBorderPoint, prevPoint *geo.Point) *geo.Point {
|
|
|
|
|
if shape.Is("") || shape.IsRectangular() {
|
|
|
|
|
return rectBorderPoint
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We want to extend the line all the way through to the other end of the shape to get the intersections
|
|
|
|
|
scaleSize := shape.GetBox().Width
|
|
|
|
|
if prevPoint.X == rectBorderPoint.X {
|
|
|
|
|
scaleSize = shape.GetBox().Height
|
|
|
|
|
}
|
|
|
|
|
vector := prevPoint.VectorTo(rectBorderPoint)
|
|
|
|
|
vector = vector.AddLength(scaleSize)
|
|
|
|
|
extendedSegment := geo.Segment{Start: prevPoint, End: prevPoint.AddVector(vector)}
|
|
|
|
|
|
|
|
|
|
closestD := math.Inf(1)
|
|
|
|
|
closestPoint := rectBorderPoint
|
|
|
|
|
|
|
|
|
|
for _, perimeterSegment := range shape.Perimeter() {
|
|
|
|
|
for _, intersectingPoint := range perimeterSegment.Intersections(extendedSegment) {
|
|
|
|
|
d := geo.EuclideanDistance(rectBorderPoint.X, rectBorderPoint.Y, intersectingPoint.X, intersectingPoint.Y)
|
|
|
|
|
if d < closestD {
|
|
|
|
|
closestD = d
|
|
|
|
|
closestPoint = intersectingPoint
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return geo.NewPoint(math.Round(closestPoint.X), math.Round(closestPoint.Y))
|
|
|
|
|
}
|
2023-01-21 04:04:59 +00:00
|
|
|
|
|
|
|
|
func boxPath(box *geo.Box) *svg.SvgPathContext {
|
|
|
|
|
pc := svg.NewSVGPathContext(box.TopLeft, 1, 1)
|
|
|
|
|
pc.StartAt(pc.Absolute(0, 0))
|
|
|
|
|
pc.L(false, box.Width, 0)
|
|
|
|
|
pc.L(false, box.Width, box.Height)
|
|
|
|
|
pc.L(false, 0, box.Height)
|
|
|
|
|
pc.Z()
|
|
|
|
|
return pc
|
|
|
|
|
}
|
2023-01-31 23:58:06 +00:00
|
|
|
|
|
|
|
|
func LimitAR(width, height, aspectRatio float64) (float64, float64) {
|
|
|
|
|
if width > aspectRatio*height {
|
|
|
|
|
height = math.Round(width / aspectRatio)
|
|
|
|
|
} else if height > aspectRatio*width {
|
|
|
|
|
width = math.Round(height / aspectRatio)
|
|
|
|
|
}
|
|
|
|
|
return width, height
|
|
|
|
|
}
|