package gliffy2drawio import ( "encoding/xml" "math" "strings" ) type MxGraph struct { Model *MxGraphModel nextID int root *MxCell defaultParent *MxCell } func NewMxGraph() *MxGraph { root := &MxCell{ID: "0"} layer := &MxCell{ID: "1", Parent: "0"} graph := &MxGraph{ Model: &MxGraphModel{ Root: &mxRoot{Cells: []*MxCell{root, layer}}, }, nextID: 2, root: root, defaultParent: layer, } return graph } func (g *MxGraph) SetDefaultParent(cell *MxCell) { g.defaultParent = cell } func (g *MxGraph) NextID() string { id := g.nextID g.nextID++ return intToString(id) } func (g *MxGraph) AddCell(cell *MxCell, parent *MxCell) { if cell == nil { return } // Allow preassigned IDs; otherwise generate. if cell.ID == "" { cell.ID = g.NextID() } if parent == nil { parent = g.defaultParent } cell.Parent = parent.ID g.Model.Root.Cells = append(g.Model.Root.Cells, cell) } // HasCell returns true if a cell with the given ID exists in the root. func (g *MxGraph) HasCell(id string) bool { for _, cell := range g.Model.Root.Cells { if cell != nil && cell.ID == id { return true } } return false } type MxGraphModel struct { XMLName xml.Name `xml:"mxGraphModel"` Style string `xml:"style,attr,omitempty"` Background string `xml:"background,attr,omitempty"` Grid int `xml:"grid,attr,omitempty"` GridSize int `xml:"gridSize,attr,omitempty"` Guides int `xml:"guides,attr,omitempty"` Dx int `xml:"dx,attr,omitempty"` Dy int `xml:"dy,attr,omitempty"` Connect int `xml:"connect,attr,omitempty"` Arrows int `xml:"arrows,attr,omitempty"` Fold int `xml:"fold,attr,omitempty"` Page int `xml:"page,attr,omitempty"` PageScale int `xml:"pageScale,attr,omitempty"` PageWidth int `xml:"pageWidth,attr,omitempty"` PageHeight int `xml:"pageHeight,attr,omitempty"` Tooltips int `xml:"tooltips,attr,omitempty"` Math int `xml:"math,attr,omitempty"` Shadow int `xml:"shadow,attr,omitempty"` Root *mxRoot `xml:"root"` } type mxRoot struct { Cells []*MxCell } type UserObject struct { Label string Link string Tooltip string } type MxCell struct { ID string Value string Style string Vertex bool Edge bool Parent string Source string Target string Geometry *MxGeometry UserObject *UserObject } type MxGeometry struct { X float64 `xml:"x,attr,omitempty"` Y float64 `xml:"y,attr,omitempty"` Width float64 `xml:"width,attr,omitempty"` Height float64 `xml:"height,attr,omitempty"` Relative int `xml:"relative,attr,omitempty"` As string `xml:"as,attr,omitempty"` Points *MxPointArray `xml:"Array,omitempty"` SourcePoint *MxPoint `xml:"-"` TargetPoint *MxPoint `xml:"-"` } type MxPoint struct { X float64 `xml:"x,attr,omitempty"` Y float64 `xml:"y,attr,omitempty"` As string `xml:"as,attr,omitempty"` } type MxPointArray struct { As string `xml:"as,attr,omitempty"` Points []*MxPoint `xml:"mxPoint"` } // MarshalXML customizes geometry output to allow multiple mxPoint children. func (g *MxGeometry) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if start.Name.Local == "" { start.Name.Local = "mxGeometry" } as := g.As if as == "" { as = "geometry" } start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "as"}, Value: as}) if g.X != 0 { start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "x"}, Value: floatToString(g.X)}) } if g.Y != 0 { start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "y"}, Value: floatToString(g.Y)}) } if g.Width != 0 { start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "width"}, Value: floatToString(g.Width)}) } if g.Height != 0 { start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "height"}, Value: floatToString(g.Height)}) } if g.Relative != 0 { start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "relative"}, Value: intToString(g.Relative)}) } if err := enc.EncodeToken(start); err != nil { return err } if g.Points != nil && len(g.Points.Points) > 0 { asVal := g.Points.As if asVal == "" { asVal = "points" } arrStart := xml.StartElement{Name: xml.Name{Local: "Array"}, Attr: []xml.Attr{{Name: xml.Name{Local: "as"}, Value: asVal}}} if err := enc.EncodeToken(arrStart); err != nil { return err } for _, p := range g.Points.Points { if p == nil { continue } if err := enc.EncodeElement(p, xml.StartElement{Name: xml.Name{Local: "mxPoint"}}); err != nil { return err } } if err := enc.EncodeToken(xml.EndElement{Name: arrStart.Name}); err != nil { return err } } if g.SourcePoint != nil { p := *g.SourcePoint if p.As == "" { p.As = "sourcePoint" } if err := enc.EncodeElement(p, xml.StartElement{Name: xml.Name{Local: "mxPoint"}}); err != nil { return err } } if g.TargetPoint != nil { p := *g.TargetPoint if p.As == "" { p.As = "targetPoint" } if err := enc.EncodeElement(p, xml.StartElement{Name: xml.Name{Local: "mxPoint"}}); err != nil { return err } } return enc.EncodeToken(xml.EndElement{Name: start.Name}) } // MarshalXML customizes how the mxRoot emits cells, handling UserObject wrappers. func (r *mxRoot) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { start.Name.Local = "root" if err := enc.EncodeToken(start); err != nil { return err } for _, cell := range r.Cells { if cell == nil { continue } if cell.UserObject != nil { uoStart := xml.StartElement{Name: xml.Name{Local: "UserObject"}} if cell.UserObject.Label != "" { uoStart.Attr = append(uoStart.Attr, xml.Attr{Name: xml.Name{Local: "label"}, Value: cell.UserObject.Label}) } if cell.UserObject.Link != "" { uoStart.Attr = append(uoStart.Attr, xml.Attr{Name: xml.Name{Local: "link"}, Value: cell.UserObject.Link}) } if cell.UserObject.Tooltip != "" { uoStart.Attr = append(uoStart.Attr, xml.Attr{Name: xml.Name{Local: "tooltip"}, Value: cell.UserObject.Tooltip}) } if err := enc.EncodeToken(uoStart); err != nil { return err } if err := enc.EncodeElement(cell.alias(), xml.StartElement{Name: xml.Name{Local: "mxCell"}}); err != nil { return err } if err := enc.EncodeToken(xml.EndElement{Name: uoStart.Name}); err != nil { return err } continue } if err := enc.EncodeElement(cell.alias(), xml.StartElement{Name: xml.Name{Local: "mxCell"}}); err != nil { return err } } return enc.EncodeToken(xml.EndElement{Name: start.Name}) } func (c *MxCell) alias() cellAlias { return cellAlias{ ID: c.ID, Value: c.Value, Style: c.Style, Vertex: boolToAttr(c.Vertex), Edge: boolToAttr(c.Edge), Parent: c.Parent, Source: c.Source, Target: c.Target, Geometry: c.geometryWithDefault(), } } func (c *MxCell) geometryWithDefault() *MxGeometry { if c.Geometry == nil { return nil } g := *c.Geometry if g.As == "" { g.As = "geometry" } if g.Points != nil && g.Points.As == "" { g.Points.As = "points" } return &g } type cellAlias struct { ID string `xml:"id,attr"` Value string `xml:"value,attr,omitempty"` Style string `xml:"style,attr,omitempty"` Vertex string `xml:"vertex,attr,omitempty"` Edge string `xml:"edge,attr,omitempty"` Parent string `xml:"parent,attr,omitempty"` Source string `xml:"source,attr,omitempty"` Target string `xml:"target,attr,omitempty"` Geometry *MxGeometry `xml:"mxGeometry,omitempty"` } func boolToAttr(v bool) string { if v { return "1" } return "" } // rotateGeometry rotates a rectangle around the given pivot. func rotateGeometry(geo *MxGeometry, rotation, cx, cy float64) { if geo == nil || rotation == 0 { return } rads := rotation * math.Pi / 180 cos := math.Cos(rads) sin := math.Sin(rads) x := geo.X - cx y := geo.Y - cy geo.X = x*cos - y*sin + cx geo.Y = x*sin + y*cos + cy } // ToXML renders the graph model to draw.io compatible XML. func (g *MxGraph) ToXML(style, background string, grid, guides bool, width, height float64) (string, error) { model := g.Model model.Style = style model.Background = background if grid { model.Grid = 1 } if guides { model.Guides = 1 } model.GridSize = 10 model.Connect = 1 model.Arrows = 1 model.Fold = 1 model.Page = 1 model.PageScale = 1 model.Dx = 0 model.Dy = 0 if width == 0 { width = 850 } if height == 0 { height = 1100 } model.PageWidth = int(width) model.PageHeight = int(height) model.Tooltips = 1 model.Math = 0 model.Shadow = 0 output, err := xml.Marshal(model) if err != nil { return "", err } return string(output), nil } // NormalizeOrigin shifts all absolute coordinates so the top-left-most point is at (0,0). func (g *MxGraph) NormalizeOrigin() { minX, minY := math.MaxFloat64, math.MaxFloat64 for _, cell := range g.Model.Root.Cells { if cell == nil || cell.Geometry == nil { continue } geo := cell.Geometry if geo.Relative == 0 { if geo.X < minX { minX = geo.X } if geo.Y < minY { minY = geo.Y } } if geo.Points != nil { for _, p := range geo.Points.Points { if p == nil { continue } if p.X < minX { minX = p.X } if p.Y < minY { minY = p.Y } } } if geo.SourcePoint != nil { if geo.SourcePoint.X < minX { minX = geo.SourcePoint.X } if geo.SourcePoint.Y < minY { minY = geo.SourcePoint.Y } } if geo.TargetPoint != nil { if geo.TargetPoint.X < minX { minX = geo.TargetPoint.X } if geo.TargetPoint.Y < minY { minY = geo.TargetPoint.Y } } } if minX == math.MaxFloat64 || minY == math.MaxFloat64 { return } dx, dy := -minX, -minY for _, cell := range g.Model.Root.Cells { if cell == nil || cell.Geometry == nil { continue } geo := cell.Geometry if geo.Relative == 0 { geo.X += dx geo.Y += dy } if geo.Points != nil { for _, p := range geo.Points.Points { if p == nil { continue } p.X += dx p.Y += dy } } if geo.SourcePoint != nil { geo.SourcePoint.X += dx geo.SourcePoint.Y += dy } if geo.TargetPoint != nil { geo.TargetPoint.X += dx geo.TargetPoint.Y += dy } } } // helper to ensure geometry has relative flag set to 1. func setRelative(geo *MxGeometry, v bool) { if geo == nil { return } if v { geo.Relative = 1 } else { geo.Relative = 0 } } // linearly interpolated center used for rotations. func midpoint(p1, p2 *MxPoint) *MxPoint { return &MxPoint{ X: (p1.X + p2.X) / 2, Y: (p1.Y + p2.Y) / 2, } } // joinStyle appends style fragments with trailing semicolon when needed. func joinStyle(parts ...string) string { var sb strings.Builder for _, p := range parts { if p == "" { continue } if !strings.HasSuffix(p, ";") { p += ";" } sb.WriteString(p) } return sb.String() }