// Package d2scenebuild converts D2's typed export model into the immutable // scene consumed by d2raster.
package d2scenebuild import ( ) const DefaultPadding = 100 type AssetOptions struct { // Resolver owns bounded image loading for this scene build. // [imageasset.ErrUnavailable] substitutes the built-in missing-image // placeholder; every other error aborts the build. Resolver *imageasset.Resolver SVGImportLimits d2svgimport.Limits SVGImportBudget SVGImportBudget } // SVGImportBudget is a document-wide ceiling on parse and emitted-scene work // across all distinct imported SVG assets. SVGImportLimits independently // bounds the transient parse/compile work of each asset. type SVGImportBudget struct { MaxSourceBytes int MaxElements int MaxAttributes int MaxAttributeBytes int MaxPathCommands int MaxTransformFunctions int MaxDeclaredResources int MaxExpandedUseInstances int } // LinkBudget is a document-wide ceiling on link/tooltip metadata and, when // Appendix is enabled, its corresponding static badges and row strings. // Callers rendering a diagram that contains such metadata must provide // positive limits; diagrams without metadata require no link budget. type LinkBudget struct { MaxRegions int MaxStringBytes int } // FontFallbackOptions bounds all primary/custom and resolved fallback font // assets retained by the scene. The resolver performs the explicit I/O phase // for code points absent from D2's configured fonts; d2raster never receives // it and performs no filesystem discovery while painting. type FontFallbackOptions struct { Resolver d2fonts.FallbackResolver MaxAssets int MaxBytes int64 MaxRunesPerText int MaxTotalRunes int MaxCoverageChecks int64 // MaxFontFacesPerText, MaxShapingRuns, and MaxShapedGlyphs bound the // pure-Go shaping pass that materializes renderer-independent Glyphs after // fallback resolution. Zero selects bounded defaults. MaxFontFacesPerText int MaxShapingRuns int MaxShapedGlyphs int } // SketchBudget is a document-wide ceiling on structured rough-go output. // MaxOperationSets and MaxOperations bound the generated intermediate work; // MaxPathCommands independently bounds the typed scene geometry retained by // the document. Callers enabling Sketch for paintable sketch geometry must // provide positive limits. type SketchBudget struct { MaxOperationSets int MaxOperations int MaxPathCommands int } // Options configures scene construction. Scale changes the logical output // dimensions, while the raster device scale is supplied to d2raster // separately. type Options struct { Pad *int64 Scale *float64 Center *bool ThemeID *int64 ThemeOverrides *d2target.ThemeOverrides // MaxNodes and MaxPathCommands enable an early, target-level admission // guard when positive. The guard rejects only when the target already // proves that the eventual scene must exceed a downstream renderer limit; // d2raster remains responsible for exact accounting of expanded geometry. MaxNodes int MaxPathCommands int Sketch bool SketchBudget SketchBudget LinkBudget LinkBudget // Appendix paints the numbered tooltip/link badges and static appendix // rows used by PNG, GIF, PDF, and PPTX exports. Interactive SVG callers // leave it false and retain typed metadata without static footnotes. Its // work is bounded by LinkBudget. Appendix bool // Assets configures the explicit, bounded I/O phase for image and // icon sources. Build stores only immutable bytes or imported vector scene // nodes in the returned document; d2raster never receives this resolver. Assets *AssetOptions Fonts *FontFallbackOptions } type builder struct { ctx context.Context diagram *d2target.Diagram options Options theme d2themes.Theme assets map[d2scene.AssetID]d2scene.Asset assetIDs map[[32]byte]d2scene.AssetID sourceAssetIDs map[string]d2scene.AssetID latexAssets map[latexAssetKey]latexAsset markdownRuler *textmeasure.Ruler markdownRulerErr error svgSourceBytes int svgElements int svgAttributes int svgAttributeBytes int svgCommands int svgTransforms int svgDeclaredResources int svgExpandedUseInstances int sketchOperationSets int sketchOperations int sketchPathCommands int sketchStreakPatterns map[string]d2scene.PatternPaint sketchStreakCommands []d2scene.PathCommand idToShape map[string]d2target.Shape // connectionMask is shared by every connection geometry node so every route // and marker is cut beneath every connection label and ordinary shape border // label. connectionMask *d2scene.Mask links []d2scene.LinkRegion linkBytes int appendixItems []appendixItem appendixStringBytes int fontRunes int fontCoverageChecks int64 fontShapeRuns int fontGlyphs int fontFaces map[d2scene.AssetID]*fontface.ParsedFace codeTokens codeTokenBudget } // Build preflights diagram features and returns a fully resolved, network-free // scene. It never emits a partial scene on error. func ( context.Context, *d2target.Diagram, Options) (*d2scene.Document, error) { if == nil { return nil, fmt.Errorf("scene: nil diagram") } if == nil { = context.Background() } if := .Err(); != nil { return nil, } if .Scale != nil && (!finite(*.Scale) || *.Scale <= 0) { return nil, invalidField("options", "scale", *.Scale, "must be finite and greater than zero") } if .MaxNodes < 0 { return nil, invalidField("options", "maxNodes", .MaxNodes, "must be non-negative") } if .MaxPathCommands < 0 { return nil, invalidField("options", "maxPathCommands", .MaxPathCommands, "must be non-negative") } if .Sketch { if := validateSketchBudget(.SketchBudget); != nil { return nil, } } if len(.Layers) != 0 || len(.Scenarios) != 0 || len(.Steps) != 0 { return nil, unsupported("diagram", "nested boards in a single scene") } := d2themescatalog.NeutralDefault.ID if .ThemeID != nil { = *.ThemeID } := d2themescatalog.Find() if == (d2themes.Theme{}) { return nil, fmt.Errorf("scene: unknown theme %d", ) } .ApplyOverrides(.ThemeOverrides) if := admitSceneWork(, , ); != nil { return nil, } := &builder{ ctx: , diagram: , options: , theme: , assets: make(map[d2scene.AssetID]d2scene.Asset), assetIDs: make(map[[32]byte]d2scene.AssetID), sourceAssetIDs: make(map[string]d2scene.AssetID), latexAssets: make(map[latexAssetKey]latexAsset), idToShape: make(map[string]d2target.Shape, len(.Shapes)), fontFaces: make(map[d2scene.AssetID]*fontface.ParsedFace), codeTokens: newCodeTokenBudget(.MaxNodes), } for , := range .Shapes { if := .Err(); != nil { return nil, } .idToShape[.ID] = } if := .preflight(); != nil { return nil, } if := .preflightAssets(); != nil { return nil, } , := .buildDocument() if != nil { return nil, } return , nil } // Every built document has a root and background; every target shape has a // group; and every connection has both a group and route node. Checking that // allocation-free lower bound, together with each route's minimum command // count, keeps giant targets out of the shape index and the later diagramObject // allocation/sort. More elaborate scene features are deliberately left to // d2raster's exact traversal. func admitSceneWork( context.Context, *d2target.Diagram, Options) error { if .MaxNodes > 0 { := .MaxNodes for , := range [...]int{2, len(.Shapes), len(.Connections), len(.Connections)} { if > { return fmt.Errorf("scene: node count exceeds limit %d", .MaxNodes) } -= } } if .MaxPathCommands == 0 { return nil } := .MaxPathCommands for := range .Connections { if := .Err(); != nil { return } := &.Connections[] // Leave malformed route structure to the detailed preflight below. A // valid straight route emits at least one command per point; a valid // cubic route emits one move plus one command per three controls. if len(.Route) < 2 || .IsCurve && (len(.Route)-1)%3 != 0 { continue } := len(.Route) if .IsCurve { = 1 + (len(.Route)-1)/3 } if > { return fmt.Errorf("scene: path command count exceeds limit %d", .MaxPathCommands) } -= } return nil } func ( *builder) () error { if := .ctx.Err(); != nil { return } := .diagram.Root if := validateShapeNumbers("root", ); != nil { return } if := validateFillPattern("root", .FillPattern); != nil { return } if .ThreeDee { return unsupported("root", "3d effect") } if .Multiple { return unsupported("root", "multiple effect") } if .Shadow || .Animated || .Blend { return unsupported("root", "effect") } if .Link != "" || .Tooltip != "" || .PrettyLink != "" || .TooltipPosition != "" { return unsupported("root", "link/tooltip metadata") } for , := range .diagram.Shapes { if := .preflightShape(); != nil { return } } for , := range .diagram.Connections { if := .preflightConnection(); != nil { return } } if := .preflightLegend(); != nil { return } if := .preflightAppendix(); != nil { return } return .compileLinkRegions() } func ( *builder) ( d2target.Shape) error { if := .ctx.Err(); != nil { return } := fmt.Sprintf("shape %q", .ID) if := validateShapeNumbers(, ); != nil { return } if := validateFillPattern(, .FillPattern); != nil { return } if := validateShapeEffects(, ); != nil { return } if .Type == d2target.ShapeImage && .Icon == nil { return invalidField(, "icon", nil, "must identify an image source for an image shape") } // Keep every source-independent layout error ahead of preflightAssets. A // malformed target must not trigger local file or network reads merely // because it also carries an icon. if .Opacity != 0 { switch .Type { case d2target.ShapeClass, d2target.ShapeSQLTable: if := validateStructuredShape(); != nil { return } case d2target.ShapeCode: if := validateCodeShape(); != nil { return } } if .Icon != nil && .Type != d2target.ShapeImage { := label.FromString(.IconPosition) if !.IsShapePosition() { return invalidField(, "iconPosition", .IconPosition, "must identify a supported icon position") } } } if shapeAssetIsEmitted() && (.options.Assets == nil || .options.Assets.Resolver == nil) { return unsupported(, "image/icon asset without a configured resolver") } if .TooltipPosition != "" { if .Tooltip == "" { return invalidField(, "tooltipPosition", .TooltipPosition, "requires a non-empty tooltip") } if := validateMarkdownLabel(+" positioned tooltip", .Tooltip, d2fonts.FONT_SIZE_M, 1, 1); != nil { return } } if .Language != "" { switch .Language { case "latex": if .Type == d2target.ShapeCode || .Type == d2target.ShapeClass || .Type == d2target.ShapeSQLTable { return unsupported(, "label language latex on a structured or code shape") } if := .validateLatexInput(, .Label); != nil { return } if := .validateLatexImportOptions(); != nil { return } case "markdown": if .Type == d2target.ShapeCode || .Type == d2target.ShapeClass || .Type == d2target.ShapeSQLTable { return unsupported(, "label language markdown on a structured or code shape") } if := validateMarkdownLabel(, .Label, .FontSize, .LabelWidth, .LabelHeight); != nil { return } default: if .Type == d2target.ShapeClass || .Type == d2target.ShapeSQLTable { return unsupported(, "label language "+.Language+" on a structured shape") } if := validateCodeText(, .Text); != nil { return } } } return nil } func ( *builder) ( d2target.Connection) error { if := .ctx.Err(); != nil { return } := fmt.Sprintf("connection %q", .ID) if := validateConnectionNumbers(, ); != nil { return } if len(.Route) < 2 { return invalidField(, "route", len(.Route), "must contain at least two points") } if .IsCurve && (len(.Route)-1)%3 != 0 { return invalidField(, "route", len(.Route), "must contain 1+3n points for a cubic connection") } for , := range .Route { if := .ctx.Err(); != nil { return } := fmt.Sprintf("route[%d]", ) if == nil { return fmt.Errorf("scene: %s field %q must not be nil", , ) } if !finite(.X) { return invalidField(, +".x", .X, "must be finite") } if !finite(.Y) { return invalidField(, +".y", .Y, "must be finite") } if := validateRouteCoordinate(, +".x", .X, .StrokeWidth); != nil { return } if := validateRouteCoordinate(, +".y", .Y, .StrokeWidth); != nil { return } if > 0 { := .Route[-1] , := .X-.X, .Y-.Y if !finite() || !finite() || !finite(geo.NewVector(, ).Length()) { return invalidField(, fmt.Sprintf("route[%d:%d]", -1, ), nil, "must have a finite segment length") } } } if .SrcArrow != d2target.NoArrowhead && .Route[0].VectorTo(.Route[1]).Length() == 0 { return invalidField(, "route", nil, "must have a non-zero source arrowhead tangent") } := len(.Route) - 1 if .DstArrow != d2target.NoArrowhead && .Route[-1].VectorTo(.Route[]).Length() == 0 { return invalidField(, "route", nil, "must have a non-zero destination arrowhead tangent") } if .Animated { if := validateAnimatedConnection(, ); != nil { return } } if (.Tooltip != "" || .Link != "") && .Label == "" { return unsupported(, "link/tooltip metadata without a label (the box-only link representation cannot encode route hit geometry)") } if .Label != "" { := label.FromString(.LabelPosition) if !.IsEdgePosition() { return invalidField(, "labelPosition", .LabelPosition, "must identify a connection label position") } := .Language == "markdown" && (.LabelWidth == 0 || .LabelHeight == 0) if .Language != "latex" && .FontSize <= 0 && ! { return invalidField(, "fontSize", .FontSize, "must be positive for a connection with a label") } if .Language != "markdown" && (.LabelWidth <= 0 || .LabelHeight <= 0) { return invalidField(, "labelDimensions", fmt.Sprintf("%dx%d", .LabelWidth, .LabelHeight), "must be positive for a connection with a label") } } if .Icon != nil { if .Label == "" && !label.FromString(.IconPosition).IsEdgePosition() { return invalidField(, "iconPosition", .IconPosition, "must identify a connection icon position") } := .GetIconPosition() if == nil || !finite(.X) || !finite(.Y) { return invalidField(, "iconPosition", .IconPosition, "must resolve to a finite point on the route") } if .options.Assets == nil || .options.Assets.Resolver == nil { return unsupported(, "icon asset without a configured resolver") } } for , := range []struct { string *d2target.Text }{{: "srcLabel", : .SrcLabel}, {: "dstLabel", : .DstLabel}} { := . if == nil || .Label == "" { continue } if .FontSize <= 0 { return invalidField(, "fontSize", .FontSize, "must be positive for an arrowhead label") } if .LabelWidth <= 0 || .LabelHeight <= 0 { return invalidField(, .+"Dimensions", fmt.Sprintf("%dx%d", .LabelWidth, .LabelHeight), "must be positive for an arrowhead label") } } if .Language == "latex" { if := .validateLatexInput(, .Label); != nil { return } if := .validateLatexImportOptions(); != nil { return } } else if .Language == "markdown" { if := validateMarkdownLabel(, .Label, .FontSize, .LabelWidth, .LabelHeight); != nil { return } } else if .Language != "" { if := validateCodeText(, .Text); != nil { return } } for , := range []d2target.Arrowhead{.SrcArrow, .DstArrow} { switch { case d2target.NoArrowhead, d2target.ArrowArrowhead, d2target.TriangleArrowhead, d2target.UnfilledTriangleArrowhead, d2target.LineArrowhead, d2target.FilledDiamondArrowhead, d2target.DiamondArrowhead, d2target.FilledCircleArrowhead, d2target.CircleArrowhead, d2target.CrossArrowhead, d2target.FilledBoxArrowhead, d2target.BoxArrowhead, d2target.CfOne, d2target.CfMany, d2target.CfOneRequired, d2target.CfManyRequired: default: return unsupported(, "arrowhead "+string()) } } return nil } func ( *builder) () (*d2scene.Document, error) { , := .diagram.BoundingBox() , := .visualBoundingBox() if .options.Appendix { // On-shape badges are part of the base viewbox before the appendix block // is appended. , = , } if := .ctx.Err(); != nil { return nil, } // Legend placement uses the unfiltered diagram bounds. visualBoundingBox // removes appendix-only metadata because scenes do not paint those icons. , := .measureLegend(, ) if != nil { return nil, } := int64(DefaultPadding) if .options.Pad != nil { = *.options.Pad } , , , , := dimensions(, , ) if != nil { return nil, } , , , , = .expandViewBox(, , , , ) if != nil { return nil, } , := .paint(.diagram.Root.Fill, "root fill") if != nil { return nil, } , := .stroke(.diagram.Root.Stroke, .diagram.Root.StrokeWidth, .diagram.Root.StrokeDash, d2scene.CapButt, d2scene.JoinMiter, "root stroke") if != nil { return nil, } := int(math.Ceil(float64(.diagram.Root.StrokeWidth) / 2)) , , , , := expandDimensions(, , , , ) if ! { return nil, invalidPaddingError() } , , , , = expandDimensions(, , , , ) if ! || < 0 || < 0 { return nil, invalidPaddingError() } , := .builtinPattern(.diagram.Root.FillPattern) if != nil { return nil, fmt.Errorf("scene: root fill pattern: %w", ) } := d2scene.NewNode(nil) .ID = "root" := var *d2scene.Node if .diagram.Root.DoubleBorder { , , , , := expandDimensions(, , , , +d2target.INNER_BORDER_OFFSET) if ! { return nil, invalidPaddingError() } , , , , = expandDimensions(, , , , ) if ! || < 0 || < 0 { return nil, invalidPaddingError() } := clampBorderRadius(float64(.diagram.Root.BorderRadius), float64(), float64()) := d2scene.NewNode(d2scene.Rect{ Box: d2scene.Box{ X: float64(), Y: float64(), Width: float64(), Height: float64(), }, RadiusX: , RadiusY: , Fill: , Stroke: , }) .ID = "root:double-border:outer" .Children = append(.Children, ) = if != nil { , := overlayPatternNode(, , "root:double-border:outer:fill-pattern") if != nil { return nil, } .Children = append(.Children, ) } , = .paint("transparent", "root double-border inner fill") if != nil { return nil, } } := clampBorderRadius(float64(.diagram.Root.BorderRadius), float64(), float64()) := d2scene.NewNode(d2scene.Rect{ Box: d2scene.Box{ X: float64(), Y: float64(), Width: float64(), Height: float64(), }, RadiusX: , RadiusY: , Fill: , Stroke: , }) .ID = "root:background" .Children = append(.Children, ) if == nil { = } if != nil { , := overlayPatternNode(, , "root:background:fill-pattern") if != nil { return nil, } .Children = append(.Children, ) } , := .buildConnectionMask(d2scene.Box{ X: float64(), Y: float64(), Width: float64(), Height: float64(), }) if != nil { return nil, } .connectionMask = := make([]diagramObject, 0, len(.diagram.Shapes)+len(.diagram.Connections)) for , := range .diagram.Shapes { = append(, diagramObject{shape: &.diagram.Shapes[], z: .ZIndex, level: .Level, shapeObject: true, order: }) } for , := range .diagram.Connections { = append(, diagramObject{connection: &.diagram.Connections[], z: .ZIndex, order: len(.diagram.Shapes) + }) } sort.SliceStable(, func(, int) bool { if [].z != [].z { return [].z < [].z } if [].shapeObject && [].shapeObject && [].level != [].level { return [].level < [].level } if [].shapeObject != [].shapeObject { return [].shapeObject } return [].order < [].order }) // Build ordinary objects before measuring the appendix. Markdown rendering // discovers link-title regions while it constructs its typed primitives; // delaying appendix measurement is what makes those titles part of the // static raster rather than silently dropping them from paged formats. := make([]*d2scene.Node, len()) := make([]*d2scene.Node, len()) for , := range { if := .ctx.Err(); != nil { return nil, } var *d2scene.Node if .shape != nil { , = .buildShape(*.shape) } else { , = .buildConnection(*.connection) } if != nil { return nil, } [] = if .shape != nil { [], = .buildPositionedTooltip(*.shape) if != nil { return nil, } } } , := .measureAppendix(, ) if != nil { return nil, } , , , , = .expandViewBox(, , , ) if != nil { return nil, } if != nil { // appendix.Append rewrites only the first rendered background rectangle's // width and height. Preserve its x/y and the svg double-border/pattern // quirk instead of resizing every root paint layer. , := .Primitive.(d2scene.Rect) if ! { return nil, fmt.Errorf("scene: appendix background is not a rectangle") } .Box.Width = float64() .Box.Height = float64() .Primitive = } .Children = append(.Children, ...) , := appendixIconNumbers(.diagram) var []*d2scene.Node for , := range { if := .ctx.Err(); != nil { return nil, } if .shape != nil { if [] != nil { = append(, []) } , := .buildAppendixIcons( , *.shape, .order, [.order], [.order], ) if != nil { return nil, } = append(, ...) } } // Paint positioned tooltips and appendix badges above every ordinary object, // preserving sorted shape order and each shape's tooltip-before-link order. .Children = append(.Children, ...) , := .buildLegend() if != nil { return nil, } if != nil { // Paint the complete legend after ordinary objects and appendix visuals, // independently of object z-index. .Children = append(.Children, ) } , := .buildAppendix() if != nil { return nil, } if != nil { // Paint the appendix separator and rows above both badges and the legend. .Children = append(.Children, ) } , := .sceneTextReferences() if != nil { return nil, } if := .resolveFontFallbacks(); != nil { return nil, } if := .shapeTextRuns(); != nil { return nil, } := d2scene.NewDocument(d2scene.Box{ X: float64(), Y: float64(), Width: float64(), Height: float64(), }, ) .Assets = .assets .Links = append([]d2scene.LinkRegion(nil), .links...) .ViewportFit = d2scene.ViewportMeet if .options.Center != nil && *.options.Center { .ViewportAlign = d2scene.ViewportAlignXMidYMid } := 1.0 if .options.Scale != nil { = *.options.Scale } if != nil { // SVG exports with appendix items use the expanded viewbox dimensions // for their outer width and height, without applying the requested scale. = 1 } := * float64() := * float64() if !finite() || !finite() || <= 0 || <= 0 { return nil, invalidField("options", "scale", , "produces non-finite or non-positive logical dimensions") } .LogicalWidth = math.Ceil() .LogicalHeight = math.Ceil() return , nil } type diagramObject struct { shape *d2target.Shape connection *d2target.Connection z int level int shapeObject bool order int } func ( *builder) ( d2target.Shape) (*d2scene.Node, error) { , := d2themes.ShapeTheme() , := .paint(, fmt.Sprintf("shape %q fill", .ID)) if != nil { return nil, } , := .stroke(, .StrokeWidth, .StrokeDash, d2scene.CapButt, d2scene.JoinRound, fmt.Sprintf("shape %q stroke", .ID)) if != nil { return nil, } := d2scene.NewNode(nil) .ID = .ID .Classes = append([]string(nil), .Classes...) .Opacity = .Opacity if .Opacity == 0 { return , nil } if .Animated { configureAnimatedShape() } if .Type == d2target.ShapeCode { , := .buildCodeShape(, ) if != nil { return nil, } .appendShapeGeometry(, , nil) , := .buildShapeIcon(, false) if != nil { return nil, } if != nil { .Children = append(.Children, ) } .Children = append(.Children, ...) return .finishShape(, ) } if .Type == d2target.ShapeClass || .Type == d2target.ShapeSQLTable { var []*d2scene.Node if .options.Sketch { , = .buildSketchStructuredShape(, , ) } else { , = .buildStructuredShape(, , ) } if != nil { return nil, } if !.options.Sketch { , := .builtinPattern(.FillPattern) if != nil { return nil, fmt.Errorf("scene: shape %q fill pattern: %w", .ID, ) } , = .interleavePattern(, , .ID, structuredPatternNode(.ID)) if != nil { return nil, } } , := .buildShapeIcon(, true) if != nil { return nil, } if != nil { = append(, ) } .appendShapeGeometry(, , ) return .finishShape(, ) } var []*d2scene.Node if .Type == d2target.ShapeImage { , := .buildShapeImage() if != nil { return nil, } = append(, ) } else if .ThreeDee || .Multiple || .DoubleBorder { var []*d2scene.Node if .options.Sketch { , = .buildSketchShapeEffects(, , ) } else { , = .buildShapeEffects(, , ) } if != nil { return nil, } if !.options.Sketch { , := .builtinPattern(.FillPattern) if != nil { return nil, fmt.Errorf("scene: shape %q fill pattern: %w", .ID, ) } , = .interleavePattern(, , .ID, effectPatternNode()) if != nil { return nil, } } = append(, ...) } else { var []*d2scene.Node if .options.Sketch { , = .buildSketchOrdinaryShape(, "") } else { , = .buildOrdinaryShapeOutline(, , , "") } if != nil { return nil, } if len() != 0 && !.options.Sketch { , := .builtinPattern(.FillPattern) if != nil { return nil, fmt.Errorf("scene: shape %q fill pattern: %w", .ID, ) } , = .interleavePattern(, , .ID, ordinaryPatternNode) if != nil { return nil, } } = append(, ...) } .appendShapeGeometry(, , ) , := .buildShapeIcon(, false) if != nil { return nil, } if != nil { .Children = append(.Children, ) } if .Label != "" { var []*d2scene.Node if .Language != "" && .Language != "latex" && .Language != "markdown" { , = .buildCodeShape(, ) } else { , = .buildShapeText() } if != nil { return nil, } .Children = append(.Children, ...) } return .finishShape(, ) } func ( *builder) ( d2target.Shape) ([]*d2scene.Node, error) { if .Language == "latex" { := latexShapeLabelTopLeft() , := .buildLatexLabelNode( fmt.Sprintf("shape %q", .ID), .ID+":label:0", .Label, .Stroke, , ) if != nil { return nil, } return []*d2scene.Node{}, nil } if .Language == "markdown" { return .buildShapeMarkdown() } , := shapeLabelPlacement() , := .font(.Text) if != nil { return nil, fmt.Errorf("scene: shape %q: %w", .ID, ) } , := .paint(.GetFontColor(), fmt.Sprintf("shape %q label color", .ID)) if != nil { return nil, } := strings.Split(.Label, "\n") := make([]*d2scene.Node, 0, len()+1) if .LabelFill != "" { , := .paint(.LabelFill, fmt.Sprintf("shape %q label fill", .ID)) if != nil { return nil, } := d2scene.NewNode(d2scene.Rect{ Box: d2scene.Box{X: .X, Y: .Y, Width: float64(.LabelWidth), Height: float64(.LabelHeight)}, Fill: , }) .ID = .ID + ":label-fill" = append(, ) } := float64(.LabelHeight) / float64(len()) for , := range { := .Y + float64(.FontSize) if > 0 { += float64() * } := d2scene.TextRun{ Text: , Origin: d2scene.Point{X: .X + float64(.LabelWidth)/2, Y: }, Anchor: d2scene.AnchorMiddle, Font: , Fill: , Underline: .Underline, Ink: d2scene.NewBounds(.X, .Y+float64()*, .X+float64(.LabelWidth), .Y+float64(+1)*), } := d2scene.NewNode() .ID = fmt.Sprintf("%s:label:%d", .ID, ) = append(, ) } return , nil } func targetGeometry( d2target.Shape) shape.Shape { := d2target.DSL_SHAPE_TO_SHAPE_TYPE[.Type] := shape.NewShape(, geo.NewBox( geo.NewPoint(float64(.Pos.X), float64(.Pos.Y)), float64(.Width), float64(.Height), )) if .Type == d2target.ShapeCloud && .ContentAspectRatio != nil { .SetInnerBoxAspectRatio(*.ContentAspectRatio) } return } func scenePath( []svg.PathCommand, d2scene.Paint, *d2scene.Stroke) (d2scene.Path, error) { := d2scene.Path{Fill: , Stroke: } .Commands = make([]d2scene.PathCommand, 0, len()) for , := range { switch .Kind { case svg.PathCommandMove: .Commands = append(.Commands, d2scene.MoveTo(.End.X, .End.Y)) case svg.PathCommandLine: .Commands = append(.Commands, d2scene.LineTo(.End.X, .End.Y)) case svg.PathCommandCubic: .Commands = append(.Commands, d2scene.CubicTo( .Control1.X, .Control1.Y, .Control2.X, .Control2.Y, .End.X, .End.Y, )) case svg.PathCommandClose: .Commands = append(.Commands, d2scene.ClosePath()) default: return d2scene.Path{}, fmt.Errorf("unknown typed command %d at index %d", .Kind, ) } } return , nil } func ( *builder) ( d2target.Text) (d2scene.Font, error) { := d2fonts.SourceSansPro if .diagram.FontFamily != nil { = *.diagram.FontFamily } switch { case .FontFamily == "", strings.EqualFold(.FontFamily, "default"): // DEFAULT is a role, not a concrete bundled font. Keep the diagram's // configured primary family when it has one. case strings.EqualFold(.FontFamily, "mono"): = d2fonts.SourceCodePro if .diagram.MonoFontFamily != nil { = *.diagram.MonoFontFamily } default: if , := d2fonts.D2_FONT_TO_FAMILY[strings.ToLower(.FontFamily)]; { = } } := d2fonts.FONT_STYLE_REGULAR if .Bold { = d2fonts.FONT_STYLE_BOLD } else if .Italic { = d2fonts.FONT_STYLE_ITALIC } := d2fonts.Font{Family: , Style: } , := d2fonts.FontFaces.Lookup() if ! { return d2scene.Font{}, fmt.Errorf("font %s/%s is not loaded", , ) } := d2scene.AssetID("font:" + string() + ":" + string()) if , := .assets[]; ! { .assets[] = d2scene.FontAsset{MIMEType: "font/ttf", Data: retainedFontBytes()} } := 400 if == d2fonts.FONT_STYLE_BOLD { = 700 } else if == d2fonts.FONT_STYLE_SEMIBOLD { = 600 } return d2scene.Font{Family: string(), Style: string(), Weight: , Size: float64(.FontSize), Asset: }, nil } func retainedFontBytes( []byte) []byte { return append([]byte(nil), ...) } func unsupported(, string) error { return fmt.Errorf("scene: %s uses unsupported %s", , ) } func finite( float64) bool { return !math.IsNaN() && !math.IsInf(, 0) } func clampBorderRadius(, , float64) float64 { return math.Max(0, math.Min(, math.Min(/2, /2))) } func invalidField(, string, any, string) error { if == nil { return fmt.Errorf("scene: %s field %q %s", , , ) } return fmt.Errorf("scene: %s field %q has invalid value %v: %s", , , , ) } func validateShapeNumbers( string, d2target.Shape) error { for , := range []struct { string int }{ {: "width", : .Width}, {: "height", : .Height}, {: "strokeWidth", : .StrokeWidth}, {: "borderRadius", : .BorderRadius}, {: "iconBorderRadius", : .IconBorderRadius}, } { if . < 0 { return invalidField(, ., ., "must be non-negative") } } if := validateOpacity(, .Opacity); != nil { return } if := validateDash(, .StrokeDash, .StrokeWidth); != nil { return } if .ContentAspectRatio != nil && (!finite(*.ContentAspectRatio) || *.ContentAspectRatio <= 0) { return invalidField(, "contentAspectRatio", *.ContentAspectRatio, "must be finite and greater than zero") } if := validateTextNumbers(, "", .Text); != nil { return } return validateShapeIntegerBounds(, ) } func validateConnectionNumbers( string, d2target.Connection) error { if .StrokeWidth < 0 { return invalidField(, "strokeWidth", .StrokeWidth, "must be non-negative") } if := validateOpacity(, .Opacity); != nil { return } if := validateDash(, .StrokeDash, .StrokeWidth); != nil { return } if !finite(.BorderRadius) || .BorderRadius < 0 { return invalidField(, "borderRadius", .BorderRadius, "must be finite and non-negative") } if !finite(.IconBorderRadius) || .IconBorderRadius < 0 { return invalidField(, "iconBorderRadius", .IconBorderRadius, "must be finite and non-negative") } if !finite(.LabelPercentage) || .LabelPercentage < 0 || .LabelPercentage > 1 { return invalidField(, "labelPercentage", .LabelPercentage, "must be finite and within [0,1]") } if := validateTextNumbers(, "", .Text); != nil { return } if .SrcLabel != nil { if := validateTextNumbers(, "srcLabel.", *.SrcLabel); != nil { return } } if .DstLabel != nil { if := validateTextNumbers(, "dstLabel.", *.DstLabel); != nil { return } } return nil } func validateOpacity( string, float64) error { if !finite() || < 0 || > 1 { return invalidField(, "opacity", , "must be finite and within [0,1]") } return nil } func validateDash( string, float64, int) error { if !finite() || < 0 { return invalidField(, "strokeDash", , "must be finite and non-negative") } if > 0 && > 0 { , := svg.GetStrokeDashAttributes(float64(), ) if !finite() || !finite() || < 0 || < 0 { return invalidField(, "strokeDash", , "must produce finite non-negative dash lengths with strokeWidth") } } return nil } func validateTextNumbers(, string, d2target.Text) error { if .FontSize < 0 { return invalidField(, +"fontSize", .FontSize, "must be non-negative") } if .LabelWidth < 0 { return invalidField(, +"labelWidth", .LabelWidth, "must be non-negative") } if .LabelHeight < 0 { return invalidField(, +"labelHeight", .LabelHeight, "must be non-negative") } return nil } func validateShapeIntegerBounds( string, d2target.Shape) error { := int64(^uint(0) >> 1) := - - 1 := int64(.StrokeWidth/2 + .StrokeWidth%2) for , := range []struct { string int int }{ {: "pos.x", : .Pos.X, : .Width}, {: "pos.y", : .Pos.Y, : .Height}, } { if , := checkedSub(int64(.), , , ); ! { return invalidField(, ., ., "must fit raster bounds arithmetic with strokeWidth") } , := checkedAdd(int64(.), int64(.), , ) if ! { return invalidField(, ., ., "must fit raster bounds arithmetic with its dimension") } if , := checkedAdd(, , , ); ! { return invalidField(, ., ., "must fit raster bounds arithmetic with strokeWidth") } } return nil } func validateRouteCoordinate(, string, float64, int) error { := int64(^uint(0) >> 1) := - - 1 := math.Ceil(float64() / 2) // BoundingBox floors/ceils route coordinates and converts them to int. // Strict comparisons avoid the rounded float64 representation of MaxInt. if math.Floor()- <= float64() || math.Ceil()+ >= float64() { return invalidField(, , , "must fit raster integer bounds arithmetic") } return nil } func invalidPaddingError( int64) error { return fmt.Errorf("padding %d produces invalid scene dimensions", ) } func dimensions(, d2target.Point, int64) (, , , int, error) { := int64(^uint(0) >> 1) := - - 1 if < || > { return 0, 0, 0, 0, invalidPaddingError() } , := checkedAdd(, , , ) if ! { return 0, 0, 0, 0, invalidPaddingError() } , := checkedSub(int64(.X), , , ) if ! { return 0, 0, 0, 0, invalidPaddingError() } , := checkedSub(int64(.Y), , , ) if ! { return 0, 0, 0, 0, invalidPaddingError() } , := checkedSub(int64(.X), int64(.X), , ) if ! { return 0, 0, 0, 0, invalidPaddingError() } , = checkedAdd(, , , ) if ! { return 0, 0, 0, 0, invalidPaddingError() } , := checkedSub(int64(.Y), int64(.Y), , ) if ! { return 0, 0, 0, 0, invalidPaddingError() } , = checkedAdd(, , , ) if ! || < 0 || < 0 { return 0, 0, 0, 0, invalidPaddingError() } return int(), int(), int(), int(), nil } func checkedAdd(, , , int64) (int64, bool) { if > 0 && > - || < 0 && < - { return 0, false } return + , true } func checkedSub(, , , int64) (int64, bool) { if > 0 && < + || < 0 && > + { return 0, false } return - , true } func expandDimensions(, , , , int) (int, int, int, int, bool) { := int64(^uint(0) >> 1) := - - 1 := int64() , := checkedAdd(, , , ) if ! { return 0, 0, 0, 0, false } , := checkedSub(int64(), , , ) if ! { return 0, 0, 0, 0, false } , := checkedSub(int64(), , , ) if ! { return 0, 0, 0, 0, false } , := checkedAdd(int64(), , , ) if ! { return 0, 0, 0, 0, false } , := checkedAdd(int64(), , , ) if ! { return 0, 0, 0, 0, false } return int(), int(), int(), int(), true } func ( *builder) ( string, int, float64, d2scene.LineCap, d2scene.LineJoin, string) (*d2scene.Stroke, error) { , := .paint(, ) if != nil { return nil, } if == nil || <= 0 { return nil, nil } := &d2scene.Stroke{Paint: , Width: float64(), Cap: , Join: , MiterLimit: 4} if != 0 { , := svg.GetStrokeDashAttributes(float64(), ) .Dashes = []float64{, } } return , nil }