// Package d2isometric builds 3D scenes from compiled D2 diagrams for native image // rendering. It composes new geometry without modifying the supplied diagram.
package d2isometric import ( ) const ( MaxNodes = 2000 MaxEdges = 5000 MaxIDBytes = 8192 MaxIDDepth = 128 maxEntries = 100000 maxBytes = 8 << 20 coordinateLimit = 1e9 ) type RenderOpts struct { ThemeID *int64 ThemeOverrides *d2target.ThemeOverrides } type Vec3 struct { X float64 `json:"x"` Y float64 `json:"y"` Z float64 `json:"z"` } type Board struct { ID string `json:"id"` SourceID string `json:"sourceId,omitempty"` Label string `json:"label"` ParentID string `json:"parentId,omitempty"` Kind string `json:"kind"` // Level records authored container depth; presentation lowers supporting plates. Level int `json:"level"` Position Vec3 `json:"position"` Size Vec3 `json:"size"` NodeIDs []string `json:"nodeIds"` HeaderDepth float64 `json:"headerDepth"` } type NodeMetadata struct { Original d2target.Shape `json:"original"` } type Node struct { ID string `json:"id"` Label string `json:"label"` Type string `json:"type"` BoardID string `json:"boardId"` ParentID string `json:"parentId,omitempty"` Container bool `json:"container"` SequenceRole string `json:"sequenceRole,omitempty"` Position Vec3 `json:"position"` Size Vec3 `json:"size"` Fill string `json:"fill"` FillExplicit bool `json:"fillExplicit"` Stroke string `json:"stroke"` StrokeExplicit bool `json:"strokeExplicit"` FontColor string `json:"fontColor"` FontExplicit bool `json:"fontExplicit"` Opacity float64 `json:"opacity"` StrokeWidth int `json:"strokeWidth"` StrokeDash float64 `json:"strokeDash"` Tooltip string `json:"tooltip,omitempty"` Link string `json:"link,omitempty"` Icon string `json:"icon,omitempty"` Metadata NodeMetadata `json:"metadata"` } type EdgeMetadata struct { Original d2target.Connection `json:"original"` } type Edge struct { ID string `json:"id"` Source string `json:"source"` Target string `json:"target"` Label string `json:"label"` SourceLabel *d2target.Text `json:"sourceLabel,omitempty"` TargetLabel *d2target.Text `json:"targetLabel,omitempty"` SourceArrow d2target.Arrowhead `json:"sourceArrow"` TargetArrow d2target.Arrowhead `json:"targetArrow"` Stroke string `json:"stroke"` StrokeExplicit bool `json:"strokeExplicit"` FontColor string `json:"fontColor"` FontExplicit bool `json:"fontExplicit"` StrokeWidth int `json:"strokeWidth"` StrokeDash float64 `json:"strokeDash"` Opacity float64 `json:"opacity"` Link string `json:"link,omitempty"` Tooltip string `json:"tooltip,omitempty"` Icon string `json:"icon,omitempty"` Points []Vec3 `json:"points"` SequenceRole string `json:"sequenceRole,omitempty"` Metadata EdgeMetadata `json:"metadata"` } type Scene struct { FontFamily *d2fonts.FontFamily `json:"fontFamily,omitempty"` MonoFontFamily *d2fonts.FontFamily `json:"monoFontFamily,omitempty"` ThemeID int64 `json:"themeId"` ThemeOverrides *d2target.ThemeOverrides `json:"themeOverrides,omitempty"` HasSequence bool `json:"hasSequence,omitempty"` PixelScale float64 `json:"pixelScale"` Description string `json:"description,omitempty"` Background string `json:"background"` Boards []Board `json:"boards"` Nodes []Node `json:"nodes"` Edges []Edge `json:"edges"` Warnings []string `json:"warnings,omitempty"` Root d2target.Shape `json:"root"` Legend *d2target.Legend `json:"legend,omitempty"` } // BuildScene maps a single D2 board. IDs, text, endpoint direction, and original // shape/edge metadata are retained, including connections to container boards. // The returned scene owns its data and is independent of the input diagram. func ( *d2target.Diagram, *RenderOpts) (*Scene, error) { if == nil { return nil, fmt.Errorf("isometric: nil diagram") } if len(.Shapes) > MaxNodes || len(.Connections) > MaxEdges { return nil, fmt.Errorf("isometric: scene exceeds %d nodes or %d edges", MaxNodes, MaxEdges) } if == nil { = &RenderOpts{} } // Admission precedes encoding, key parsing, graph traversal and allocations. := budget{} for , := range []any{.Name, .Description, .Root, .Legend, .Shapes, .Connections, } { if := .check(reflect.ValueOf()); != nil { return nil, } } for , := range .Shapes { if .Width < 0 || .Height < 0 || .LabelWidth < 0 || .LabelHeight < 0 || .StrokeWidth < 0 || .Opacity < 0 || .Opacity > 1 { return nil, fmt.Errorf("isometric: node %q has invalid dimensions or opacity", .ID) } } for , := range .Connections { if .StrokeWidth < 0 || .Opacity < 0 || .Opacity > 1 { return nil, fmt.Errorf("isometric: edge %q has invalid stroke or opacity", .ID) } for , := range .Route { if == nil { return nil, fmt.Errorf("isometric: edge %q has nil route point", .ID) } } } , := json.Marshal(struct { []d2target.Shape []d2target.Connection d2target.Shape *d2target.Legend }{.Shapes, .Connections, .Root, .Legend}) if != nil { return nil, fmt.Errorf("isometric: invalid diagram: %w", ) } if len() > maxBytes*2 { return nil, fmt.Errorf("isometric: encoded scene metadata exceeds budget") } var struct { []d2target.Shape []d2target.Connection d2target.Shape *d2target.Legend } if = json.Unmarshal(, &); != nil { return nil, } // url.Userinfo has private fields that JSON cannot round-trip. Copy URL // values explicitly so icon identity and ownership also survive userinfo. for := range . { .[].Icon = cloneURL(.Shapes[].Icon) } for := range . { .[].Icon = cloneURL(.Connections[].Icon) } ..Icon = cloneURL(.Root.Icon) if . != nil { for := range ..Shapes { ..Shapes[].Icon = cloneURL(.Legend.Shapes[].Icon) } for := range ..Connections { ..Connections[].Icon = cloneURL(.Legend.Connections[].Icon) } } := int64(0) var *d2target.ThemeOverrides if .Config != nil { if .Config.ThemeID != nil { = *.Config.ThemeID } = .Config.ThemeOverrides } if .ThemeID != nil { = *.ThemeID } if .ThemeOverrides != nil { = .ThemeOverrides } if := .check(reflect.ValueOf()); != nil { return nil, } := d2themescatalog.Find() if .Name == "" { return nil, fmt.Errorf("isometric: unknown theme %d", ) } .ApplyOverrides() := func( string) string { return d2themes.ResolveThemeColor(, ) } := &Scene{PixelScale: SceneScale, Description: .Description, Background: (.Root.Fill), Boards: []Board{}, Nodes: []Node{}, Edges: []Edge{}} .Root = . .Legend = . .ThemeID = if != nil { , := json.Marshal() if != nil { return nil, } if = json.Unmarshal(, &.ThemeOverrides); != nil { return nil, } } if .FontFamily != nil { := *.FontFamily .FontFamily = & } if .MonoFontFamily != nil { := *.MonoFontFamily .MonoFontFamily = & } if .Background == "" { .Background = "#f5f7fa" } := map[string]int{} := map[string]int{} := make([][]string, len(.)) for , := range . { if len(.ID) > MaxIDBytes { return nil, fmt.Errorf("isometric: node ID exceeds %d bytes", MaxIDBytes) } if .ID == "" { return nil, fmt.Errorf("isometric: node %d has empty ID", ) } if , := [.ID]; { return nil, fmt.Errorf("isometric: duplicate node ID %q", .ID) } , := d2parser.ParseKey(.ID) if != nil { return nil, fmt.Errorf("isometric: node ID: %w", ) } if len(.Path) > MaxIDDepth { return nil, fmt.Errorf("isometric: node ID nesting exceeds %d components", MaxIDDepth) } for , := range .Path { [] = append([], .Unbox().ScalarString()) } := pathKey([]) if , := []; { return nil, fmt.Errorf("isometric: equivalent node ID %q", .ID) } [.ID] = [] = := Node{ID: .ID, Label: .Label, Type: .Type, Fill: (.Fill), Stroke: (.Stroke), FontColor: (.GetFontColor()), Opacity: .Opacity, StrokeWidth: .StrokeWidth, StrokeDash: .StrokeDash, Tooltip: .Tooltip, Link: .Link, Metadata: NodeMetadata{Original: }} .FillExplicit = != nil || (.Fill != "" && !color.IsThemeColor(.Fill)) .StrokeExplicit = != nil || (.Stroke != "" && !color.IsThemeColor(.Stroke)) .FontExplicit = != nil || (.Color != "" && !color.IsThemeColor(.Color)) if .Icon != nil { .Icon = .Icon.String() } .Nodes = append(.Nodes, ) } := make([]int, len(.Nodes)) for := range { [] = -1 } for , := range { for := len() - 1; > 0; -- { if , := [pathKey([:])]; { [] = .Nodes[].ParentID = .Nodes[].ID .Nodes[].Container = true break } } } := map[string]bool{} for , := range . { if .ID == "" || [.ID] { return nil, fmt.Errorf("isometric: empty or duplicate edge ID %q", .ID) } [.ID] = true := Edge{ID: .ID, Source: .Src, Target: .Dst, Label: .Label, SourceLabel: .SrcLabel, TargetLabel: .DstLabel, SourceArrow: .SrcArrow, TargetArrow: .DstArrow, Stroke: (.Stroke), FontColor: (.Color), StrokeWidth: .StrokeWidth, StrokeDash: .StrokeDash, Opacity: .Opacity, Link: .Link, Tooltip: .Tooltip, Metadata: EdgeMetadata{Original: }} .StrokeExplicit = != nil || (.Stroke != "" && !color.IsThemeColor(.Stroke)) .FontExplicit = != nil || (.Color != "" && !color.IsThemeColor(.Color)) if .Icon != nil { .Icon = .Icon.String() } .Edges = append(.Edges, ) , := [.Src] , := [.Dst] if (! || !) && len(.Route) < 2 { return nil, fmt.Errorf("isometric: edge %q has an unknown endpoint and no usable original route", .ID) } } classifySequence(, , ) if := composeHierarchy(, , ); != nil { return nil, } if len(.Layers)+len(.Scenarios)+len(.Steps) > 0 { .Warnings = append(.Warnings, "This scene contains the selected D2 board. The export pipeline renders other layers, scenarios and steps separately.") } return , nil } func pathKey( []string) string { , := json.Marshal(); return string() } func cloneURL( *url.URL) *url.URL { if == nil { return nil } := * if .User != nil { := *.User .User = & } return & } type budget struct{ bytes, entries int } func ( *budget) ( reflect.Value) error { if !.IsValid() { return nil } switch .Kind() { case reflect.Pointer, reflect.Interface: if !.IsNil() { return .(.Elem()) } case reflect.String: if !utf8.ValidString(.String()) { return fmt.Errorf("isometric: invalid UTF-8 text") } .bytes += len(.String()) if .bytes > maxBytes { return fmt.Errorf("isometric: text exceeds %d bytes", maxBytes) } case reflect.Float32, reflect.Float64: if math.IsNaN(.Float()) || math.IsInf(.Float(), 0) || math.Abs(.Float()) > coordinateLimit { return fmt.Errorf("isometric: nonfinite or excessive numeric value") } case reflect.Int, reflect.Int64, reflect.Int32: if .Int() < -int64(coordinateLimit) || .Int() > int64(coordinateLimit) { return fmt.Errorf("isometric: excessive numeric value") } case reflect.Slice, reflect.Array: .entries += .Len() if .entries > maxEntries { return fmt.Errorf("isometric: collection entries exceed %d", maxEntries) } for := 0; < .Len(); ++ { if := .(.Index()); != nil { return } } case reflect.Struct: for := 0; < .NumField(); ++ { if := .(.Field()); != nil { return } } } return nil } func sourceOrder( []Node, []int) { sort.SliceStable(, func(, int) bool { , := [[]].Metadata.Original, [[]].Metadata.Original if .Pos.Y != .Pos.Y { return .Pos.Y < .Pos.Y } if .Pos.X != .Pos.X { return .Pos.X < .Pos.X } return strings.Compare(.ID, .ID) < 0 }) }