742 lines
19 KiB
Go
742 lines
19 KiB
Go
package d2target
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"math"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"oss.terrastruct.com/util-go/go2"
|
|
|
|
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
|
"oss.terrastruct.com/d2/lib/color"
|
|
"oss.terrastruct.com/d2/lib/geo"
|
|
"oss.terrastruct.com/d2/lib/label"
|
|
"oss.terrastruct.com/d2/lib/shape"
|
|
"oss.terrastruct.com/d2/lib/svg"
|
|
)
|
|
|
|
const (
|
|
DEFAULT_ICON_SIZE = 32
|
|
MAX_ICON_SIZE = 64
|
|
|
|
THREE_DEE_OFFSET = 15
|
|
MULTIPLE_OFFSET = 10
|
|
|
|
INNER_BORDER_OFFSET = 5
|
|
|
|
BG_COLOR = color.N7
|
|
FG_COLOR = color.N1
|
|
)
|
|
|
|
var BorderOffset = geo.NewVector(5, 5)
|
|
|
|
type Diagram struct {
|
|
Name string `json:"name"`
|
|
// See docs on the same field in d2graph to understand what it means.
|
|
IsFolderOnly bool `json:"isFolderOnly"`
|
|
Description string `json:"description,omitempty"`
|
|
FontFamily *d2fonts.FontFamily `json:"fontFamily,omitempty"`
|
|
|
|
Shapes []Shape `json:"shapes"`
|
|
Connections []Connection `json:"connections"`
|
|
|
|
Root Shape `json:"root"`
|
|
// Maybe Icon can be used as a watermark in the root shape
|
|
|
|
Layers []*Diagram `json:"layers,omitempty"`
|
|
Scenarios []*Diagram `json:"scenarios,omitempty"`
|
|
Steps []*Diagram `json:"steps,omitempty"`
|
|
}
|
|
|
|
func (diagram Diagram) Bytes() ([]byte, error) {
|
|
b1, err := json.Marshal(diagram.Shapes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b2, err := json.Marshal(diagram.Connections)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
base := append(b1, b2...)
|
|
|
|
for _, d := range diagram.Layers {
|
|
slices, err := d.Bytes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
base = append(base, slices...)
|
|
}
|
|
for _, d := range diagram.Scenarios {
|
|
slices, err := d.Bytes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
base = append(base, slices...)
|
|
}
|
|
for _, d := range diagram.Steps {
|
|
slices, err := d.Bytes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
base = append(base, slices...)
|
|
}
|
|
|
|
return base, nil
|
|
}
|
|
|
|
func (diagram Diagram) HasShape(condition func(Shape) bool) bool {
|
|
for _, d := range diagram.Layers {
|
|
if d.HasShape(condition) {
|
|
return true
|
|
}
|
|
}
|
|
for _, d := range diagram.Scenarios {
|
|
if d.HasShape(condition) {
|
|
return true
|
|
}
|
|
}
|
|
for _, d := range diagram.Steps {
|
|
if d.HasShape(condition) {
|
|
return true
|
|
}
|
|
}
|
|
for _, s := range diagram.Shapes {
|
|
if condition(s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (diagram Diagram) HashID() (string, error) {
|
|
bytes, err := diagram.Bytes()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
h := fnv.New32a()
|
|
h.Write(bytes)
|
|
// CSS names can't start with numbers, so prepend a little something
|
|
return fmt.Sprintf("d2-%d", h.Sum32()), nil
|
|
}
|
|
|
|
func (diagram Diagram) NestedBoundingBox() (topLeft, bottomRight Point) {
|
|
tl, br := diagram.BoundingBox()
|
|
for _, d := range diagram.Layers {
|
|
tl2, br2 := d.NestedBoundingBox()
|
|
tl.X = go2.Min(tl.X, tl2.X)
|
|
tl.Y = go2.Min(tl.Y, tl2.Y)
|
|
br.X = go2.Max(br.X, br2.X)
|
|
br.Y = go2.Max(br.Y, br2.Y)
|
|
}
|
|
for _, d := range diagram.Scenarios {
|
|
tl2, br2 := d.NestedBoundingBox()
|
|
tl.X = go2.Min(tl.X, tl2.X)
|
|
tl.Y = go2.Min(tl.Y, tl2.Y)
|
|
br.X = go2.Max(br.X, br2.X)
|
|
br.Y = go2.Max(br.Y, br2.Y)
|
|
}
|
|
for _, d := range diagram.Steps {
|
|
tl2, br2 := d.NestedBoundingBox()
|
|
tl.X = go2.Min(tl.X, tl2.X)
|
|
tl.Y = go2.Min(tl.Y, tl2.Y)
|
|
br.X = go2.Max(br.X, br2.X)
|
|
br.Y = go2.Max(br.Y, br2.Y)
|
|
}
|
|
return tl, br
|
|
}
|
|
|
|
func (diagram Diagram) BoundingBox() (topLeft, bottomRight Point) {
|
|
if len(diagram.Shapes) == 0 {
|
|
return Point{0, 0}, Point{0, 0}
|
|
}
|
|
x1 := int(math.MaxInt32)
|
|
y1 := int(math.MaxInt32)
|
|
x2 := int(math.MinInt32)
|
|
y2 := int(math.MinInt32)
|
|
|
|
for _, targetShape := range diagram.Shapes {
|
|
x1 = go2.Min(x1, targetShape.Pos.X-int(math.Ceil(float64(targetShape.StrokeWidth)/2.)))
|
|
y1 = go2.Min(y1, targetShape.Pos.Y-int(math.Ceil(float64(targetShape.StrokeWidth)/2.)))
|
|
x2 = go2.Max(x2, targetShape.Pos.X+targetShape.Width+int(math.Ceil(float64(targetShape.StrokeWidth)/2.)))
|
|
y2 = go2.Max(y2, targetShape.Pos.Y+targetShape.Height+int(math.Ceil(float64(targetShape.StrokeWidth)/2.)))
|
|
|
|
if targetShape.Tooltip != "" || targetShape.Link != "" {
|
|
// 16 is the icon radius
|
|
y1 = go2.Min(y1, targetShape.Pos.Y-targetShape.StrokeWidth-16)
|
|
x2 = go2.Max(x2, targetShape.Pos.X+targetShape.StrokeWidth+targetShape.Width+16)
|
|
}
|
|
|
|
if targetShape.ThreeDee {
|
|
y1 = go2.Min(y1, targetShape.Pos.Y-THREE_DEE_OFFSET-targetShape.StrokeWidth)
|
|
x2 = go2.Max(x2, targetShape.Pos.X+THREE_DEE_OFFSET+targetShape.Width+targetShape.StrokeWidth)
|
|
}
|
|
if targetShape.Multiple {
|
|
y1 = go2.Min(y1, targetShape.Pos.Y-MULTIPLE_OFFSET-targetShape.StrokeWidth)
|
|
x2 = go2.Max(x2, targetShape.Pos.X+MULTIPLE_OFFSET+targetShape.Width+targetShape.StrokeWidth)
|
|
}
|
|
|
|
if targetShape.Icon != nil && label.Position(targetShape.IconPosition).IsOutside() {
|
|
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(targetShape.Width), float64(targetShape.Height))
|
|
s := shape.NewShape(targetShape.Type, contentBox)
|
|
size := GetIconSize(s.GetInnerBox(), targetShape.IconPosition)
|
|
|
|
if strings.HasPrefix(targetShape.IconPosition, "OUTSIDE_TOP") {
|
|
y1 = go2.Min(y1, targetShape.Pos.Y-label.PADDING-size)
|
|
} else if strings.HasPrefix(targetShape.IconPosition, "OUTSIDE_BOTTOM") {
|
|
y2 = go2.Max(y2, targetShape.Pos.Y+label.PADDING+size)
|
|
} else if strings.HasPrefix(targetShape.IconPosition, "OUTSIDE_LEFT") {
|
|
x1 = go2.Min(x1, targetShape.Pos.X-label.PADDING-size)
|
|
} else if strings.HasPrefix(targetShape.IconPosition, "OUTSIDE_RIGHT") {
|
|
x2 = go2.Max(x2, targetShape.Pos.X+label.PADDING+size)
|
|
}
|
|
}
|
|
|
|
if targetShape.Label != "" {
|
|
labelPosition := label.Position(targetShape.LabelPosition)
|
|
if !labelPosition.IsOutside() {
|
|
continue
|
|
}
|
|
|
|
shapeType := DSL_SHAPE_TO_SHAPE_TYPE[targetShape.Type]
|
|
s := shape.NewShape(shapeType,
|
|
geo.NewBox(
|
|
geo.NewPoint(float64(targetShape.Pos.X), float64(targetShape.Pos.Y)),
|
|
float64(targetShape.Width),
|
|
float64(targetShape.Height),
|
|
),
|
|
)
|
|
|
|
labelTL := labelPosition.GetPointOnBox(s.GetBox(), label.PADDING, float64(targetShape.LabelWidth), float64(targetShape.LabelHeight))
|
|
x1 = go2.Min(x1, int(labelTL.X))
|
|
y1 = go2.Min(y1, int(labelTL.Y))
|
|
x2 = go2.Max(x2, int(labelTL.X)+targetShape.LabelWidth)
|
|
y2 = go2.Max(y2, int(labelTL.Y)+targetShape.LabelHeight)
|
|
}
|
|
}
|
|
|
|
for _, connection := range diagram.Connections {
|
|
for _, point := range connection.Route {
|
|
x1 = go2.Min(x1, int(math.Floor(point.X))-int(math.Ceil(float64(connection.StrokeWidth)/2.)))
|
|
y1 = go2.Min(y1, int(math.Floor(point.Y))-int(math.Ceil(float64(connection.StrokeWidth)/2.)))
|
|
x2 = go2.Max(x2, int(math.Ceil(point.X))+int(math.Ceil(float64(connection.StrokeWidth)/2.)))
|
|
y2 = go2.Max(y2, int(math.Ceil(point.Y))+int(math.Ceil(float64(connection.StrokeWidth)/2.)))
|
|
}
|
|
|
|
if connection.Label != "" {
|
|
labelTL := connection.GetLabelTopLeft()
|
|
x1 = go2.Min(x1, int(labelTL.X))
|
|
y1 = go2.Min(y1, int(labelTL.Y))
|
|
x2 = go2.Max(x2, int(labelTL.X)+connection.LabelWidth)
|
|
y2 = go2.Max(y2, int(labelTL.Y)+connection.LabelHeight)
|
|
}
|
|
}
|
|
|
|
return Point{x1, y1}, Point{x2, y2}
|
|
}
|
|
|
|
func (diagram Diagram) GetUniqueChars() string {
|
|
var uniqueChars string
|
|
uniqueMap := make(map[rune]bool)
|
|
for _, s := range diagram.Shapes {
|
|
for _, char := range s.Label {
|
|
if _, exists := uniqueMap[char]; !exists {
|
|
uniqueMap[char] = true
|
|
uniqueChars = uniqueChars + string(char)
|
|
}
|
|
}
|
|
for _, char := range s.Tooltip {
|
|
if _, exists := uniqueMap[char]; !exists {
|
|
uniqueMap[char] = true
|
|
uniqueChars = uniqueChars + string(char)
|
|
}
|
|
}
|
|
for _, char := range s.Link {
|
|
if _, exists := uniqueMap[char]; !exists {
|
|
uniqueMap[char] = true
|
|
uniqueChars = uniqueChars + string(char)
|
|
}
|
|
}
|
|
if s.Type == ShapeClass {
|
|
for _, cf := range s.Fields {
|
|
uniqueChars = uniqueChars + cf.GetUniqueChars(uniqueMap)
|
|
}
|
|
for _, cm := range s.Methods {
|
|
uniqueChars = uniqueChars + cm.GetUniqueChars(uniqueMap)
|
|
}
|
|
}
|
|
if s.Type == ShapeSQLTable {
|
|
for _, c := range s.Columns {
|
|
uniqueChars = uniqueChars + c.GetUniqueChars(uniqueMap)
|
|
}
|
|
}
|
|
}
|
|
for _, c := range diagram.Connections {
|
|
for _, char := range c.Label {
|
|
if _, exists := uniqueMap[char]; !exists {
|
|
uniqueMap[char] = true
|
|
uniqueChars = uniqueChars + string(char)
|
|
}
|
|
}
|
|
}
|
|
|
|
return uniqueChars
|
|
}
|
|
|
|
func NewDiagram() *Diagram {
|
|
return &Diagram{
|
|
Root: Shape{
|
|
Fill: BG_COLOR,
|
|
},
|
|
}
|
|
}
|
|
|
|
type Shape struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
|
|
Pos Point `json:"pos"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
|
|
Opacity float64 `json:"opacity"`
|
|
StrokeDash float64 `json:"strokeDash"`
|
|
StrokeWidth int `json:"strokeWidth"`
|
|
|
|
BorderRadius int `json:"borderRadius"`
|
|
|
|
Fill string `json:"fill"`
|
|
FillPattern string `json:"fillPattern,omitempty"`
|
|
Stroke string `json:"stroke"`
|
|
|
|
Shadow bool `json:"shadow"`
|
|
ThreeDee bool `json:"3d"`
|
|
Multiple bool `json:"multiple"`
|
|
DoubleBorder bool `json:"double-border"`
|
|
|
|
Tooltip string `json:"tooltip"`
|
|
Link string `json:"link"`
|
|
Icon *url.URL `json:"icon"`
|
|
IconPosition string `json:"iconPosition"`
|
|
|
|
// Whether the shape should allow shapes behind it to bleed through
|
|
// Currently just used for sequence diagram groups
|
|
Blend bool `json:"blend"`
|
|
|
|
Class
|
|
SQLTable
|
|
|
|
Text
|
|
|
|
LabelPosition string `json:"labelPosition,omitempty"`
|
|
|
|
ZIndex int `json:"zIndex"`
|
|
Level int `json:"level"`
|
|
|
|
// These are used for special shapes, sql_table and class
|
|
PrimaryAccentColor string `json:"primaryAccentColor,omitempty"`
|
|
SecondaryAccentColor string `json:"secondaryAccentColor,omitempty"`
|
|
NeutralAccentColor string `json:"neutralAccentColor,omitempty"`
|
|
}
|
|
|
|
func (s Shape) GetFontColor() string {
|
|
if s.Type == ShapeClass || s.Type == ShapeSQLTable {
|
|
if !color.IsThemeColor(s.Color) {
|
|
return s.Color
|
|
}
|
|
return s.Stroke
|
|
}
|
|
if s.Color != color.Empty {
|
|
return s.Color
|
|
}
|
|
return color.N1
|
|
}
|
|
|
|
// TODO remove this function, just set fields on themeable
|
|
func (s Shape) CSSStyle() string {
|
|
out := ""
|
|
|
|
out += fmt.Sprintf(`stroke-width:%d;`, s.StrokeWidth)
|
|
if s.StrokeDash != 0 {
|
|
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(s.StrokeWidth), s.StrokeDash)
|
|
out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func (s *Shape) SetType(t string) {
|
|
// Some types are synonyms of other types, but with hinting for autolayout
|
|
// They should only have one representation in the final export
|
|
if strings.EqualFold(t, ShapeCircle) {
|
|
t = ShapeOval
|
|
} else if strings.EqualFold(t, ShapeSquare) {
|
|
t = ShapeRectangle
|
|
}
|
|
s.Type = strings.ToLower(t)
|
|
}
|
|
|
|
func (s Shape) GetZIndex() int {
|
|
return s.ZIndex
|
|
}
|
|
|
|
func (s Shape) GetID() string {
|
|
return s.ID
|
|
}
|
|
|
|
type Text struct {
|
|
Label string `json:"label"`
|
|
FontSize int `json:"fontSize"`
|
|
FontFamily string `json:"fontFamily"`
|
|
Language string `json:"language"`
|
|
Color string `json:"color"`
|
|
|
|
Italic bool `json:"italic"`
|
|
Bold bool `json:"bold"`
|
|
Underline bool `json:"underline"`
|
|
|
|
LabelWidth int `json:"labelWidth"`
|
|
LabelHeight int `json:"labelHeight"`
|
|
LabelFill string `json:"labelFill,omitempty"`
|
|
}
|
|
|
|
func BaseShape() *Shape {
|
|
return &Shape{
|
|
Opacity: 1,
|
|
StrokeDash: 0,
|
|
StrokeWidth: 2,
|
|
Text: Text{
|
|
Bold: true,
|
|
FontFamily: "DEFAULT",
|
|
},
|
|
}
|
|
}
|
|
|
|
type Connection struct {
|
|
ID string `json:"id"`
|
|
|
|
Src string `json:"src"`
|
|
SrcArrow Arrowhead `json:"srcArrow"`
|
|
SrcLabel string `json:"srcLabel"`
|
|
|
|
Dst string `json:"dst"`
|
|
DstArrow Arrowhead `json:"dstArrow"`
|
|
DstLabel string `json:"dstLabel"`
|
|
|
|
Opacity float64 `json:"opacity"`
|
|
StrokeDash float64 `json:"strokeDash"`
|
|
StrokeWidth int `json:"strokeWidth"`
|
|
Stroke string `json:"stroke"`
|
|
Fill string `json:"fill,omitempty"`
|
|
BorderRadius float64 `json:"borderRadius,omitempty"`
|
|
|
|
Text
|
|
LabelPosition string `json:"labelPosition"`
|
|
LabelPercentage float64 `json:"labelPercentage"`
|
|
|
|
Route []*geo.Point `json:"route"`
|
|
IsCurve bool `json:"isCurve,omitempty"`
|
|
|
|
Animated bool `json:"animated"`
|
|
Tooltip string `json:"tooltip"`
|
|
Icon *url.URL `json:"icon"`
|
|
|
|
ZIndex int `json:"zIndex"`
|
|
}
|
|
|
|
func BaseConnection() *Connection {
|
|
return &Connection{
|
|
SrcArrow: NoArrowhead,
|
|
DstArrow: NoArrowhead,
|
|
Route: make([]*geo.Point, 0),
|
|
Opacity: 1,
|
|
StrokeDash: 0,
|
|
StrokeWidth: 2,
|
|
BorderRadius: 10,
|
|
Text: Text{
|
|
Italic: true,
|
|
FontFamily: "DEFAULT",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (c Connection) GetFontColor() string {
|
|
if c.Color != color.Empty {
|
|
return c.Color
|
|
}
|
|
return color.N1
|
|
}
|
|
|
|
func (c Connection) CSSStyle() string {
|
|
out := ""
|
|
|
|
out += fmt.Sprintf(`stroke-width:%d;`, c.StrokeWidth)
|
|
strokeDash := c.StrokeDash
|
|
if strokeDash == 0 && c.Animated {
|
|
strokeDash = 5
|
|
}
|
|
if strokeDash != 0 {
|
|
dashSize, gapSize := svg.GetStrokeDashAttributes(float64(c.StrokeWidth), strokeDash)
|
|
out += fmt.Sprintf(`stroke-dasharray:%f,%f;`, dashSize, gapSize)
|
|
|
|
if c.Animated {
|
|
dashOffset := -10
|
|
if c.SrcArrow != NoArrowhead && c.DstArrow == NoArrowhead {
|
|
dashOffset = 10
|
|
}
|
|
out += fmt.Sprintf(`stroke-dashoffset:%f;`, float64(dashOffset)*(dashSize+gapSize))
|
|
out += fmt.Sprintf(`animation: dashdraw %fs linear infinite;`, gapSize*0.5)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (c *Connection) GetLabelTopLeft() *geo.Point {
|
|
return label.Position(c.LabelPosition).GetPointOnRoute(
|
|
c.Route,
|
|
float64(c.StrokeWidth),
|
|
c.LabelPercentage,
|
|
float64(c.LabelWidth),
|
|
float64(c.LabelHeight),
|
|
)
|
|
}
|
|
|
|
func (c Connection) GetZIndex() int {
|
|
return c.ZIndex
|
|
}
|
|
|
|
func (c Connection) GetID() string {
|
|
return c.ID
|
|
}
|
|
|
|
type Arrowhead string
|
|
|
|
const (
|
|
NoArrowhead Arrowhead = "none"
|
|
ArrowArrowhead Arrowhead = "arrow"
|
|
TriangleArrowhead Arrowhead = "triangle"
|
|
DiamondArrowhead Arrowhead = "diamond"
|
|
FilledDiamondArrowhead Arrowhead = "filled-diamond"
|
|
CircleArrowhead Arrowhead = "circle"
|
|
FilledCircleArrowhead Arrowhead = "filled-circle"
|
|
|
|
// For fat arrows
|
|
LineArrowhead Arrowhead = "line"
|
|
|
|
// Crows feet notation
|
|
CfOne Arrowhead = "cf-one"
|
|
CfMany Arrowhead = "cf-many"
|
|
CfOneRequired Arrowhead = "cf-one-required"
|
|
CfManyRequired Arrowhead = "cf-many-required"
|
|
)
|
|
|
|
var Arrowheads = map[string]struct{}{
|
|
string(NoArrowhead): {},
|
|
string(ArrowArrowhead): {},
|
|
string(TriangleArrowhead): {},
|
|
string(DiamondArrowhead): {},
|
|
string(FilledDiamondArrowhead): {},
|
|
string(CircleArrowhead): {},
|
|
string(FilledCircleArrowhead): {},
|
|
string(CfOne): {},
|
|
string(CfMany): {},
|
|
string(CfOneRequired): {},
|
|
string(CfManyRequired): {},
|
|
}
|
|
|
|
func ToArrowhead(arrowheadType string, filled bool) Arrowhead {
|
|
switch arrowheadType {
|
|
case string(DiamondArrowhead):
|
|
if filled {
|
|
return FilledDiamondArrowhead
|
|
}
|
|
return DiamondArrowhead
|
|
case string(CircleArrowhead):
|
|
if filled {
|
|
return FilledCircleArrowhead
|
|
}
|
|
return CircleArrowhead
|
|
case string(ArrowArrowhead):
|
|
return ArrowArrowhead
|
|
case string(CfOne):
|
|
return CfOne
|
|
case string(CfMany):
|
|
return CfMany
|
|
case string(CfOneRequired):
|
|
return CfOneRequired
|
|
case string(CfManyRequired):
|
|
return CfManyRequired
|
|
default:
|
|
return TriangleArrowhead
|
|
}
|
|
}
|
|
|
|
type Point struct {
|
|
X int `json:"x"`
|
|
Y int `json:"y"`
|
|
}
|
|
|
|
func NewPoint(x, y int) Point {
|
|
return Point{X: x, Y: y}
|
|
}
|
|
|
|
const (
|
|
ShapeRectangle = "rectangle"
|
|
ShapeSquare = "square"
|
|
ShapePage = "page"
|
|
ShapeParallelogram = "parallelogram"
|
|
ShapeDocument = "document"
|
|
ShapeCylinder = "cylinder"
|
|
ShapeQueue = "queue"
|
|
ShapePackage = "package"
|
|
ShapeStep = "step"
|
|
ShapeCallout = "callout"
|
|
ShapeStoredData = "stored_data"
|
|
ShapePerson = "person"
|
|
ShapeDiamond = "diamond"
|
|
ShapeOval = "oval"
|
|
ShapeCircle = "circle"
|
|
ShapeHexagon = "hexagon"
|
|
ShapeCloud = "cloud"
|
|
ShapeText = "text"
|
|
ShapeCode = "code"
|
|
ShapeClass = "class"
|
|
ShapeSQLTable = "sql_table"
|
|
ShapeImage = "image"
|
|
ShapeSequenceDiagram = "sequence_diagram"
|
|
)
|
|
|
|
var Shapes = []string{
|
|
ShapeRectangle,
|
|
ShapeSquare,
|
|
ShapePage,
|
|
ShapeParallelogram,
|
|
ShapeDocument,
|
|
ShapeCylinder,
|
|
ShapeQueue,
|
|
ShapePackage,
|
|
ShapeStep,
|
|
ShapeCallout,
|
|
ShapeStoredData,
|
|
ShapePerson,
|
|
ShapeDiamond,
|
|
ShapeOval,
|
|
ShapeCircle,
|
|
ShapeHexagon,
|
|
ShapeCloud,
|
|
ShapeText,
|
|
ShapeCode,
|
|
ShapeClass,
|
|
ShapeSQLTable,
|
|
ShapeImage,
|
|
ShapeSequenceDiagram,
|
|
}
|
|
|
|
func IsShape(s string) bool {
|
|
if s == "" {
|
|
// Default shape is rectangle.
|
|
return true
|
|
}
|
|
for _, s2 := range Shapes {
|
|
if strings.EqualFold(s, s2) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type MText struct {
|
|
Text string `json:"text"`
|
|
FontSize int `json:"fontSize"`
|
|
IsBold bool `json:"isBold"`
|
|
IsItalic bool `json:"isItalic"`
|
|
Language string `json:"language"`
|
|
Shape string `json:"shape"`
|
|
|
|
Dimensions TextDimensions `json:"dimensions,omitempty"`
|
|
}
|
|
|
|
type TextDimensions struct {
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
}
|
|
|
|
func NewTextDimensions(w, h int) *TextDimensions {
|
|
return &TextDimensions{Width: w, Height: h}
|
|
}
|
|
|
|
func (text MText) GetColor(isItalic bool) string {
|
|
if isItalic {
|
|
return color.N2
|
|
}
|
|
return color.N1
|
|
}
|
|
|
|
var DSL_SHAPE_TO_SHAPE_TYPE = map[string]string{
|
|
"": shape.SQUARE_TYPE,
|
|
ShapeRectangle: shape.SQUARE_TYPE,
|
|
ShapeSquare: shape.REAL_SQUARE_TYPE,
|
|
ShapePage: shape.PAGE_TYPE,
|
|
ShapeParallelogram: shape.PARALLELOGRAM_TYPE,
|
|
ShapeDocument: shape.DOCUMENT_TYPE,
|
|
ShapeCylinder: shape.CYLINDER_TYPE,
|
|
ShapeQueue: shape.QUEUE_TYPE,
|
|
ShapePackage: shape.PACKAGE_TYPE,
|
|
ShapeStep: shape.STEP_TYPE,
|
|
ShapeCallout: shape.CALLOUT_TYPE,
|
|
ShapeStoredData: shape.STORED_DATA_TYPE,
|
|
ShapePerson: shape.PERSON_TYPE,
|
|
ShapeDiamond: shape.DIAMOND_TYPE,
|
|
ShapeOval: shape.OVAL_TYPE,
|
|
ShapeCircle: shape.CIRCLE_TYPE,
|
|
ShapeHexagon: shape.HEXAGON_TYPE,
|
|
ShapeCloud: shape.CLOUD_TYPE,
|
|
ShapeText: shape.TEXT_TYPE,
|
|
ShapeCode: shape.CODE_TYPE,
|
|
ShapeClass: shape.CLASS_TYPE,
|
|
ShapeSQLTable: shape.TABLE_TYPE,
|
|
ShapeImage: shape.IMAGE_TYPE,
|
|
ShapeSequenceDiagram: shape.SQUARE_TYPE,
|
|
}
|
|
|
|
var SHAPE_TYPE_TO_DSL_SHAPE map[string]string
|
|
|
|
func init() {
|
|
SHAPE_TYPE_TO_DSL_SHAPE = make(map[string]string, len(DSL_SHAPE_TO_SHAPE_TYPE))
|
|
for k, v := range DSL_SHAPE_TO_SHAPE_TYPE {
|
|
SHAPE_TYPE_TO_DSL_SHAPE[v] = k
|
|
}
|
|
}
|
|
|
|
func GetIconSize(box *geo.Box, position string) int {
|
|
iconPosition := label.Position(position)
|
|
|
|
minDimension := int(math.Min(box.Width, box.Height))
|
|
halfMinDimension := int(math.Ceil(0.5 * float64(minDimension)))
|
|
|
|
var size int
|
|
|
|
if iconPosition == label.InsideMiddleCenter {
|
|
size = halfMinDimension
|
|
} else {
|
|
size = go2.Min(
|
|
minDimension,
|
|
go2.Max(DEFAULT_ICON_SIZE, halfMinDimension),
|
|
)
|
|
}
|
|
|
|
size = go2.Min(size, MAX_ICON_SIZE)
|
|
|
|
if !iconPosition.IsOutside() {
|
|
size = go2.Min(size,
|
|
go2.Min(
|
|
go2.Max(int(box.Width)-2*label.PADDING, 0),
|
|
go2.Max(int(box.Height)-2*label.PADDING, 0),
|
|
),
|
|
)
|
|
}
|
|
|
|
return size
|
|
}
|