2022-11-03 13:54:49 +00:00
|
|
|
package d2graph
|
|
|
|
|
|
|
|
|
|
import (
|
2023-05-09 01:38:41 +00:00
|
|
|
"bytes"
|
2023-04-01 00:18:17 +00:00
|
|
|
"context"
|
2022-11-03 13:54:49 +00:00
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2023-08-05 03:16:25 +00:00
|
|
|
"io/fs"
|
2022-12-29 02:14:19 +00:00
|
|
|
"math"
|
2022-11-03 13:54:49 +00:00
|
|
|
"net/url"
|
2023-01-28 07:05:42 +00:00
|
|
|
"sort"
|
2022-11-03 13:54:49 +00:00
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2023-03-31 08:58:02 +00:00
|
|
|
"golang.org/x/text/cases"
|
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
|
|
2022-12-01 18:48:01 +00:00
|
|
|
"oss.terrastruct.com/util-go/go2"
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
"oss.terrastruct.com/d2/d2ast"
|
|
|
|
|
"oss.terrastruct.com/d2/d2format"
|
|
|
|
|
"oss.terrastruct.com/d2/d2parser"
|
|
|
|
|
"oss.terrastruct.com/d2/d2renderers/d2fonts"
|
2022-11-27 21:54:41 +00:00
|
|
|
"oss.terrastruct.com/d2/d2renderers/d2latex"
|
2022-11-03 13:54:49 +00:00
|
|
|
"oss.terrastruct.com/d2/d2target"
|
2023-03-14 17:40:52 +00:00
|
|
|
"oss.terrastruct.com/d2/d2themes"
|
|
|
|
|
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
|
2023-01-09 18:16:28 +00:00
|
|
|
"oss.terrastruct.com/d2/lib/color"
|
2022-11-03 13:54:49 +00:00
|
|
|
"oss.terrastruct.com/d2/lib/geo"
|
2023-06-22 20:24:24 +00:00
|
|
|
"oss.terrastruct.com/d2/lib/label"
|
2023-01-21 04:04:59 +00:00
|
|
|
"oss.terrastruct.com/d2/lib/shape"
|
2022-12-01 13:46:45 +00:00
|
|
|
"oss.terrastruct.com/d2/lib/textmeasure"
|
2022-11-03 13:54:49 +00:00
|
|
|
)
|
|
|
|
|
|
2022-12-05 20:48:03 +00:00
|
|
|
const INNER_LABEL_PADDING int = 5
|
2023-01-23 18:32:12 +00:00
|
|
|
const DEFAULT_SHAPE_SIZE = 100.
|
2023-02-03 22:53:17 +00:00
|
|
|
const MIN_SHAPE_SIZE = 5
|
2022-12-05 20:48:03 +00:00
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
type Graph struct {
|
2023-08-05 03:16:25 +00:00
|
|
|
FS fs.FS `json:"-"`
|
2023-03-01 18:43:53 +00:00
|
|
|
Parent *Graph `json:"-"`
|
|
|
|
|
Name string `json:"name"`
|
2023-02-27 22:33:33 +00:00
|
|
|
// IsFolderOnly indicates a board or scenario itself makes no modifications from its
|
|
|
|
|
// base. Folder only boards do not have a render and are used purely for organizing
|
2023-02-27 22:25:37 +00:00
|
|
|
// the board tree.
|
2023-02-27 22:33:33 +00:00
|
|
|
IsFolderOnly bool `json:"isFolderOnly"`
|
|
|
|
|
AST *d2ast.Map `json:"ast"`
|
2023-06-19 06:57:08 +00:00
|
|
|
// BaseAST is the AST of the original graph without inherited fields and edges
|
2023-06-21 01:55:48 +00:00
|
|
|
BaseAST *d2ast.Map `json:"-"`
|
2022-11-03 13:54:49 +00:00
|
|
|
|
|
|
|
|
Root *Object `json:"root"`
|
|
|
|
|
Edges []*Edge `json:"edges"`
|
|
|
|
|
Objects []*Object `json:"objects"`
|
2023-01-04 01:50:27 +00:00
|
|
|
|
|
|
|
|
Layers []*Graph `json:"layers,omitempty"`
|
|
|
|
|
Scenarios []*Graph `json:"scenarios,omitempty"`
|
|
|
|
|
Steps []*Graph `json:"steps,omitempty"`
|
2023-03-14 17:40:52 +00:00
|
|
|
|
|
|
|
|
Theme *d2themes.Theme `json:"theme,omitempty"`
|
2023-09-05 22:01:22 +00:00
|
|
|
|
|
|
|
|
// Object.Level uses the location of a nested graph
|
|
|
|
|
RootLevel int `json:"rootLevel,omitempty"`
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-22 08:21:21 +00:00
|
|
|
func NewGraph() *Graph {
|
|
|
|
|
d := &Graph{}
|
2022-11-03 13:54:49 +00:00
|
|
|
d.Root = &Object{
|
2023-04-14 03:04:55 +00:00
|
|
|
Graph: d,
|
|
|
|
|
Parent: nil,
|
|
|
|
|
Children: make(map[string]*Object),
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-03 01:56:32 +00:00
|
|
|
func (g *Graph) RootBoard() *Graph {
|
|
|
|
|
for g.Parent != nil {
|
|
|
|
|
g = g.Parent
|
|
|
|
|
}
|
|
|
|
|
return g
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-01 00:18:17 +00:00
|
|
|
type LayoutGraph func(context.Context, *Graph) error
|
2023-11-21 20:31:36 +00:00
|
|
|
type RouteEdges func(context.Context, *Graph, []*Edge) error
|
2023-04-01 00:18:17 +00:00
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
// TODO consider having different Scalar types
|
|
|
|
|
// Right now we'll hold any types in Value and just convert, e.g. floats
|
|
|
|
|
type Scalar struct {
|
|
|
|
|
Value string `json:"value"`
|
|
|
|
|
MapKey *d2ast.Key `json:"-"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO maybe rename to Shape
|
|
|
|
|
type Object struct {
|
|
|
|
|
Graph *Graph `json:"-"`
|
|
|
|
|
Parent *Object `json:"-"`
|
|
|
|
|
|
|
|
|
|
// IDVal is the actual value of the ID whereas ID is the value in d2 syntax.
|
|
|
|
|
// e.g. ID: "yes'\""
|
|
|
|
|
// IDVal: yes'"
|
|
|
|
|
//
|
|
|
|
|
// ID allows joining on . naively and construct a valid D2 key path
|
2023-04-14 03:04:55 +00:00
|
|
|
ID string `json:"id"`
|
|
|
|
|
IDVal string `json:"id_val"`
|
|
|
|
|
Map *d2ast.Map `json:"-"`
|
|
|
|
|
References []Reference `json:"references,omitempty"`
|
2022-11-03 13:54:49 +00:00
|
|
|
|
|
|
|
|
*geo.Box `json:"box,omitempty"`
|
|
|
|
|
LabelPosition *string `json:"labelPosition,omitempty"`
|
|
|
|
|
IconPosition *string `json:"iconPosition,omitempty"`
|
|
|
|
|
|
2023-11-17 02:08:33 +00:00
|
|
|
ContentAspectRatio *float64 `json:"contentAspectRatio,omitempty"`
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
Class *d2target.Class `json:"class,omitempty"`
|
|
|
|
|
SQLTable *d2target.SQLTable `json:"sql_table,omitempty"`
|
|
|
|
|
|
|
|
|
|
Children map[string]*Object `json:"-"`
|
|
|
|
|
ChildrenArray []*Object `json:"-"`
|
|
|
|
|
|
2023-04-14 03:04:55 +00:00
|
|
|
Attributes `json:"attributes"`
|
2022-11-29 22:21:23 +00:00
|
|
|
|
2022-11-30 19:22:43 +00:00
|
|
|
ZIndex int `json:"zIndex"`
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Attributes struct {
|
2023-04-14 03:04:55 +00:00
|
|
|
Label Scalar `json:"label"`
|
|
|
|
|
LabelDimensions d2target.TextDimensions `json:"labelDimensions"`
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
Style Style `json:"style"`
|
|
|
|
|
Icon *url.URL `json:"icon,omitempty"`
|
2023-02-20 22:15:47 +00:00
|
|
|
Tooltip *Scalar `json:"tooltip,omitempty"`
|
|
|
|
|
Link *Scalar `json:"link,omitempty"`
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2023-04-14 03:04:55 +00:00
|
|
|
WidthAttr *Scalar `json:"width,omitempty"`
|
|
|
|
|
HeightAttr *Scalar `json:"height,omitempty"`
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2023-02-18 22:54:10 +00:00
|
|
|
Top *Scalar `json:"top,omitempty"`
|
|
|
|
|
Left *Scalar `json:"left,omitempty"`
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
// TODO consider separate Attributes struct for shape-specific and edge-specific
|
|
|
|
|
// Shapes only
|
|
|
|
|
NearKey *d2ast.KeyPath `json:"near_key"`
|
|
|
|
|
Language string `json:"language,omitempty"`
|
|
|
|
|
// TODO: default to ShapeRectangle instead of empty string
|
|
|
|
|
Shape Scalar `json:"shape"`
|
2022-11-29 05:01:42 +00:00
|
|
|
|
2023-05-01 11:59:57 +00:00
|
|
|
Direction Scalar `json:"direction"`
|
|
|
|
|
Constraint []string `json:"constraint"`
|
2023-04-01 00:18:17 +00:00
|
|
|
|
2023-04-12 20:48:53 +00:00
|
|
|
GridRows *Scalar `json:"gridRows,omitempty"`
|
|
|
|
|
GridColumns *Scalar `json:"gridColumns,omitempty"`
|
|
|
|
|
GridGap *Scalar `json:"gridGap,omitempty"`
|
|
|
|
|
VerticalGap *Scalar `json:"verticalGap,omitempty"`
|
|
|
|
|
HorizontalGap *Scalar `json:"horizontalGap,omitempty"`
|
2023-02-06 21:32:08 +00:00
|
|
|
|
2023-06-22 23:13:08 +00:00
|
|
|
LabelPosition *Scalar `json:"labelPosition,omitempty"`
|
|
|
|
|
IconPosition *Scalar `json:"iconPosition,omitempty"`
|
|
|
|
|
|
2023-02-06 21:32:08 +00:00
|
|
|
// These names are attached to the rendered elements in SVG
|
|
|
|
|
// so that users can target them however they like outside of D2
|
|
|
|
|
Classes []string `json:"classes,omitempty"`
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
2023-03-31 08:58:02 +00:00
|
|
|
// ApplyTextTransform will alter the `Label.Value` of the current object based
|
|
|
|
|
// on the specification of the `text-transform` styling option. This function
|
|
|
|
|
// has side-effects!
|
|
|
|
|
func (a *Attributes) ApplyTextTransform() {
|
|
|
|
|
if a.Style.NoneTextTransform() {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if a.Style.TextTransform != nil && a.Style.TextTransform.Value == "uppercase" {
|
|
|
|
|
a.Label.Value = strings.ToUpper(a.Label.Value)
|
|
|
|
|
}
|
|
|
|
|
if a.Style.TextTransform != nil && a.Style.TextTransform.Value == "lowercase" {
|
|
|
|
|
a.Label.Value = strings.ToLower(a.Label.Value)
|
|
|
|
|
}
|
|
|
|
|
if a.Style.TextTransform != nil && a.Style.TextTransform.Value == "capitalize" {
|
|
|
|
|
a.Label.Value = cases.Title(language.Und).String(a.Label.Value)
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-25 01:32:24 +00:00
|
|
|
func (a *Attributes) ToArrowhead() d2target.Arrowhead {
|
2023-11-08 00:57:43 +00:00
|
|
|
var filled *bool
|
2023-04-25 01:32:24 +00:00
|
|
|
if a.Style.Filled != nil {
|
2023-11-08 00:57:43 +00:00
|
|
|
v, _ := strconv.ParseBool(a.Style.Filled.Value)
|
|
|
|
|
filled = go2.Pointer(v)
|
2023-04-25 01:32:24 +00:00
|
|
|
}
|
|
|
|
|
return d2target.ToArrowhead(a.Shape.Value, filled)
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
type Reference struct {
|
|
|
|
|
Key *d2ast.KeyPath `json:"key"`
|
|
|
|
|
KeyPathIndex int `json:"key_path_index"`
|
|
|
|
|
|
|
|
|
|
MapKey *d2ast.Key `json:"-"`
|
|
|
|
|
MapKeyEdgeIndex int `json:"map_key_edge_index"`
|
|
|
|
|
Scope *d2ast.Map `json:"-"`
|
2023-01-24 05:48:43 +00:00
|
|
|
ScopeObj *Object `json:"-"`
|
2023-06-20 03:06:26 +00:00
|
|
|
ScopeAST *d2ast.Map `json:"-"`
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r Reference) MapKeyEdgeDest() bool {
|
|
|
|
|
return r.Key == r.MapKey.Edges[r.MapKeyEdgeIndex].Dst
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r Reference) InEdge() bool {
|
|
|
|
|
return r.Key != r.MapKey.Key
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Style struct {
|
2023-03-31 08:58:02 +00:00
|
|
|
Opacity *Scalar `json:"opacity,omitempty"`
|
|
|
|
|
Stroke *Scalar `json:"stroke,omitempty"`
|
|
|
|
|
Fill *Scalar `json:"fill,omitempty"`
|
|
|
|
|
FillPattern *Scalar `json:"fillPattern,omitempty"`
|
|
|
|
|
StrokeWidth *Scalar `json:"strokeWidth,omitempty"`
|
|
|
|
|
StrokeDash *Scalar `json:"strokeDash,omitempty"`
|
|
|
|
|
BorderRadius *Scalar `json:"borderRadius,omitempty"`
|
|
|
|
|
Shadow *Scalar `json:"shadow,omitempty"`
|
|
|
|
|
ThreeDee *Scalar `json:"3d,omitempty"`
|
|
|
|
|
Multiple *Scalar `json:"multiple,omitempty"`
|
|
|
|
|
Font *Scalar `json:"font,omitempty"`
|
|
|
|
|
FontSize *Scalar `json:"fontSize,omitempty"`
|
|
|
|
|
FontColor *Scalar `json:"fontColor,omitempty"`
|
|
|
|
|
Animated *Scalar `json:"animated,omitempty"`
|
|
|
|
|
Bold *Scalar `json:"bold,omitempty"`
|
|
|
|
|
Italic *Scalar `json:"italic,omitempty"`
|
|
|
|
|
Underline *Scalar `json:"underline,omitempty"`
|
|
|
|
|
Filled *Scalar `json:"filled,omitempty"`
|
|
|
|
|
DoubleBorder *Scalar `json:"doubleBorder,omitempty"`
|
|
|
|
|
TextTransform *Scalar `json:"textTransform,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NoneTextTransform will return a boolean if the text should not have any
|
|
|
|
|
// transformation applied. This should overwrite theme specific transformations
|
|
|
|
|
// like `CapsLock` from the `terminal` theme.
|
|
|
|
|
func (s Style) NoneTextTransform() bool {
|
|
|
|
|
return s.TextTransform != nil && s.TextTransform.Value == "none"
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Style) Apply(key, value string) error {
|
|
|
|
|
switch key {
|
|
|
|
|
case "opacity":
|
|
|
|
|
if s.Opacity == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
f, err := strconv.ParseFloat(value, 64)
|
|
|
|
|
if err != nil || (f < 0 || f > 1) {
|
|
|
|
|
return errors.New(`expected "opacity" to be a number between 0.0 and 1.0`)
|
|
|
|
|
}
|
|
|
|
|
s.Opacity.Value = value
|
|
|
|
|
case "stroke":
|
|
|
|
|
if s.Stroke == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
2023-12-14 19:35:35 +00:00
|
|
|
if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
|
2022-11-03 13:54:49 +00:00
|
|
|
return errors.New(`expected "stroke" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
|
|
|
|
|
}
|
|
|
|
|
s.Stroke.Value = value
|
|
|
|
|
case "fill":
|
|
|
|
|
if s.Fill == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
2023-12-14 19:35:35 +00:00
|
|
|
if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
|
2022-11-03 13:54:49 +00:00
|
|
|
return errors.New(`expected "fill" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
|
|
|
|
|
}
|
|
|
|
|
s.Fill.Value = value
|
2023-03-14 03:07:13 +00:00
|
|
|
case "fill-pattern":
|
|
|
|
|
if s.FillPattern == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if !go2.Contains(FillPatterns, strings.ToLower(value)) {
|
2023-03-16 05:53:12 +00:00
|
|
|
return fmt.Errorf(`expected "fill-pattern" to be one of: %s`, strings.Join(FillPatterns, ", "))
|
2023-03-14 03:07:13 +00:00
|
|
|
}
|
|
|
|
|
s.FillPattern.Value = value
|
2022-11-03 13:54:49 +00:00
|
|
|
case "stroke-width":
|
|
|
|
|
if s.StrokeWidth == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
f, err := strconv.Atoi(value)
|
|
|
|
|
if err != nil || (f < 0 || f > 15) {
|
|
|
|
|
return errors.New(`expected "stroke-width" to be a number between 0 and 15`)
|
|
|
|
|
}
|
|
|
|
|
s.StrokeWidth.Value = value
|
|
|
|
|
case "stroke-dash":
|
|
|
|
|
if s.StrokeDash == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
f, err := strconv.Atoi(value)
|
|
|
|
|
if err != nil || (f < 0 || f > 10) {
|
|
|
|
|
return errors.New(`expected "stroke-dash" to be a number between 0 and 10`)
|
|
|
|
|
}
|
|
|
|
|
s.StrokeDash.Value = value
|
|
|
|
|
case "border-radius":
|
|
|
|
|
if s.BorderRadius == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
2023-04-13 14:20:31 +00:00
|
|
|
f, err := strconv.Atoi(value)
|
|
|
|
|
if err != nil || (f < 0) {
|
|
|
|
|
return errors.New(`expected "border-radius" to be a number greater or equal to 0`)
|
2023-03-09 16:10:12 +00:00
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
s.BorderRadius.Value = value
|
|
|
|
|
case "shadow":
|
|
|
|
|
if s.Shadow == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
_, err := strconv.ParseBool(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.New(`expected "shadow" to be true or false`)
|
|
|
|
|
}
|
|
|
|
|
s.Shadow.Value = value
|
|
|
|
|
case "3d":
|
|
|
|
|
if s.ThreeDee == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
_, err := strconv.ParseBool(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.New(`expected "3d" to be true or false`)
|
|
|
|
|
}
|
|
|
|
|
s.ThreeDee.Value = value
|
|
|
|
|
case "multiple":
|
|
|
|
|
if s.Multiple == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
_, err := strconv.ParseBool(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.New(`expected "multiple" to be true or false`)
|
|
|
|
|
}
|
|
|
|
|
s.Multiple.Value = value
|
|
|
|
|
case "font":
|
|
|
|
|
if s.Font == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
2023-03-07 06:21:23 +00:00
|
|
|
if _, ok := d2fonts.D2_FONT_TO_FAMILY[strings.ToLower(value)]; !ok {
|
2022-11-03 13:54:49 +00:00
|
|
|
return fmt.Errorf(`"%v" is not a valid font in our system`, value)
|
|
|
|
|
}
|
2023-03-07 06:21:23 +00:00
|
|
|
s.Font.Value = strings.ToLower(value)
|
2022-11-03 13:54:49 +00:00
|
|
|
case "font-size":
|
|
|
|
|
if s.FontSize == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
f, err := strconv.Atoi(value)
|
|
|
|
|
if err != nil || (f < 8 || f > 100) {
|
|
|
|
|
return errors.New(`expected "font-size" to be a number between 8 and 100`)
|
|
|
|
|
}
|
|
|
|
|
s.FontSize.Value = value
|
|
|
|
|
case "font-color":
|
|
|
|
|
if s.FontColor == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
2023-12-14 19:35:35 +00:00
|
|
|
if !go2.Contains(color.NamedColors, strings.ToLower(value)) && !color.ColorHexRegex.MatchString(value) {
|
2022-11-03 13:54:49 +00:00
|
|
|
return errors.New(`expected "font-color" to be a valid named color ("orange") or a hex code ("#f0ff3a")`)
|
|
|
|
|
}
|
|
|
|
|
s.FontColor.Value = value
|
|
|
|
|
case "animated":
|
|
|
|
|
if s.Animated == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
_, err := strconv.ParseBool(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.New(`expected "animated" to be true or false`)
|
|
|
|
|
}
|
|
|
|
|
s.Animated.Value = value
|
|
|
|
|
case "bold":
|
|
|
|
|
if s.Bold == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
_, err := strconv.ParseBool(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.New(`expected "bold" to be true or false`)
|
|
|
|
|
}
|
|
|
|
|
s.Bold.Value = value
|
|
|
|
|
case "italic":
|
|
|
|
|
if s.Italic == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
_, err := strconv.ParseBool(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.New(`expected "italic" to be true or false`)
|
|
|
|
|
}
|
|
|
|
|
s.Italic.Value = value
|
|
|
|
|
case "underline":
|
|
|
|
|
if s.Underline == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
_, err := strconv.ParseBool(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.New(`expected "underline" to be true or false`)
|
|
|
|
|
}
|
|
|
|
|
s.Underline.Value = value
|
|
|
|
|
case "filled":
|
|
|
|
|
if s.Filled == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
_, err := strconv.ParseBool(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.New(`expected "filled" to be true or false`)
|
|
|
|
|
}
|
|
|
|
|
s.Filled.Value = value
|
2022-12-31 07:26:38 +00:00
|
|
|
case "double-border":
|
|
|
|
|
if s.DoubleBorder == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
_, err := strconv.ParseBool(value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.New(`expected "double-border" to be true or false`)
|
|
|
|
|
}
|
|
|
|
|
s.DoubleBorder.Value = value
|
2023-03-31 08:58:02 +00:00
|
|
|
case "text-transform":
|
|
|
|
|
if s.TextTransform == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if !go2.Contains(textTransforms, strings.ToLower(value)) {
|
2023-04-08 18:35:54 +00:00
|
|
|
return fmt.Errorf(`expected "text-transform" to be one of (%s)`, strings.Join(textTransforms, ", "))
|
2023-03-31 08:58:02 +00:00
|
|
|
}
|
|
|
|
|
s.TextTransform.Value = value
|
2022-11-03 13:54:49 +00:00
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("unknown style key: %s", key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ContainerLevel int
|
|
|
|
|
|
|
|
|
|
func (l ContainerLevel) LabelSize() int {
|
|
|
|
|
// Largest to smallest
|
|
|
|
|
if l == 1 {
|
|
|
|
|
return d2fonts.FONT_SIZE_XXL
|
|
|
|
|
} else if l == 2 {
|
|
|
|
|
return d2fonts.FONT_SIZE_XL
|
|
|
|
|
} else if l == 3 {
|
|
|
|
|
return d2fonts.FONT_SIZE_L
|
|
|
|
|
}
|
|
|
|
|
return d2fonts.FONT_SIZE_M
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-09 18:16:28 +00:00
|
|
|
func (obj *Object) GetFill() string {
|
2022-12-05 05:57:59 +00:00
|
|
|
level := int(obj.Level())
|
2023-04-14 03:04:55 +00:00
|
|
|
shape := obj.Shape.Value
|
2023-02-25 04:17:09 +00:00
|
|
|
|
|
|
|
|
if strings.EqualFold(shape, d2target.ShapeSQLTable) || strings.EqualFold(shape, d2target.ShapeClass) {
|
|
|
|
|
return color.N1
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-05 20:09:32 +00:00
|
|
|
if obj.IsSequenceDiagramNote() {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.N7
|
2022-12-05 05:57:59 +00:00
|
|
|
} else if obj.IsSequenceDiagramGroup() {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.N5
|
2022-12-05 20:09:32 +00:00
|
|
|
} else if obj.Parent.IsSequenceDiagram() {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B5
|
2022-12-05 03:57:52 +00:00
|
|
|
}
|
|
|
|
|
|
2022-12-05 22:09:27 +00:00
|
|
|
// fill for spans
|
|
|
|
|
sd := obj.OuterSequenceDiagram()
|
|
|
|
|
if sd != nil {
|
|
|
|
|
level -= int(sd.Level())
|
|
|
|
|
if level == 1 {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B3
|
2022-12-05 22:09:27 +00:00
|
|
|
} else if level == 2 {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B4
|
2022-12-05 22:09:27 +00:00
|
|
|
} else if level == 3 {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B5
|
2022-12-05 22:27:37 +00:00
|
|
|
} else if level == 4 {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.N6
|
2022-12-05 22:09:27 +00:00
|
|
|
}
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.N7
|
2022-12-05 22:09:27 +00:00
|
|
|
}
|
|
|
|
|
|
2022-12-05 22:30:40 +00:00
|
|
|
if obj.IsSequenceDiagram() {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.N7
|
2022-12-05 21:53:09 +00:00
|
|
|
}
|
|
|
|
|
|
2024-03-15 21:55:18 +00:00
|
|
|
if shape == "" || strings.EqualFold(shape, d2target.ShapeSquare) || strings.EqualFold(shape, d2target.ShapeCircle) || strings.EqualFold(shape, d2target.ShapeOval) || strings.EqualFold(shape, d2target.ShapeRectangle) || strings.EqualFold(shape, d2target.ShapeHierarchy) {
|
2022-11-03 13:54:49 +00:00
|
|
|
if level == 1 {
|
|
|
|
|
if !obj.IsContainer() {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B6
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B4
|
2022-11-03 13:54:49 +00:00
|
|
|
} else if level == 2 {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B5
|
2022-11-03 13:54:49 +00:00
|
|
|
} else if level == 3 {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B6
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.N7
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.EqualFold(shape, d2target.ShapeCylinder) || strings.EqualFold(shape, d2target.ShapeStoredData) || strings.EqualFold(shape, d2target.ShapePackage) {
|
|
|
|
|
if level == 1 {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.AA4
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.AA5
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.EqualFold(shape, d2target.ShapeStep) || strings.EqualFold(shape, d2target.ShapePage) || strings.EqualFold(shape, d2target.ShapeDocument) {
|
|
|
|
|
if level == 1 {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.AB4
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.AB5
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.EqualFold(shape, d2target.ShapePerson) {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B3
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
if strings.EqualFold(shape, d2target.ShapeDiamond) {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.N4
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
if strings.EqualFold(shape, d2target.ShapeCloud) || strings.EqualFold(shape, d2target.ShapeCallout) {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.N7
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
if strings.EqualFold(shape, d2target.ShapeQueue) || strings.EqualFold(shape, d2target.ShapeParallelogram) || strings.EqualFold(shape, d2target.ShapeHexagon) {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.N5
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.N7
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-09 18:16:28 +00:00
|
|
|
func (obj *Object) GetStroke(dashGapSize interface{}) string {
|
2023-04-14 03:04:55 +00:00
|
|
|
shape := obj.Shape.Value
|
2022-12-18 05:22:33 +00:00
|
|
|
if strings.EqualFold(shape, d2target.ShapeCode) ||
|
|
|
|
|
strings.EqualFold(shape, d2target.ShapeText) {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.N1
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2022-12-24 23:56:22 +00:00
|
|
|
if strings.EqualFold(shape, d2target.ShapeClass) ||
|
|
|
|
|
strings.EqualFold(shape, d2target.ShapeSQLTable) {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.N7
|
2022-12-24 23:56:22 +00:00
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
if dashGapSize != 0.0 {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B2
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B1
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (obj *Object) Level() ContainerLevel {
|
|
|
|
|
if obj.Parent == nil {
|
2023-09-05 22:01:22 +00:00
|
|
|
return ContainerLevel(obj.Graph.RootLevel)
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
return 1 + obj.Parent.Level()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (obj *Object) IsContainer() bool {
|
|
|
|
|
return len(obj.Children) > 0
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-02 22:52:30 +00:00
|
|
|
func (obj *Object) HasOutsideBottomLabel() bool {
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj == nil {
|
2023-03-02 22:52:30 +00:00
|
|
|
return false
|
|
|
|
|
}
|
2023-04-14 03:04:55 +00:00
|
|
|
switch obj.Shape.Value {
|
2023-03-02 22:52:30 +00:00
|
|
|
case d2target.ShapeImage, d2target.ShapePerson:
|
|
|
|
|
return true
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-13 22:20:12 +00:00
|
|
|
func (obj *Object) HasLabel() bool {
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj == nil {
|
2023-04-13 22:20:12 +00:00
|
|
|
return false
|
|
|
|
|
}
|
2023-04-14 03:04:55 +00:00
|
|
|
switch obj.Shape.Value {
|
2023-04-13 22:20:12 +00:00
|
|
|
case d2target.ShapeText, d2target.ShapeClass, d2target.ShapeSQLTable, d2target.ShapeCode:
|
|
|
|
|
return false
|
|
|
|
|
default:
|
2023-04-14 03:04:55 +00:00
|
|
|
return obj.Label.Value != ""
|
2023-04-13 22:20:12 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-13 00:01:48 +00:00
|
|
|
func (obj *Object) HasIcon() bool {
|
|
|
|
|
return obj.Icon != nil && obj.Shape.Value != d2target.ShapeImage
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
func (obj *Object) AbsID() string {
|
|
|
|
|
if obj.Parent != nil && obj.Parent.ID != "" {
|
|
|
|
|
return obj.Parent.AbsID() + "." + obj.ID
|
|
|
|
|
}
|
|
|
|
|
return obj.ID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (obj *Object) AbsIDArray() []string {
|
|
|
|
|
if obj.Parent == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return append(obj.Parent.AbsIDArray(), obj.ID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (obj *Object) Text() *d2target.MText {
|
2023-04-14 03:04:55 +00:00
|
|
|
isBold := !obj.IsContainer() && obj.Shape.Value != "text"
|
2022-12-26 05:49:26 +00:00
|
|
|
isItalic := false
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Style.Bold != nil && obj.Style.Bold.Value == "true" {
|
2022-12-26 05:49:26 +00:00
|
|
|
isBold = true
|
|
|
|
|
}
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Style.Italic != nil && obj.Style.Italic.Value == "true" {
|
2022-12-26 05:49:26 +00:00
|
|
|
isItalic = true
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
fontSize := d2fonts.FONT_SIZE_M
|
2023-02-06 21:12:43 +00:00
|
|
|
|
|
|
|
|
if obj.Class != nil || obj.SQLTable != nil {
|
2023-02-06 21:14:31 +00:00
|
|
|
fontSize = d2fonts.FONT_SIZE_L
|
2023-02-06 21:12:43 +00:00
|
|
|
}
|
|
|
|
|
|
2022-12-05 18:46:06 +00:00
|
|
|
if obj.OuterSequenceDiagram() == nil {
|
2023-06-15 21:33:37 +00:00
|
|
|
// Note: during grid layout when children are temporarily removed `IsContainer` is false
|
|
|
|
|
if (obj.IsContainer() || obj.IsGridDiagram()) && obj.Shape.Value != "text" {
|
2022-12-05 18:46:06 +00:00
|
|
|
fontSize = obj.Level().LabelSize()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
isBold = false
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Style.FontSize != nil {
|
|
|
|
|
fontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
|
2022-11-29 19:27:41 +00:00
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
// Class and Table objects have Label set to header
|
|
|
|
|
if obj.Class != nil || obj.SQLTable != nil {
|
2023-02-06 08:42:28 +00:00
|
|
|
fontSize += d2target.HeaderFontAdd
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2022-12-25 00:19:09 +00:00
|
|
|
if obj.Class != nil {
|
|
|
|
|
isBold = false
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
return &d2target.MText{
|
2023-04-14 03:04:55 +00:00
|
|
|
Text: obj.Label.Value,
|
2022-11-03 13:54:49 +00:00
|
|
|
FontSize: fontSize,
|
2022-12-05 18:46:06 +00:00
|
|
|
IsBold: isBold,
|
2022-12-26 05:49:26 +00:00
|
|
|
IsItalic: isItalic,
|
2023-04-14 03:04:55 +00:00
|
|
|
Language: obj.Language,
|
|
|
|
|
Shape: obj.Shape.Value,
|
2022-11-03 13:54:49 +00:00
|
|
|
|
|
|
|
|
Dimensions: obj.LabelDimensions,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (obj *Object) newObject(id string) *Object {
|
|
|
|
|
idval := id
|
|
|
|
|
k, _ := d2parser.ParseKey(id)
|
|
|
|
|
if k != nil && len(k.Path) > 0 {
|
|
|
|
|
idval = k.Path[0].Unbox().ScalarString()
|
|
|
|
|
}
|
|
|
|
|
child := &Object{
|
|
|
|
|
ID: id,
|
|
|
|
|
IDVal: idval,
|
2023-04-14 03:04:55 +00:00
|
|
|
Attributes: Attributes{
|
2022-11-03 13:54:49 +00:00
|
|
|
Label: Scalar{
|
|
|
|
|
Value: idval,
|
|
|
|
|
},
|
2023-01-24 11:09:40 +00:00
|
|
|
Shape: Scalar{
|
|
|
|
|
Value: d2target.ShapeRectangle,
|
|
|
|
|
},
|
2022-11-03 13:54:49 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
Graph: obj.Graph,
|
|
|
|
|
Parent: obj,
|
|
|
|
|
|
|
|
|
|
Children: make(map[string]*Object),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj.Children[strings.ToLower(id)] = child
|
|
|
|
|
obj.ChildrenArray = append(obj.ChildrenArray, child)
|
|
|
|
|
|
|
|
|
|
if obj.Graph != nil {
|
|
|
|
|
obj.Graph.Objects = append(obj.Graph.Objects, child)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return child
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (obj *Object) HasChild(ids []string) (*Object, bool) {
|
2023-01-24 05:48:43 +00:00
|
|
|
if len(ids) == 0 {
|
|
|
|
|
return obj, true
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
if len(ids) == 1 && ids[0] != "style" {
|
|
|
|
|
_, ok := ReservedKeywords[ids[0]]
|
|
|
|
|
if ok {
|
|
|
|
|
return obj, true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
id := ids[0]
|
|
|
|
|
ids = ids[1:]
|
|
|
|
|
|
|
|
|
|
child, ok := obj.Children[strings.ToLower(id)]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(ids) >= 1 {
|
|
|
|
|
return child.HasChild(ids)
|
|
|
|
|
}
|
|
|
|
|
return child, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (obj *Object) HasEdge(mk *d2ast.Key) (*Edge, bool) {
|
|
|
|
|
ea, ok := obj.FindEdges(mk)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
for _, e := range ea {
|
|
|
|
|
if e.Index == *mk.EdgeIndex.Int {
|
|
|
|
|
return e, true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-22 08:21:21 +00:00
|
|
|
// TODO: remove once not used anywhere
|
2022-11-03 13:54:49 +00:00
|
|
|
func ResolveUnderscoreKey(ida []string, obj *Object) (resolvedObj *Object, resolvedIDA []string, _ error) {
|
2022-12-06 04:45:59 +00:00
|
|
|
if len(ida) > 0 && !obj.IsSequenceDiagram() {
|
2022-12-06 03:50:08 +00:00
|
|
|
objSD := obj.OuterSequenceDiagram()
|
|
|
|
|
if objSD != nil {
|
|
|
|
|
referencesActor := false
|
|
|
|
|
for _, c := range objSD.ChildrenArray {
|
|
|
|
|
if c.ID == ida[0] {
|
|
|
|
|
referencesActor = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if referencesActor {
|
|
|
|
|
obj = objSD
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
resolvedObj = obj
|
|
|
|
|
resolvedIDA = ida
|
|
|
|
|
|
|
|
|
|
for i, id := range ida {
|
|
|
|
|
if id != "_" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if i != 0 && ida[i-1] != "_" {
|
|
|
|
|
return nil, nil, errors.New(`parent "_" can only be used in the beginning of paths, e.g. "_.x"`)
|
|
|
|
|
}
|
|
|
|
|
if resolvedObj == obj.Graph.Root {
|
|
|
|
|
return nil, nil, errors.New(`parent "_" cannot be used in the root scope`)
|
|
|
|
|
}
|
|
|
|
|
if i == len(ida)-1 {
|
|
|
|
|
return nil, nil, errors.New(`invalid use of parent "_"`)
|
|
|
|
|
}
|
|
|
|
|
resolvedObj = resolvedObj.Parent
|
|
|
|
|
resolvedIDA = resolvedIDA[1:]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return resolvedObj, resolvedIDA, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: remove edges []edge and scope each edge inside Object.
|
|
|
|
|
func (obj *Object) FindEdges(mk *d2ast.Key) ([]*Edge, bool) {
|
|
|
|
|
if len(mk.Edges) != 1 {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
if mk.EdgeIndex.Int == nil {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
ae := mk.Edges[0]
|
|
|
|
|
|
|
|
|
|
srcObj, srcID, err := ResolveUnderscoreKey(Key(ae.Src), obj)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
dstObj, dstID, err := ResolveUnderscoreKey(Key(ae.Dst), obj)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
src := strings.Join(srcID, ".")
|
|
|
|
|
dst := strings.Join(dstID, ".")
|
|
|
|
|
if srcObj.Parent != nil {
|
|
|
|
|
src = srcObj.AbsID() + "." + src
|
|
|
|
|
}
|
|
|
|
|
if dstObj.Parent != nil {
|
|
|
|
|
dst = dstObj.AbsID() + "." + dst
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var ea []*Edge
|
|
|
|
|
for _, e := range obj.Graph.Edges {
|
|
|
|
|
if strings.EqualFold(src, e.Src.AbsID()) &&
|
|
|
|
|
((ae.SrcArrow == "<" && e.SrcArrow) || (ae.SrcArrow == "" && !e.SrcArrow)) &&
|
|
|
|
|
strings.EqualFold(dst, e.Dst.AbsID()) &&
|
|
|
|
|
((ae.DstArrow == ">" && e.DstArrow) || (ae.DstArrow == "" && !e.DstArrow)) {
|
|
|
|
|
ea = append(ea, e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ea, true
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-02 18:30:54 +00:00
|
|
|
func (obj *Object) ensureChildEdge(ida []string) *Object {
|
|
|
|
|
for i := range ida {
|
2023-04-14 03:04:55 +00:00
|
|
|
switch obj.Shape.Value {
|
2023-01-24 05:48:43 +00:00
|
|
|
case d2target.ShapeClass, d2target.ShapeSQLTable:
|
|
|
|
|
// This will only be called for connecting edges where we want to truncate to the
|
|
|
|
|
// container.
|
|
|
|
|
return obj
|
2023-02-02 18:30:54 +00:00
|
|
|
default:
|
|
|
|
|
obj = obj.EnsureChild(ida[i : i+1])
|
2023-01-24 05:48:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return obj
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
// EnsureChild grabs the child by ids or creates it if it does not exist including all
|
|
|
|
|
// intermediate nodes.
|
2023-01-24 11:09:40 +00:00
|
|
|
func (obj *Object) EnsureChild(ida []string) *Object {
|
|
|
|
|
seq := obj.OuterSequenceDiagram()
|
|
|
|
|
if seq != nil {
|
|
|
|
|
for _, c := range seq.ChildrenArray {
|
|
|
|
|
if c.ID == ida[0] {
|
2023-02-02 18:30:54 +00:00
|
|
|
if obj.ID == ida[0] {
|
|
|
|
|
// In cases of a.a where EnsureChild is called on the parent a, the second a should
|
|
|
|
|
// be created as a child of a and not as a child of the diagram. This is super
|
|
|
|
|
// unfortunate code but alas.
|
|
|
|
|
break
|
|
|
|
|
}
|
2023-01-24 11:09:40 +00:00
|
|
|
obj = seq
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-02-02 18:30:54 +00:00
|
|
|
|
|
|
|
|
if len(ida) == 0 {
|
|
|
|
|
return obj
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-24 11:09:40 +00:00
|
|
|
_, is := ReservedKeywordHolders[ida[0]]
|
|
|
|
|
if len(ida) == 1 && !is {
|
|
|
|
|
_, ok := ReservedKeywords[ida[0]]
|
2022-11-03 13:54:49 +00:00
|
|
|
if ok {
|
|
|
|
|
return obj
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-24 11:09:40 +00:00
|
|
|
id := ida[0]
|
|
|
|
|
ida = ida[1:]
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2023-02-02 18:30:54 +00:00
|
|
|
if id == "_" {
|
|
|
|
|
return obj.Parent.EnsureChild(ida)
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
|
|
|
|
|
child, ok := obj.Children[strings.ToLower(id)]
|
|
|
|
|
if !ok {
|
|
|
|
|
child = obj.newObject(id)
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-24 11:09:40 +00:00
|
|
|
if len(ida) >= 1 {
|
|
|
|
|
return child.EnsureChild(ida)
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
return child
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (obj *Object) AppendReferences(ida []string, ref Reference, unresolvedObj *Object) {
|
2023-01-24 05:48:43 +00:00
|
|
|
ref.ScopeObj = unresolvedObj
|
2022-11-03 13:54:49 +00:00
|
|
|
numUnderscores := 0
|
|
|
|
|
for i := range ida {
|
|
|
|
|
if ida[i] == "_" {
|
|
|
|
|
numUnderscores++
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
child, ok := obj.HasChild(ida[numUnderscores : i+1])
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ref.KeyPathIndex = i
|
|
|
|
|
child.References = append(child.References, ref)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-29 02:14:19 +00:00
|
|
|
func (obj *Object) GetLabelSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) (*d2target.TextDimensions, error) {
|
2023-04-14 03:04:55 +00:00
|
|
|
shapeType := strings.ToLower(obj.Shape.Value)
|
2022-12-29 02:14:19 +00:00
|
|
|
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Style.Font != nil {
|
|
|
|
|
f := d2fonts.D2_FONT_TO_FAMILY[obj.Style.Font.Value]
|
2023-03-07 06:21:23 +00:00
|
|
|
fontFamily = &f
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-29 02:14:19 +00:00
|
|
|
var dims *d2target.TextDimensions
|
|
|
|
|
switch shapeType {
|
|
|
|
|
case d2target.ShapeText:
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Language == "latex" {
|
2022-12-29 02:14:19 +00:00
|
|
|
width, height, err := d2latex.Measure(obj.Text().Text)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
dims = d2target.NewTextDimensions(width, height)
|
2023-04-14 03:04:55 +00:00
|
|
|
} else if obj.Language != "" {
|
2022-12-29 02:14:19 +00:00
|
|
|
var err error
|
|
|
|
|
dims, err = getMarkdownDimensions(mtexts, ruler, obj.Text(), fontFamily)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case d2target.ShapeClass:
|
|
|
|
|
dims = GetTextDimensions(mtexts, ruler, obj.Text(), go2.Pointer(d2fonts.SourceCodePro))
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
dims = GetTextDimensions(mtexts, ruler, obj.Text(), fontFamily)
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-14 03:04:55 +00:00
|
|
|
if shapeType == d2target.ShapeSQLTable && obj.Label.Value == "" {
|
2022-12-29 02:14:19 +00:00
|
|
|
// measure with placeholder text to determine height
|
|
|
|
|
placeholder := *obj.Text()
|
|
|
|
|
placeholder.Text = "Table"
|
|
|
|
|
dims = GetTextDimensions(mtexts, ruler, &placeholder, fontFamily)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if dims == nil {
|
2023-01-24 20:09:59 +00:00
|
|
|
if obj.Text().Text == "" {
|
|
|
|
|
return d2target.NewTextDimensions(0, 0), nil
|
|
|
|
|
}
|
2022-12-29 02:14:19 +00:00
|
|
|
if shapeType == d2target.ShapeImage {
|
|
|
|
|
dims = d2target.NewTextDimensions(0, 0)
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("dimensions for object label %#v not found", obj.Text())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dims, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-03 23:34:41 +00:00
|
|
|
func (obj *Object) GetDefaultSize(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily, labelDims d2target.TextDimensions, withLabelPadding bool) (*d2target.TextDimensions, error) {
|
2022-12-29 02:14:19 +00:00
|
|
|
dims := d2target.TextDimensions{}
|
2023-06-19 21:17:34 +00:00
|
|
|
dslShape := strings.ToLower(obj.Shape.Value)
|
2022-12-29 02:14:19 +00:00
|
|
|
|
2023-06-19 21:17:34 +00:00
|
|
|
if dslShape == d2target.ShapeCode {
|
|
|
|
|
fontSize := obj.Text().FontSize
|
|
|
|
|
// 0.5em padding on each side
|
|
|
|
|
labelDims.Width += fontSize
|
|
|
|
|
labelDims.Height += fontSize
|
|
|
|
|
} else if withLabelPadding {
|
2023-02-03 22:53:17 +00:00
|
|
|
labelDims.Width += INNER_LABEL_PADDING
|
|
|
|
|
labelDims.Height += INNER_LABEL_PADDING
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-19 21:17:34 +00:00
|
|
|
switch dslShape {
|
2022-12-29 02:14:19 +00:00
|
|
|
default:
|
|
|
|
|
return d2target.NewTextDimensions(labelDims.Width, labelDims.Height), nil
|
2023-04-18 18:29:34 +00:00
|
|
|
case d2target.ShapeText:
|
|
|
|
|
w := labelDims.Width
|
|
|
|
|
if w < MIN_SHAPE_SIZE {
|
|
|
|
|
w = MIN_SHAPE_SIZE
|
|
|
|
|
}
|
|
|
|
|
h := labelDims.Height
|
|
|
|
|
if h < MIN_SHAPE_SIZE {
|
|
|
|
|
h = MIN_SHAPE_SIZE
|
|
|
|
|
}
|
|
|
|
|
return d2target.NewTextDimensions(w, h), nil
|
|
|
|
|
|
2022-12-29 02:14:19 +00:00
|
|
|
case d2target.ShapeImage:
|
|
|
|
|
return d2target.NewTextDimensions(128, 128), nil
|
|
|
|
|
|
|
|
|
|
case d2target.ShapeClass:
|
2023-01-24 20:09:59 +00:00
|
|
|
maxWidth := go2.Max(12, labelDims.Width)
|
2022-12-29 02:14:19 +00:00
|
|
|
|
2023-02-06 08:47:13 +00:00
|
|
|
fontSize := d2fonts.FONT_SIZE_L
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Style.FontSize != nil {
|
|
|
|
|
fontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
|
2023-02-06 08:47:13 +00:00
|
|
|
}
|
|
|
|
|
|
2022-12-29 02:14:19 +00:00
|
|
|
for _, f := range obj.Class.Fields {
|
2023-02-06 08:47:13 +00:00
|
|
|
fdims := GetTextDimensions(mtexts, ruler, f.Text(fontSize), go2.Pointer(d2fonts.SourceCodePro))
|
2022-12-29 02:14:19 +00:00
|
|
|
if fdims == nil {
|
2023-02-06 08:47:13 +00:00
|
|
|
return nil, fmt.Errorf("dimensions for class field %#v not found", f.Text(fontSize))
|
2022-12-29 02:14:19 +00:00
|
|
|
}
|
2023-02-01 00:19:22 +00:00
|
|
|
maxWidth = go2.Max(maxWidth, fdims.Width)
|
2022-12-29 02:14:19 +00:00
|
|
|
}
|
|
|
|
|
for _, m := range obj.Class.Methods {
|
2023-02-06 08:47:13 +00:00
|
|
|
mdims := GetTextDimensions(mtexts, ruler, m.Text(fontSize), go2.Pointer(d2fonts.SourceCodePro))
|
2022-12-29 02:14:19 +00:00
|
|
|
if mdims == nil {
|
2023-02-06 08:47:13 +00:00
|
|
|
return nil, fmt.Errorf("dimensions for class method %#v not found", m.Text(fontSize))
|
2022-12-29 02:14:19 +00:00
|
|
|
}
|
2023-02-01 00:19:22 +00:00
|
|
|
maxWidth = go2.Max(maxWidth, mdims.Width)
|
2022-12-29 02:14:19 +00:00
|
|
|
}
|
2023-01-27 18:52:46 +00:00
|
|
|
// ┌─PrefixWidth ┌─CenterPadding
|
|
|
|
|
// ┌─┬─┬───────┬──────┬───┬──┐
|
|
|
|
|
// │ + getJobs() Job[] │
|
|
|
|
|
// └─┴─┴───────┴──────┴───┴──┘
|
|
|
|
|
// └─PrefixPadding └──TypePadding
|
|
|
|
|
// ├───────┤ + ├───┤ = maxWidth
|
2023-01-24 01:19:38 +00:00
|
|
|
dims.Width = d2target.PrefixPadding + d2target.PrefixWidth + maxWidth + d2target.CenterPadding + d2target.TypePadding
|
2022-12-29 02:14:19 +00:00
|
|
|
|
|
|
|
|
// All rows should be the same height
|
|
|
|
|
var anyRowText *d2target.MText
|
|
|
|
|
if len(obj.Class.Fields) > 0 {
|
2023-02-06 08:47:13 +00:00
|
|
|
anyRowText = obj.Class.Fields[0].Text(fontSize)
|
2022-12-29 02:14:19 +00:00
|
|
|
} else if len(obj.Class.Methods) > 0 {
|
2023-02-06 08:47:13 +00:00
|
|
|
anyRowText = obj.Class.Methods[0].Text(fontSize)
|
2022-12-29 02:14:19 +00:00
|
|
|
}
|
|
|
|
|
if anyRowText != nil {
|
2023-01-24 01:19:38 +00:00
|
|
|
rowHeight := GetTextDimensions(mtexts, ruler, anyRowText, go2.Pointer(d2fonts.SourceCodePro)).Height + d2target.VerticalPadding
|
2022-12-29 02:14:19 +00:00
|
|
|
dims.Height = rowHeight * (len(obj.Class.Fields) + len(obj.Class.Methods) + 2)
|
|
|
|
|
} else {
|
2023-02-04 02:27:38 +00:00
|
|
|
dims.Height = 2*go2.Max(12, labelDims.Height) + d2target.VerticalPadding
|
2022-12-29 02:14:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case d2target.ShapeSQLTable:
|
|
|
|
|
maxNameWidth := 0
|
|
|
|
|
maxTypeWidth := 0
|
2023-06-05 18:33:34 +00:00
|
|
|
maxConstraintWidth := 0
|
2022-12-29 02:14:19 +00:00
|
|
|
|
2023-02-06 08:42:28 +00:00
|
|
|
colFontSize := d2fonts.FONT_SIZE_L
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Style.FontSize != nil {
|
|
|
|
|
colFontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
|
2023-02-06 08:42:28 +00:00
|
|
|
}
|
|
|
|
|
|
2022-12-29 02:14:19 +00:00
|
|
|
for i := range obj.SQLTable.Columns {
|
|
|
|
|
// Note: we want to set dimensions of actual column not the for loop copy of the struct
|
|
|
|
|
c := &obj.SQLTable.Columns[i]
|
2023-02-06 08:42:28 +00:00
|
|
|
|
|
|
|
|
ctexts := c.Texts(colFontSize)
|
2022-12-29 02:14:19 +00:00
|
|
|
|
|
|
|
|
nameDims := GetTextDimensions(mtexts, ruler, ctexts[0], fontFamily)
|
|
|
|
|
if nameDims == nil {
|
|
|
|
|
return nil, fmt.Errorf("dimensions for sql_table name %#v not found", ctexts[0].Text)
|
|
|
|
|
}
|
|
|
|
|
c.Name.LabelWidth = nameDims.Width
|
|
|
|
|
c.Name.LabelHeight = nameDims.Height
|
2023-02-01 00:19:22 +00:00
|
|
|
maxNameWidth = go2.Max(maxNameWidth, nameDims.Width)
|
2022-12-29 02:14:19 +00:00
|
|
|
|
|
|
|
|
typeDims := GetTextDimensions(mtexts, ruler, ctexts[1], fontFamily)
|
|
|
|
|
if typeDims == nil {
|
|
|
|
|
return nil, fmt.Errorf("dimensions for sql_table type %#v not found", ctexts[1].Text)
|
|
|
|
|
}
|
|
|
|
|
c.Type.LabelWidth = typeDims.Width
|
|
|
|
|
c.Type.LabelHeight = typeDims.Height
|
2023-02-01 00:19:22 +00:00
|
|
|
maxTypeWidth = go2.Max(maxTypeWidth, typeDims.Width)
|
2022-12-29 02:14:19 +00:00
|
|
|
|
2023-05-08 20:57:27 +00:00
|
|
|
if l := len(c.Constraint); l > 0 {
|
2023-06-05 18:33:34 +00:00
|
|
|
constraintDims := GetTextDimensions(mtexts, ruler, ctexts[2], fontFamily)
|
2023-06-05 18:46:38 +00:00
|
|
|
if constraintDims == nil {
|
2023-06-05 18:33:34 +00:00
|
|
|
return nil, fmt.Errorf("dimensions for sql_table constraint %#v not found", ctexts[2].Text)
|
2023-05-08 20:57:27 +00:00
|
|
|
}
|
2023-06-05 18:33:34 +00:00
|
|
|
maxConstraintWidth = go2.Max(maxConstraintWidth, constraintDims.Width)
|
2022-12-29 02:14:19 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The rows get padded a little due to header font being larger than row font
|
2023-01-24 20:09:59 +00:00
|
|
|
dims.Height = go2.Max(12, labelDims.Height*(len(obj.SQLTable.Columns)+1))
|
2022-12-29 07:15:16 +00:00
|
|
|
headerWidth := d2target.HeaderPadding + labelDims.Width + d2target.HeaderPadding
|
2023-06-05 18:33:34 +00:00
|
|
|
rowsWidth := d2target.NamePadding + maxNameWidth + d2target.TypePadding + maxTypeWidth + d2target.TypePadding + maxConstraintWidth
|
|
|
|
|
if maxConstraintWidth != 0 {
|
|
|
|
|
rowsWidth += d2target.ConstraintPadding
|
|
|
|
|
}
|
2023-01-24 20:09:59 +00:00
|
|
|
dims.Width = go2.Max(12, go2.Max(headerWidth, rowsWidth))
|
2022-12-29 02:14:19 +00:00
|
|
|
}
|
2022-12-29 02:24:48 +00:00
|
|
|
|
2022-12-29 02:14:19 +00:00
|
|
|
return &dims, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-05 03:58:39 +00:00
|
|
|
// resizes the object to fit content of the given width and height in its inner box with the given padding.
|
|
|
|
|
// this accounts for the shape of the object, and if there is a desired width or height set for the object
|
|
|
|
|
func (obj *Object) SizeToContent(contentWidth, contentHeight, paddingX, paddingY float64) {
|
2023-05-04 02:34:57 +00:00
|
|
|
dslShape := strings.ToLower(obj.Shape.Value)
|
|
|
|
|
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape]
|
2023-05-05 03:58:39 +00:00
|
|
|
s := shape.NewShape(shapeType, geo.NewBox(geo.NewPoint(0, 0), contentWidth, contentHeight))
|
2023-05-04 02:34:57 +00:00
|
|
|
|
|
|
|
|
var fitWidth, fitHeight float64
|
|
|
|
|
if shapeType == shape.PERSON_TYPE {
|
2023-05-05 03:58:39 +00:00
|
|
|
fitWidth = contentWidth + paddingX
|
|
|
|
|
fitHeight = contentHeight + paddingY
|
2023-05-04 02:34:57 +00:00
|
|
|
} else {
|
2023-05-05 03:58:39 +00:00
|
|
|
fitWidth, fitHeight = s.GetDimensionsToFit(contentWidth, contentHeight, paddingX, paddingY)
|
2023-05-04 02:34:57 +00:00
|
|
|
}
|
2024-04-10 15:50:51 +00:00
|
|
|
|
|
|
|
|
var desiredWidth int
|
|
|
|
|
if obj.WidthAttr != nil {
|
|
|
|
|
desiredWidth, _ = strconv.Atoi(obj.WidthAttr.Value)
|
|
|
|
|
obj.Width = float64(desiredWidth)
|
|
|
|
|
} else {
|
|
|
|
|
obj.Width = fitWidth
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var desiredHeight int
|
|
|
|
|
if obj.HeightAttr != nil {
|
|
|
|
|
desiredHeight, _ = strconv.Atoi(obj.HeightAttr.Value)
|
|
|
|
|
obj.Height = float64(desiredHeight)
|
|
|
|
|
} else {
|
|
|
|
|
obj.Height = fitHeight
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if obj.SQLTable != nil || obj.Class != nil || obj.Language != "" {
|
|
|
|
|
obj.Width = math.Max(float64(desiredWidth), fitWidth)
|
|
|
|
|
obj.Height = math.Max(float64(desiredHeight), fitHeight)
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-04 02:34:57 +00:00
|
|
|
if s.AspectRatio1() {
|
|
|
|
|
sideLength := math.Max(obj.Width, obj.Height)
|
|
|
|
|
obj.Width = sideLength
|
|
|
|
|
obj.Height = sideLength
|
|
|
|
|
} else if desiredHeight == 0 || desiredWidth == 0 {
|
2023-11-17 02:08:33 +00:00
|
|
|
switch shapeType {
|
2023-05-04 02:34:57 +00:00
|
|
|
case shape.PERSON_TYPE:
|
|
|
|
|
obj.Width, obj.Height = shape.LimitAR(obj.Width, obj.Height, shape.PERSON_AR_LIMIT)
|
|
|
|
|
case shape.OVAL_TYPE:
|
|
|
|
|
obj.Width, obj.Height = shape.LimitAR(obj.Width, obj.Height, shape.OVAL_AR_LIMIT)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-17 02:08:33 +00:00
|
|
|
if shapeType == shape.CLOUD_TYPE {
|
|
|
|
|
innerBox := s.GetInnerBoxForContent(contentWidth, contentHeight)
|
|
|
|
|
obj.ContentAspectRatio = go2.Pointer(innerBox.Width / innerBox.Height)
|
|
|
|
|
}
|
2023-05-04 02:34:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-03-28 00:57:15 +00:00
|
|
|
func (obj *Object) OuterNearContainer() *Object {
|
2023-04-05 00:31:23 +00:00
|
|
|
for obj != nil {
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.NearKey != nil {
|
2023-04-05 00:31:23 +00:00
|
|
|
return obj
|
2023-03-24 19:02:44 +00:00
|
|
|
}
|
2023-04-05 00:31:23 +00:00
|
|
|
obj = obj.Parent
|
2023-03-24 19:02:44 +00:00
|
|
|
}
|
2023-03-28 00:57:15 +00:00
|
|
|
return nil
|
2023-03-24 19:02:44 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-14 21:55:52 +00:00
|
|
|
func (obj *Object) IsConstantNear() bool {
|
|
|
|
|
if obj.NearKey == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
keyPath := Key(obj.NearKey)
|
|
|
|
|
|
|
|
|
|
// interesting if there is a shape with id=top-left, then top-left isn't treated a constant near
|
|
|
|
|
_, isKey := obj.Graph.Root.HasChild(keyPath)
|
|
|
|
|
if isKey {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
_, isConst := NearConstants[keyPath[0]]
|
|
|
|
|
return isConst
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
type Edge struct {
|
|
|
|
|
Index int `json:"index"`
|
|
|
|
|
|
2022-11-16 18:54:34 +00:00
|
|
|
SrcTableColumnIndex *int `json:"srcTableColumnIndex,omitempty"`
|
|
|
|
|
DstTableColumnIndex *int `json:"dstTableColumnIndex,omitempty"`
|
2022-11-15 22:41:44 +00:00
|
|
|
|
2023-04-14 03:04:55 +00:00
|
|
|
LabelPosition *string `json:"labelPosition,omitempty"`
|
|
|
|
|
LabelPercentage *float64 `json:"labelPercentage,omitempty"`
|
2022-11-03 13:54:49 +00:00
|
|
|
|
|
|
|
|
IsCurve bool `json:"isCurve"`
|
|
|
|
|
Route []*geo.Point `json:"route,omitempty"`
|
|
|
|
|
|
|
|
|
|
Src *Object `json:"-"`
|
|
|
|
|
SrcArrow bool `json:"src_arrow"`
|
|
|
|
|
SrcArrowhead *Attributes `json:"srcArrowhead,omitempty"`
|
|
|
|
|
Dst *Object `json:"-"`
|
|
|
|
|
// TODO alixander (Mon Sep 12 2022): deprecate SrcArrow and DstArrow and just use SrcArrowhead and DstArrowhead
|
|
|
|
|
DstArrow bool `json:"dst_arrow"`
|
|
|
|
|
DstArrowhead *Attributes `json:"dstArrowhead,omitempty"`
|
|
|
|
|
|
|
|
|
|
References []EdgeReference `json:"references,omitempty"`
|
2023-04-14 03:04:55 +00:00
|
|
|
Attributes `json:"attributes,omitempty"`
|
2022-11-29 22:21:23 +00:00
|
|
|
|
2022-11-30 19:22:43 +00:00
|
|
|
ZIndex int `json:"zIndex"`
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type EdgeReference struct {
|
|
|
|
|
Edge *d2ast.Edge `json:"-"`
|
|
|
|
|
|
|
|
|
|
MapKey *d2ast.Key `json:"-"`
|
|
|
|
|
MapKeyEdgeIndex int `json:"map_key_edge_index"`
|
|
|
|
|
Scope *d2ast.Map `json:"-"`
|
|
|
|
|
ScopeObj *Object `json:"-"`
|
2023-06-25 18:51:21 +00:00
|
|
|
ScopeAST *d2ast.Map `json:"-"`
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-05 00:44:05 +00:00
|
|
|
func (e *Edge) GetAstEdge() *d2ast.Edge {
|
|
|
|
|
return e.References[0].Edge
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-09 18:16:28 +00:00
|
|
|
func (e *Edge) GetStroke(dashGapSize interface{}) string {
|
2022-11-03 13:54:49 +00:00
|
|
|
if dashGapSize != 0.0 {
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B2
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2023-01-09 18:16:28 +00:00
|
|
|
return color.B1
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *Edge) ArrowString() string {
|
|
|
|
|
if e.SrcArrow && e.DstArrow {
|
|
|
|
|
return "<->"
|
|
|
|
|
}
|
|
|
|
|
if e.SrcArrow {
|
|
|
|
|
return "<-"
|
|
|
|
|
}
|
|
|
|
|
if e.DstArrow {
|
|
|
|
|
return "->"
|
|
|
|
|
}
|
|
|
|
|
return "--"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *Edge) Text() *d2target.MText {
|
2022-11-29 19:27:41 +00:00
|
|
|
fontSize := d2fonts.FONT_SIZE_M
|
2023-04-14 03:04:55 +00:00
|
|
|
if e.Style.FontSize != nil {
|
|
|
|
|
fontSize, _ = strconv.Atoi(e.Style.FontSize.Value)
|
2022-11-29 19:27:41 +00:00
|
|
|
}
|
2023-03-29 23:36:30 +00:00
|
|
|
isBold := false
|
2023-04-14 03:04:55 +00:00
|
|
|
if e.Style.Bold != nil {
|
|
|
|
|
isBold, _ = strconv.ParseBool(e.Style.Bold.Value)
|
2023-03-29 23:36:30 +00:00
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
return &d2target.MText{
|
2023-04-14 03:04:55 +00:00
|
|
|
Text: e.Label.Value,
|
2022-11-29 19:27:41 +00:00
|
|
|
FontSize: fontSize,
|
2023-03-29 23:36:30 +00:00
|
|
|
IsBold: isBold,
|
2022-11-03 13:54:49 +00:00
|
|
|
IsItalic: true,
|
|
|
|
|
|
|
|
|
|
Dimensions: e.LabelDimensions,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-13 22:02:26 +00:00
|
|
|
func (e *Edge) Move(dx, dy float64) {
|
|
|
|
|
for _, p := range e.Route {
|
|
|
|
|
p.X += dx
|
|
|
|
|
p.Y += dy
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
func (e *Edge) AbsID() string {
|
|
|
|
|
srcIDA := e.Src.AbsIDArray()
|
|
|
|
|
dstIDA := e.Dst.AbsIDArray()
|
|
|
|
|
|
|
|
|
|
var commonIDA []string
|
|
|
|
|
for len(srcIDA) > 1 && len(dstIDA) > 1 {
|
|
|
|
|
if !strings.EqualFold(srcIDA[0], dstIDA[0]) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
commonIDA = append(commonIDA, srcIDA[0])
|
|
|
|
|
srcIDA = srcIDA[1:]
|
|
|
|
|
dstIDA = dstIDA[1:]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
commonKey := ""
|
|
|
|
|
if len(commonIDA) > 0 {
|
|
|
|
|
commonKey = strings.Join(commonIDA, ".") + "."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf("%s(%s %s %s)[%d]", commonKey, strings.Join(srcIDA, "."), e.ArrowString(), strings.Join(dstIDA, "."), e.Index)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (obj *Object) Connect(srcID, dstID []string, srcArrow, dstArrow bool, label string) (*Edge, error) {
|
|
|
|
|
for _, id := range [][]string{srcID, dstID} {
|
|
|
|
|
for _, p := range id {
|
|
|
|
|
if _, ok := ReservedKeywords[p]; ok {
|
|
|
|
|
return nil, errors.New("cannot connect to reserved keyword")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-24 11:09:40 +00:00
|
|
|
src := obj.ensureChildEdge(srcID)
|
|
|
|
|
dst := obj.ensureChildEdge(dstID)
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2023-01-22 08:21:21 +00:00
|
|
|
e := &Edge{
|
2023-04-14 03:04:55 +00:00
|
|
|
Attributes: Attributes{
|
2022-11-03 13:54:49 +00:00
|
|
|
Label: Scalar{
|
|
|
|
|
Value: label,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Src: src,
|
|
|
|
|
SrcArrow: srcArrow,
|
|
|
|
|
Dst: dst,
|
|
|
|
|
DstArrow: dstArrow,
|
|
|
|
|
}
|
2023-01-22 08:21:21 +00:00
|
|
|
e.initIndex()
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2023-01-28 01:19:12 +00:00
|
|
|
addSQLTableColumnIndices(e, srcID, dstID, obj, src, dst)
|
2023-01-22 08:59:02 +00:00
|
|
|
|
|
|
|
|
obj.Graph.Edges = append(obj.Graph.Edges, e)
|
|
|
|
|
return e, nil
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2023-01-28 01:19:12 +00:00
|
|
|
func addSQLTableColumnIndices(e *Edge, srcID, dstID []string, obj, src, dst *Object) {
|
2023-04-14 03:04:55 +00:00
|
|
|
if src.Shape.Value == d2target.ShapeSQLTable {
|
2023-01-22 08:59:02 +00:00
|
|
|
if src == dst {
|
|
|
|
|
// Ignore edge to column inside table.
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-01-22 08:21:21 +00:00
|
|
|
objAbsID := obj.AbsIDArray()
|
|
|
|
|
srcAbsID := src.AbsIDArray()
|
2023-01-22 09:43:25 +00:00
|
|
|
if len(objAbsID)+len(srcID) > len(srcAbsID) {
|
2023-01-22 08:21:21 +00:00
|
|
|
for i, d2col := range src.SQLTable.Columns {
|
|
|
|
|
if d2col.Name.Label == srcID[len(srcID)-1] {
|
|
|
|
|
d2col.Reference = dst.AbsID()
|
|
|
|
|
e.SrcTableColumnIndex = new(int)
|
|
|
|
|
*e.SrcTableColumnIndex = i
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-14 03:04:55 +00:00
|
|
|
if dst.Shape.Value == d2target.ShapeSQLTable {
|
2023-01-22 08:21:21 +00:00
|
|
|
objAbsID := obj.AbsIDArray()
|
|
|
|
|
dstAbsID := dst.AbsIDArray()
|
2023-01-22 09:43:25 +00:00
|
|
|
if len(objAbsID)+len(dstID) > len(dstAbsID) {
|
2023-01-22 08:21:21 +00:00
|
|
|
for i, d2col := range dst.SQLTable.Columns {
|
|
|
|
|
if d2col.Name.Label == dstID[len(dstID)-1] {
|
|
|
|
|
d2col.Reference = dst.AbsID()
|
|
|
|
|
e.DstTableColumnIndex = new(int)
|
|
|
|
|
*e.DstTableColumnIndex = i
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Treat undirectional/bidirectional edge here and in HasEdge flipped. Same with
|
|
|
|
|
// SrcArrow.
|
|
|
|
|
func (e *Edge) initIndex() {
|
|
|
|
|
for _, e2 := range e.Src.Graph.Edges {
|
|
|
|
|
if e.Src == e2.Src &&
|
|
|
|
|
e.SrcArrow == e2.SrcArrow &&
|
|
|
|
|
e.Dst == e2.Dst &&
|
|
|
|
|
e.DstArrow == e2.DstArrow {
|
|
|
|
|
e.Index++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func findMeasured(mtexts []*d2target.MText, t1 *d2target.MText) *d2target.TextDimensions {
|
|
|
|
|
for i, t2 := range mtexts {
|
|
|
|
|
if t1.Text != t2.Text {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if t1.FontSize != t2.FontSize {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if t1.IsBold != t2.IsBold {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if t1.IsItalic != t2.IsItalic {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if t1.Language != t2.Language {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return &mtexts[i].Dimensions
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-22 05:58:56 +00:00
|
|
|
func getMarkdownDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText, fontFamily *d2fonts.FontFamily) (*d2target.TextDimensions, error) {
|
2022-11-03 13:54:49 +00:00
|
|
|
if dims := findMeasured(mtexts, t); dims != nil {
|
|
|
|
|
return dims, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ruler != nil {
|
2023-04-13 03:13:07 +00:00
|
|
|
width, height, err := textmeasure.MeasureMarkdown(t.Text, ruler, fontFamily, t.FontSize)
|
2022-11-03 13:54:49 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return d2target.NewTextDimensions(width, height), nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-10 00:55:34 +00:00
|
|
|
if strings.TrimSpace(t.Text) == "" {
|
|
|
|
|
return d2target.NewTextDimensions(1, 1), nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
return nil, fmt.Errorf("text not pre-measured and no ruler provided")
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-28 04:29:51 +00:00
|
|
|
func GetTextDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, t *d2target.MText, fontFamily *d2fonts.FontFamily) *d2target.TextDimensions {
|
2022-11-03 13:54:49 +00:00
|
|
|
if dims := findMeasured(mtexts, t); dims != nil {
|
|
|
|
|
return dims
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ruler != nil {
|
|
|
|
|
var w int
|
|
|
|
|
var h int
|
|
|
|
|
if t.Language != "" {
|
2023-06-19 21:17:34 +00:00
|
|
|
originalLineHeight := ruler.LineHeightFactor
|
2023-06-19 22:25:14 +00:00
|
|
|
ruler.LineHeightFactor = textmeasure.CODE_LINE_HEIGHT
|
2023-06-19 21:17:34 +00:00
|
|
|
w, h = ruler.MeasureMono(d2fonts.SourceCodePro.Font(t.FontSize, d2fonts.FONT_STYLE_REGULAR), t.Text)
|
|
|
|
|
ruler.LineHeightFactor = originalLineHeight
|
2023-01-14 04:03:39 +00:00
|
|
|
|
|
|
|
|
// count empty leading and trailing lines since ruler will not be able to measure it
|
|
|
|
|
lines := strings.Split(t.Text, "\n")
|
2023-06-19 22:25:14 +00:00
|
|
|
hasLeading := false
|
|
|
|
|
if len(lines) > 0 && strings.TrimSpace(lines[0]) == "" {
|
|
|
|
|
hasLeading = true
|
2023-01-14 04:03:39 +00:00
|
|
|
}
|
2023-06-19 22:25:14 +00:00
|
|
|
numTrailing := 0
|
2023-01-14 04:03:39 +00:00
|
|
|
for i := len(lines) - 1; i >= 0; i-- {
|
|
|
|
|
if strings.TrimSpace(lines[i]) == "" {
|
2023-06-19 22:25:14 +00:00
|
|
|
numTrailing++
|
2023-01-14 04:03:39 +00:00
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-06-19 22:25:14 +00:00
|
|
|
if hasLeading && numTrailing < len(lines) {
|
|
|
|
|
h += t.FontSize
|
|
|
|
|
}
|
|
|
|
|
h += int(math.Ceil(textmeasure.CODE_LINE_HEIGHT * float64(t.FontSize*numTrailing)))
|
2022-11-03 13:54:49 +00:00
|
|
|
} else {
|
|
|
|
|
style := d2fonts.FONT_STYLE_REGULAR
|
|
|
|
|
if t.IsBold {
|
|
|
|
|
style = d2fonts.FONT_STYLE_BOLD
|
|
|
|
|
} else if t.IsItalic {
|
|
|
|
|
style = d2fonts.FONT_STYLE_ITALIC
|
|
|
|
|
}
|
2022-12-21 07:43:45 +00:00
|
|
|
if fontFamily == nil {
|
2022-12-22 05:58:56 +00:00
|
|
|
fontFamily = go2.Pointer(d2fonts.SourceSansPro)
|
2022-12-21 07:43:45 +00:00
|
|
|
}
|
|
|
|
|
w, h = ruler.Measure(fontFamily.Font(t.FontSize, style), t.Text)
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
return d2target.NewTextDimensions(w, h)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func appendTextDedup(texts []*d2target.MText, t *d2target.MText) []*d2target.MText {
|
2022-12-28 04:29:51 +00:00
|
|
|
if GetTextDimensions(texts, nil, t, nil) == nil {
|
2022-11-03 13:54:49 +00:00
|
|
|
return append(texts, t)
|
|
|
|
|
}
|
|
|
|
|
return texts
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-21 07:43:45 +00:00
|
|
|
func (g *Graph) SetDimensions(mtexts []*d2target.MText, ruler *textmeasure.Ruler, fontFamily *d2fonts.FontFamily) error {
|
2023-01-25 02:20:34 +00:00
|
|
|
if ruler != nil && fontFamily != nil {
|
|
|
|
|
if ok := ruler.HasFontFamilyLoaded(fontFamily); !ok {
|
|
|
|
|
return fmt.Errorf("ruler does not have entire font family %s loaded, is a style missing?", *fontFamily)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-14 17:40:52 +00:00
|
|
|
if g.Theme != nil && g.Theme.SpecialRules.Mono {
|
|
|
|
|
tmp := d2fonts.SourceCodePro
|
|
|
|
|
fontFamily = &tmp
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
for _, obj := range g.Objects {
|
|
|
|
|
obj.Box = &geo.Box{}
|
2022-12-22 05:36:08 +00:00
|
|
|
|
2023-06-22 20:24:24 +00:00
|
|
|
// user-specified label/icon positions
|
|
|
|
|
if obj.HasLabel() && obj.Attributes.LabelPosition != nil {
|
|
|
|
|
scalar := *obj.Attributes.LabelPosition
|
|
|
|
|
position := LabelPositionsMapping[scalar.Value]
|
2023-07-17 21:21:36 +00:00
|
|
|
obj.LabelPosition = go2.Pointer(position.String())
|
2023-06-22 20:24:24 +00:00
|
|
|
}
|
|
|
|
|
if obj.Icon != nil && obj.Attributes.IconPosition != nil {
|
|
|
|
|
scalar := *obj.Attributes.IconPosition
|
|
|
|
|
position := LabelPositionsMapping[scalar.Value]
|
2023-07-17 21:21:36 +00:00
|
|
|
obj.IconPosition = go2.Pointer(position.String())
|
2023-06-22 20:24:24 +00:00
|
|
|
}
|
|
|
|
|
|
2022-12-28 23:58:00 +00:00
|
|
|
var desiredWidth int
|
|
|
|
|
var desiredHeight int
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.WidthAttr != nil {
|
|
|
|
|
desiredWidth, _ = strconv.Atoi(obj.WidthAttr.Value)
|
2022-12-22 05:36:08 +00:00
|
|
|
}
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.HeightAttr != nil {
|
|
|
|
|
desiredHeight, _ = strconv.Atoi(obj.HeightAttr.Value)
|
2022-12-22 05:36:08 +00:00
|
|
|
}
|
2023-01-01 09:34:03 +00:00
|
|
|
|
2023-04-14 03:04:55 +00:00
|
|
|
dslShape := strings.ToLower(obj.Shape.Value)
|
2023-01-27 19:50:48 +00:00
|
|
|
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Label.Value == "" &&
|
2023-01-27 19:50:48 +00:00
|
|
|
dslShape != d2target.ShapeImage &&
|
|
|
|
|
dslShape != d2target.ShapeSQLTable &&
|
|
|
|
|
dslShape != d2target.ShapeClass {
|
|
|
|
|
|
|
|
|
|
if dslShape == d2target.ShapeCircle || dslShape == d2target.ShapeSquare {
|
|
|
|
|
sideLength := DEFAULT_SHAPE_SIZE
|
|
|
|
|
if desiredWidth != 0 || desiredHeight != 0 {
|
2023-02-01 00:19:22 +00:00
|
|
|
sideLength = float64(go2.Max(desiredWidth, desiredHeight))
|
2023-01-27 19:50:48 +00:00
|
|
|
}
|
|
|
|
|
obj.Width = sideLength
|
|
|
|
|
obj.Height = sideLength
|
|
|
|
|
} else {
|
|
|
|
|
obj.Width = DEFAULT_SHAPE_SIZE
|
|
|
|
|
obj.Height = DEFAULT_SHAPE_SIZE
|
|
|
|
|
if desiredWidth != 0 {
|
|
|
|
|
obj.Width = float64(desiredWidth)
|
|
|
|
|
}
|
|
|
|
|
if desiredHeight != 0 {
|
|
|
|
|
obj.Height = float64(desiredHeight)
|
|
|
|
|
}
|
2023-01-01 09:34:03 +00:00
|
|
|
}
|
2023-01-27 19:50:48 +00:00
|
|
|
|
2023-01-01 09:34:03 +00:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-14 03:04:55 +00:00
|
|
|
if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !strings.EqualFold(obj.Shape.Value, d2target.ShapeCode) {
|
|
|
|
|
if obj.Language != "latex" && !obj.Style.NoneTextTransform() {
|
|
|
|
|
obj.Label.Value = strings.ToUpper(obj.Label.Value)
|
2023-03-30 03:09:06 +00:00
|
|
|
}
|
2023-03-14 17:40:52 +00:00
|
|
|
}
|
2023-04-14 03:04:55 +00:00
|
|
|
obj.ApplyTextTransform()
|
2023-03-14 17:40:52 +00:00
|
|
|
|
2022-12-29 02:14:19 +00:00
|
|
|
labelDims, err := obj.GetLabelSize(mtexts, ruler, fontFamily)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2023-02-03 22:53:17 +00:00
|
|
|
obj.LabelDimensions = *labelDims
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2023-02-03 23:34:41 +00:00
|
|
|
// if there is a desired width or height, fit to content box without inner label padding for smallest minimum size
|
|
|
|
|
withInnerLabelPadding := desiredWidth == 0 && desiredHeight == 0 &&
|
2023-04-14 03:04:55 +00:00
|
|
|
dslShape != d2target.ShapeText && obj.Label.Value != ""
|
2023-02-03 23:34:41 +00:00
|
|
|
defaultDims, err := obj.GetDefaultSize(mtexts, ruler, fontFamily, *labelDims, withInnerLabelPadding)
|
2022-12-29 02:14:19 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2023-02-03 22:53:17 +00:00
|
|
|
if dslShape == d2target.ShapeImage {
|
|
|
|
|
if desiredWidth == 0 {
|
|
|
|
|
desiredWidth = defaultDims.Width
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2023-02-03 22:53:17 +00:00
|
|
|
if desiredHeight == 0 {
|
|
|
|
|
desiredHeight = defaultDims.Height
|
2023-01-24 04:33:41 +00:00
|
|
|
}
|
2023-02-03 22:53:17 +00:00
|
|
|
obj.Width = float64(go2.Max(MIN_SHAPE_SIZE, desiredWidth))
|
|
|
|
|
obj.Height = float64(go2.Max(MIN_SHAPE_SIZE, desiredHeight))
|
|
|
|
|
// images don't need further processing
|
|
|
|
|
continue
|
2023-01-24 03:10:31 +00:00
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
|
2023-02-03 22:53:17 +00:00
|
|
|
contentBox := geo.NewBox(geo.NewPoint(0, 0), float64(defaultDims.Width), float64(defaultDims.Height))
|
|
|
|
|
shapeType := d2target.DSL_SHAPE_TO_SHAPE_TYPE[dslShape]
|
|
|
|
|
s := shape.NewShape(shapeType, contentBox)
|
|
|
|
|
paddingX, paddingY := s.GetDefaultPadding()
|
2023-02-05 09:54:35 +00:00
|
|
|
if desiredWidth != 0 {
|
2023-01-27 19:10:58 +00:00
|
|
|
paddingX = 0.
|
2023-02-05 09:54:35 +00:00
|
|
|
}
|
|
|
|
|
if desiredHeight != 0 {
|
2023-01-27 19:10:58 +00:00
|
|
|
paddingY = 0.
|
2023-02-05 09:54:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// give shapes with icons extra padding to fit their label
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Icon != nil {
|
2023-12-12 00:14:43 +00:00
|
|
|
switch shapeType {
|
|
|
|
|
case shape.TABLE_TYPE, shape.CLASS_TYPE, shape.CODE_TYPE, shape.TEXT_TYPE:
|
|
|
|
|
default:
|
|
|
|
|
labelHeight := float64(labelDims.Height + INNER_LABEL_PADDING)
|
|
|
|
|
// Evenly pad enough to fit label above icon
|
|
|
|
|
if desiredWidth == 0 {
|
|
|
|
|
paddingX += labelHeight
|
|
|
|
|
}
|
|
|
|
|
if desiredHeight == 0 {
|
|
|
|
|
paddingY += labelHeight
|
|
|
|
|
}
|
2023-02-03 22:53:17 +00:00
|
|
|
}
|
2023-02-05 09:54:35 +00:00
|
|
|
}
|
|
|
|
|
if desiredWidth == 0 {
|
2023-02-03 22:53:17 +00:00
|
|
|
switch shapeType {
|
2023-12-12 00:14:43 +00:00
|
|
|
case shape.TABLE_TYPE, shape.CLASS_TYPE, shape.CODE_TYPE:
|
2023-02-03 22:53:17 +00:00
|
|
|
default:
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Link != nil {
|
2023-02-03 22:53:17 +00:00
|
|
|
paddingX += 32
|
|
|
|
|
}
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Tooltip != nil {
|
2023-02-03 22:53:17 +00:00
|
|
|
paddingX += 32
|
|
|
|
|
}
|
2023-01-27 19:10:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
2023-02-03 22:53:17 +00:00
|
|
|
|
2023-05-05 03:58:39 +00:00
|
|
|
obj.SizeToContent(contentBox.Width, contentBox.Height, paddingX, paddingY)
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
for _, edge := range g.Edges {
|
2023-04-15 02:08:29 +00:00
|
|
|
usedFont := fontFamily
|
|
|
|
|
if edge.Style.Font != nil {
|
|
|
|
|
f := d2fonts.D2_FONT_TO_FAMILY[edge.Style.Font.Value]
|
|
|
|
|
usedFont = &f
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
if edge.SrcArrowhead != nil && edge.SrcArrowhead.Label.Value != "" {
|
2023-04-14 18:32:14 +00:00
|
|
|
t := edge.Text()
|
|
|
|
|
t.Text = edge.SrcArrowhead.Label.Value
|
2023-04-15 02:08:29 +00:00
|
|
|
dims := GetTextDimensions(mtexts, ruler, t, usedFont)
|
2023-04-14 18:32:14 +00:00
|
|
|
edge.SrcArrowhead.LabelDimensions = *dims
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
if edge.DstArrowhead != nil && edge.DstArrowhead.Label.Value != "" {
|
|
|
|
|
t := edge.Text()
|
2023-04-14 18:32:14 +00:00
|
|
|
t.Text = edge.DstArrowhead.Label.Value
|
2023-04-15 02:08:29 +00:00
|
|
|
dims := GetTextDimensions(mtexts, ruler, t, usedFont)
|
2023-04-14 18:32:14 +00:00
|
|
|
edge.DstArrowhead.LabelDimensions = *dims
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-14 03:04:55 +00:00
|
|
|
if edge.Label.Value == "" {
|
2022-11-03 13:54:49 +00:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-14 03:04:55 +00:00
|
|
|
if g.Theme != nil && g.Theme.SpecialRules.CapsLock && !edge.Style.NoneTextTransform() {
|
|
|
|
|
edge.Label.Value = strings.ToUpper(edge.Label.Value)
|
2023-03-16 22:02:06 +00:00
|
|
|
}
|
2023-04-14 03:04:55 +00:00
|
|
|
edge.ApplyTextTransform()
|
2023-03-14 17:40:52 +00:00
|
|
|
|
2023-03-16 22:02:06 +00:00
|
|
|
dims := GetTextDimensions(mtexts, ruler, edge.Text(), usedFont)
|
2022-11-03 13:54:49 +00:00
|
|
|
if dims == nil {
|
|
|
|
|
return fmt.Errorf("dimensions for edge label %#v not found", edge.Text())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
edge.LabelDimensions = *dims
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *Graph) Texts() []*d2target.MText {
|
|
|
|
|
var texts []*d2target.MText
|
|
|
|
|
|
2023-03-16 17:48:54 +00:00
|
|
|
capsLock := g.Theme != nil && g.Theme.SpecialRules.CapsLock
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
for _, obj := range g.Objects {
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Label.Value != "" {
|
|
|
|
|
obj.ApplyTextTransform()
|
2023-03-16 17:48:54 +00:00
|
|
|
text := obj.Text()
|
2023-04-14 03:04:55 +00:00
|
|
|
if capsLock && !strings.EqualFold(obj.Shape.Value, d2target.ShapeCode) {
|
|
|
|
|
if obj.Language != "latex" && !obj.Style.NoneTextTransform() {
|
2023-03-30 03:09:06 +00:00
|
|
|
text.Text = strings.ToUpper(text.Text)
|
|
|
|
|
}
|
2023-03-16 17:48:54 +00:00
|
|
|
}
|
|
|
|
|
texts = appendTextDedup(texts, text)
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
if obj.Class != nil {
|
2023-02-06 08:47:13 +00:00
|
|
|
fontSize := d2fonts.FONT_SIZE_L
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Style.FontSize != nil {
|
|
|
|
|
fontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
|
2023-02-06 08:47:13 +00:00
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
for _, field := range obj.Class.Fields {
|
2023-02-06 08:47:13 +00:00
|
|
|
texts = appendTextDedup(texts, field.Text(fontSize))
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
for _, method := range obj.Class.Methods {
|
2023-02-06 08:47:13 +00:00
|
|
|
texts = appendTextDedup(texts, method.Text(fontSize))
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
} else if obj.SQLTable != nil {
|
2023-02-06 08:42:28 +00:00
|
|
|
colFontSize := d2fonts.FONT_SIZE_L
|
2023-04-14 03:04:55 +00:00
|
|
|
if obj.Style.FontSize != nil {
|
|
|
|
|
colFontSize, _ = strconv.Atoi(obj.Style.FontSize.Value)
|
2023-02-06 08:42:28 +00:00
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
for _, column := range obj.SQLTable.Columns {
|
2023-02-06 08:42:28 +00:00
|
|
|
for _, t := range column.Texts(colFontSize) {
|
2022-12-23 08:29:03 +00:00
|
|
|
texts = appendTextDedup(texts, t)
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, edge := range g.Edges {
|
2023-04-14 03:04:55 +00:00
|
|
|
if edge.Label.Value != "" {
|
|
|
|
|
edge.ApplyTextTransform()
|
2023-03-16 17:48:54 +00:00
|
|
|
text := edge.Text()
|
2023-04-14 03:04:55 +00:00
|
|
|
if capsLock && !edge.Style.NoneTextTransform() {
|
2023-03-16 17:48:54 +00:00
|
|
|
text.Text = strings.ToUpper(text.Text)
|
|
|
|
|
}
|
2023-03-16 18:01:25 +00:00
|
|
|
texts = appendTextDedup(texts, text)
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
if edge.SrcArrowhead != nil && edge.SrcArrowhead.Label.Value != "" {
|
|
|
|
|
t := edge.Text()
|
|
|
|
|
t.Text = edge.SrcArrowhead.Label.Value
|
|
|
|
|
texts = appendTextDedup(texts, t)
|
|
|
|
|
}
|
|
|
|
|
if edge.DstArrowhead != nil && edge.DstArrowhead.Label.Value != "" {
|
|
|
|
|
t := edge.Text()
|
|
|
|
|
t.Text = edge.DstArrowhead.Label.Value
|
|
|
|
|
texts = appendTextDedup(texts, t)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-12 01:34:12 +00:00
|
|
|
for _, board := range g.Layers {
|
|
|
|
|
for _, t := range board.Texts() {
|
|
|
|
|
texts = appendTextDedup(texts, t)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, board := range g.Scenarios {
|
|
|
|
|
for _, t := range board.Texts() {
|
|
|
|
|
texts = appendTextDedup(texts, t)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, board := range g.Steps {
|
|
|
|
|
for _, t := range board.Texts() {
|
|
|
|
|
texts = appendTextDedup(texts, t)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
return texts
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Key(k *d2ast.KeyPath) []string {
|
2023-01-11 23:50:49 +00:00
|
|
|
return d2format.KeyPath(k)
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
2023-01-22 09:43:25 +00:00
|
|
|
// All reserved keywords. See init below.
|
|
|
|
|
var ReservedKeywords map[string]struct{}
|
|
|
|
|
|
|
|
|
|
// Non Style/Holder keywords.
|
|
|
|
|
var SimpleReservedKeywords = map[string]struct{}{
|
2023-04-12 20:48:53 +00:00
|
|
|
"label": {},
|
|
|
|
|
"desc": {},
|
|
|
|
|
"shape": {},
|
|
|
|
|
"icon": {},
|
|
|
|
|
"constraint": {},
|
|
|
|
|
"tooltip": {},
|
|
|
|
|
"link": {},
|
|
|
|
|
"near": {},
|
|
|
|
|
"width": {},
|
|
|
|
|
"height": {},
|
|
|
|
|
"direction": {},
|
|
|
|
|
"top": {},
|
|
|
|
|
"left": {},
|
|
|
|
|
"grid-rows": {},
|
|
|
|
|
"grid-columns": {},
|
|
|
|
|
"grid-gap": {},
|
|
|
|
|
"vertical-gap": {},
|
|
|
|
|
"horizontal-gap": {},
|
|
|
|
|
"class": {},
|
2023-07-11 21:56:09 +00:00
|
|
|
"vars": {},
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
2023-06-01 06:28:33 +00:00
|
|
|
// ReservedKeywordHolders are reserved keywords that are meaningless on its own and must hold composites
|
2022-11-03 13:54:49 +00:00
|
|
|
var ReservedKeywordHolders = map[string]struct{}{
|
|
|
|
|
"style": {},
|
|
|
|
|
"source-arrowhead": {},
|
|
|
|
|
"target-arrowhead": {},
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-01 06:28:33 +00:00
|
|
|
// CompositeReservedKeywords are reserved keywords that can hold composites
|
|
|
|
|
var CompositeReservedKeywords = map[string]struct{}{
|
2023-06-22 23:13:08 +00:00
|
|
|
"classes": {},
|
|
|
|
|
"constraint": {},
|
|
|
|
|
"label": {},
|
|
|
|
|
"icon": {},
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// StyleKeywords are reserved keywords which cannot exist outside of the "style" keyword
|
|
|
|
|
var StyleKeywords = map[string]struct{}{
|
|
|
|
|
"opacity": {},
|
|
|
|
|
"stroke": {},
|
|
|
|
|
"fill": {},
|
2023-03-14 03:07:13 +00:00
|
|
|
"fill-pattern": {},
|
2022-11-03 13:54:49 +00:00
|
|
|
"stroke-width": {},
|
|
|
|
|
"stroke-dash": {},
|
|
|
|
|
"border-radius": {},
|
|
|
|
|
|
|
|
|
|
// Only for text
|
2023-04-08 19:24:29 +00:00
|
|
|
"font": {},
|
|
|
|
|
"font-size": {},
|
|
|
|
|
"font-color": {},
|
|
|
|
|
"bold": {},
|
|
|
|
|
"italic": {},
|
|
|
|
|
"underline": {},
|
|
|
|
|
"text-transform": {},
|
2022-11-03 13:54:49 +00:00
|
|
|
|
|
|
|
|
// Only for shapes
|
2022-12-31 07:26:38 +00:00
|
|
|
"shadow": {},
|
|
|
|
|
"multiple": {},
|
|
|
|
|
"double-border": {},
|
2022-11-03 13:54:49 +00:00
|
|
|
|
|
|
|
|
// Only for squares
|
|
|
|
|
"3d": {},
|
|
|
|
|
|
|
|
|
|
// Only for edges
|
|
|
|
|
"animated": {},
|
|
|
|
|
"filled": {},
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-26 00:33:32 +00:00
|
|
|
// TODO maybe autofmt should allow other values, and transform them to conform
|
|
|
|
|
// e.g. left-center becomes center-left
|
2022-12-25 21:42:11 +00:00
|
|
|
var NearConstantsArray = []string{
|
|
|
|
|
"top-left",
|
|
|
|
|
"top-center",
|
|
|
|
|
"top-right",
|
|
|
|
|
|
|
|
|
|
"center-left",
|
|
|
|
|
"center-right",
|
|
|
|
|
|
|
|
|
|
"bottom-left",
|
|
|
|
|
"bottom-center",
|
|
|
|
|
"bottom-right",
|
|
|
|
|
}
|
2022-12-26 01:43:43 +00:00
|
|
|
var NearConstants map[string]struct{}
|
2022-12-25 21:42:11 +00:00
|
|
|
|
2023-06-22 23:13:08 +00:00
|
|
|
// LabelPositionsArray are the values that labels and icons can set `near` to
|
|
|
|
|
var LabelPositionsArray = []string{
|
|
|
|
|
"top-left",
|
|
|
|
|
"top-center",
|
|
|
|
|
"top-right",
|
|
|
|
|
|
|
|
|
|
"center-left",
|
|
|
|
|
"center-center",
|
|
|
|
|
"center-right",
|
|
|
|
|
|
|
|
|
|
"bottom-left",
|
|
|
|
|
"bottom-center",
|
|
|
|
|
"bottom-right",
|
|
|
|
|
|
|
|
|
|
"outside-top-left",
|
|
|
|
|
"outside-top-center",
|
|
|
|
|
"outside-top-right",
|
|
|
|
|
|
|
|
|
|
"outside-left-top",
|
|
|
|
|
"outside-left-center",
|
|
|
|
|
"outside-left-bottom",
|
|
|
|
|
|
|
|
|
|
"outside-right-top",
|
|
|
|
|
"outside-right-center",
|
|
|
|
|
"outside-right-bottom",
|
|
|
|
|
|
|
|
|
|
"outside-bottom-left",
|
|
|
|
|
"outside-bottom-center",
|
|
|
|
|
"outside-bottom-right",
|
|
|
|
|
}
|
|
|
|
|
var LabelPositions map[string]struct{}
|
|
|
|
|
|
2023-06-22 20:24:24 +00:00
|
|
|
// convert to label.Position
|
|
|
|
|
var LabelPositionsMapping = map[string]label.Position{
|
|
|
|
|
"top-left": label.InsideTopLeft,
|
|
|
|
|
"top-center": label.InsideTopCenter,
|
|
|
|
|
"top-right": label.InsideTopRight,
|
|
|
|
|
|
|
|
|
|
"center-left": label.InsideMiddleLeft,
|
|
|
|
|
"center-center": label.InsideMiddleCenter,
|
|
|
|
|
"center-right": label.InsideMiddleRight,
|
|
|
|
|
|
|
|
|
|
"bottom-left": label.InsideBottomLeft,
|
|
|
|
|
"bottom-center": label.InsideBottomCenter,
|
|
|
|
|
"bottom-right": label.InsideBottomRight,
|
|
|
|
|
|
|
|
|
|
"outside-top-left": label.OutsideTopLeft,
|
|
|
|
|
"outside-top-center": label.OutsideTopCenter,
|
|
|
|
|
"outside-top-right": label.OutsideTopRight,
|
|
|
|
|
|
|
|
|
|
"outside-left-top": label.OutsideLeftTop,
|
|
|
|
|
"outside-left-center": label.OutsideLeftMiddle,
|
|
|
|
|
"outside-left-bottom": label.OutsideLeftBottom,
|
|
|
|
|
|
|
|
|
|
"outside-right-top": label.OutsideRightTop,
|
|
|
|
|
"outside-right-center": label.OutsideRightMiddle,
|
|
|
|
|
"outside-right-bottom": label.OutsideRightBottom,
|
|
|
|
|
|
|
|
|
|
"outside-bottom-left": label.OutsideBottomLeft,
|
|
|
|
|
"outside-bottom-center": label.OutsideBottomCenter,
|
|
|
|
|
"outside-bottom-right": label.OutsideBottomRight,
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-14 03:07:13 +00:00
|
|
|
var FillPatterns = []string{
|
2024-03-25 21:30:23 +00:00
|
|
|
"none",
|
2023-03-14 03:07:13 +00:00
|
|
|
"dots",
|
2023-03-16 05:53:12 +00:00
|
|
|
"lines",
|
|
|
|
|
"grain",
|
2023-03-18 22:43:28 +00:00
|
|
|
"paper",
|
2023-03-14 03:07:13 +00:00
|
|
|
}
|
|
|
|
|
|
2023-03-31 08:58:02 +00:00
|
|
|
var textTransforms = []string{"none", "uppercase", "lowercase", "capitalize"}
|
|
|
|
|
|
2023-01-28 01:19:12 +00:00
|
|
|
// BoardKeywords contains the keywords that create new boards.
|
|
|
|
|
var BoardKeywords = map[string]struct{}{
|
|
|
|
|
"layers": {},
|
|
|
|
|
"scenarios": {},
|
|
|
|
|
"steps": {},
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-03 13:54:49 +00:00
|
|
|
func init() {
|
2023-01-22 09:43:25 +00:00
|
|
|
ReservedKeywords = make(map[string]struct{})
|
|
|
|
|
for k, v := range SimpleReservedKeywords {
|
|
|
|
|
ReservedKeywords[k] = v
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
for k, v := range StyleKeywords {
|
|
|
|
|
ReservedKeywords[k] = v
|
|
|
|
|
}
|
|
|
|
|
for k, v := range ReservedKeywordHolders {
|
2023-06-01 06:28:33 +00:00
|
|
|
CompositeReservedKeywords[k] = v
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2023-01-28 01:19:12 +00:00
|
|
|
for k, v := range BoardKeywords {
|
2023-06-01 06:28:33 +00:00
|
|
|
CompositeReservedKeywords[k] = v
|
2023-01-28 01:19:12 +00:00
|
|
|
}
|
2023-06-01 06:28:33 +00:00
|
|
|
for k, v := range CompositeReservedKeywords {
|
|
|
|
|
ReservedKeywords[k] = v
|
2023-01-28 01:19:12 +00:00
|
|
|
}
|
|
|
|
|
|
2022-12-26 01:43:43 +00:00
|
|
|
NearConstants = make(map[string]struct{}, len(NearConstantsArray))
|
|
|
|
|
for _, k := range NearConstantsArray {
|
|
|
|
|
NearConstants[k] = struct{}{}
|
|
|
|
|
}
|
2023-06-22 23:13:08 +00:00
|
|
|
|
|
|
|
|
LabelPositions = make(map[string]struct{}, len(LabelPositionsArray))
|
|
|
|
|
for _, k := range LabelPositionsArray {
|
|
|
|
|
LabelPositions[k] = struct{}{}
|
|
|
|
|
}
|
2022-11-03 13:54:49 +00:00
|
|
|
}
|
2023-01-27 18:41:25 +00:00
|
|
|
|
2023-01-28 01:19:12 +00:00
|
|
|
func (g *Graph) GetBoard(name string) *Graph {
|
2023-01-27 18:41:25 +00:00
|
|
|
for _, l := range g.Layers {
|
|
|
|
|
if l.Name == name {
|
|
|
|
|
return l
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, l := range g.Scenarios {
|
|
|
|
|
if l.Name == name {
|
|
|
|
|
return l
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, l := range g.Steps {
|
|
|
|
|
if l.Name == name {
|
|
|
|
|
return l
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2023-01-28 07:05:42 +00:00
|
|
|
|
2023-02-02 20:24:48 +00:00
|
|
|
func (g *Graph) SortObjectsByAST() {
|
|
|
|
|
objects := append([]*Object(nil), g.Objects...)
|
|
|
|
|
sort.Slice(objects, func(i, j int) bool {
|
|
|
|
|
o1 := objects[i]
|
|
|
|
|
o2 := objects[j]
|
|
|
|
|
if len(o1.References) == 0 || len(o2.References) == 0 {
|
|
|
|
|
return i < j
|
|
|
|
|
}
|
|
|
|
|
r1 := o1.References[0]
|
|
|
|
|
r2 := o2.References[0]
|
|
|
|
|
return r1.Key.Path[r1.KeyPathIndex].Unbox().GetRange().Before(r2.Key.Path[r2.KeyPathIndex].Unbox().GetRange())
|
|
|
|
|
})
|
|
|
|
|
g.Objects = objects
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-28 07:45:04 +00:00
|
|
|
func (g *Graph) SortEdgesByAST() {
|
2023-01-28 07:05:42 +00:00
|
|
|
edges := append([]*Edge(nil), g.Edges...)
|
|
|
|
|
sort.Slice(edges, func(i, j int) bool {
|
|
|
|
|
e1 := edges[i]
|
|
|
|
|
e2 := edges[j]
|
|
|
|
|
if len(e1.References) == 0 || len(e2.References) == 0 {
|
|
|
|
|
return i < j
|
|
|
|
|
}
|
2023-01-28 08:00:25 +00:00
|
|
|
return e1.References[0].Edge.Range.Before(e2.References[0].Edge.Range)
|
2023-01-28 07:05:42 +00:00
|
|
|
})
|
2023-01-28 07:45:04 +00:00
|
|
|
g.Edges = edges
|
2023-01-28 07:05:42 +00:00
|
|
|
}
|
2023-02-22 20:15:59 +00:00
|
|
|
|
|
|
|
|
func (obj *Object) IsDescendantOf(ancestor *Object) bool {
|
|
|
|
|
if obj == ancestor {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if obj.Parent == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return obj.Parent.IsDescendantOf(ancestor)
|
|
|
|
|
}
|
2023-03-14 17:40:52 +00:00
|
|
|
|
|
|
|
|
// ApplyTheme applies themes on the graph level
|
|
|
|
|
// This is different than on the render level, which only changes colors
|
|
|
|
|
// A theme applied on the graph level applies special rules that change the graph
|
|
|
|
|
func (g *Graph) ApplyTheme(themeID int64) error {
|
|
|
|
|
theme := d2themescatalog.Find(themeID)
|
|
|
|
|
if theme == (d2themes.Theme{}) {
|
|
|
|
|
return fmt.Errorf("theme %d not found", themeID)
|
|
|
|
|
}
|
|
|
|
|
g.Theme = &theme
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2023-05-08 20:22:48 +00:00
|
|
|
|
2023-05-09 01:38:41 +00:00
|
|
|
func (g *Graph) PrintString() string {
|
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
|
fmt.Fprint(buf, "Objects: [")
|
|
|
|
|
for _, obj := range g.Objects {
|
2023-09-16 05:07:06 +00:00
|
|
|
fmt.Fprintf(buf, "%v, ", obj.AbsID())
|
2023-05-09 01:38:41 +00:00
|
|
|
}
|
|
|
|
|
fmt.Fprint(buf, "]")
|
|
|
|
|
return buf.String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (obj *Object) IterDescendants(apply func(parent, child *Object)) {
|
|
|
|
|
for _, c := range obj.ChildrenArray {
|
|
|
|
|
apply(obj, c)
|
|
|
|
|
c.IterDescendants(apply)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-05-24 22:39:38 +00:00
|
|
|
|
2023-05-25 02:31:01 +00:00
|
|
|
func (obj *Object) IsMultiple() bool {
|
|
|
|
|
return obj.Style.Multiple != nil && obj.Style.Multiple.Value == "true"
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-26 19:37:51 +00:00
|
|
|
func (obj *Object) Is3D() bool {
|
2023-05-25 02:31:01 +00:00
|
|
|
return obj.Style.ThreeDee != nil && obj.Style.ThreeDee.Value == "true"
|
|
|
|
|
}
|
2023-11-09 01:12:12 +00:00
|
|
|
|
|
|
|
|
func (obj *Object) Spacing() (margin, padding geo.Spacing) {
|
2023-12-12 22:34:31 +00:00
|
|
|
return obj.SpacingOpt(2*label.PADDING, 2*label.PADDING, true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (obj *Object) SpacingOpt(labelPadding, iconPadding float64, maxIconSize bool) (margin, padding geo.Spacing) {
|
2023-11-09 01:12:12 +00:00
|
|
|
if obj.HasLabel() {
|
|
|
|
|
var position label.Position
|
|
|
|
|
if obj.LabelPosition != nil {
|
|
|
|
|
position = label.FromString(*obj.LabelPosition)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var labelWidth, labelHeight float64
|
|
|
|
|
if obj.LabelDimensions.Width > 0 {
|
2023-12-12 22:34:31 +00:00
|
|
|
labelWidth = float64(obj.LabelDimensions.Width) + labelPadding
|
2023-11-09 01:12:12 +00:00
|
|
|
}
|
|
|
|
|
if obj.LabelDimensions.Height > 0 {
|
2023-12-12 22:34:31 +00:00
|
|
|
labelHeight = float64(obj.LabelDimensions.Height) + labelPadding
|
2023-11-09 01:12:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight:
|
|
|
|
|
padding.Top = labelHeight
|
|
|
|
|
case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight:
|
|
|
|
|
padding.Bottom = labelHeight
|
|
|
|
|
case label.InsideMiddleLeft:
|
|
|
|
|
padding.Left = labelWidth
|
|
|
|
|
case label.InsideMiddleRight:
|
|
|
|
|
padding.Right = labelWidth
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-13 00:01:48 +00:00
|
|
|
if obj.HasIcon() {
|
2023-11-09 01:12:12 +00:00
|
|
|
var position label.Position
|
|
|
|
|
if obj.IconPosition != nil {
|
|
|
|
|
position = label.FromString(*obj.IconPosition)
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-12 22:34:31 +00:00
|
|
|
iconSize := float64(d2target.MAX_ICON_SIZE + iconPadding)
|
|
|
|
|
if !maxIconSize {
|
|
|
|
|
iconSize = float64(d2target.GetIconSize(obj.Box, position.String())) + iconPadding
|
|
|
|
|
}
|
2023-11-09 01:12:12 +00:00
|
|
|
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)
|
|
|
|
|
case label.InsideTopLeft, label.InsideTopCenter, label.InsideTopRight:
|
|
|
|
|
padding.Top = math.Max(padding.Top, iconSize)
|
|
|
|
|
case label.InsideBottomLeft, label.InsideBottomCenter, label.InsideBottomRight:
|
|
|
|
|
padding.Bottom = math.Max(padding.Bottom, iconSize)
|
|
|
|
|
case label.InsideMiddleLeft:
|
|
|
|
|
padding.Left = math.Max(padding.Left, iconSize)
|
|
|
|
|
case label.InsideMiddleRight:
|
|
|
|
|
padding.Right = math.Max(padding.Right, iconSize)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dx, dy := obj.GetModifierElementAdjustments()
|
|
|
|
|
margin.Right += dx
|
|
|
|
|
margin.Top += dy
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|