package d2scenebuild

import (
	
	
	
	
	
	

	
	

	
	
	
	
)

type sceneTextReference struct {
	node    *d2scene.Node
	value   d2scene.TextRun
	pointer *d2scene.TextRun
	missing map[rune]struct{}
}

type previousShapedText struct {
	ids            [8]d2scene.AssetID
	text           string
	glyphs         []d2scene.Glyph
	size           fixed.Int26_6
	coverage       int64
	runs, idCount  int
	hasShapedValue bool
}

func ( *sceneTextReference) ( []d2scene.AssetID) {
	.value.Fallbacks = 
	if .pointer != nil {
		.pointer.Fallbacks = 
	}
}

func ( *sceneTextReference) ( []d2scene.Glyph) {
	.value.Glyphs = 
	if .pointer != nil {
		.pointer.Glyphs = 
		return
	}
	.node.Primitive = .value
}

type sceneFontCoverage struct {
	font   *fontface.ParsedFace
	source *fontface.BundledFaceSource
	runes  map[rune]uint8
}

const (
	sceneRuneUnsupported uint8 = 1
	sceneRuneSupported   uint8 = 2
)

type sceneFontFallbackKey struct {
	family string
	style  string
	weight int
}

type sceneFontFallbackBucket struct {
	request    d2fonts.FallbackRequest
	missing    map[rune]struct{}
	references []*sceneTextReference
}

const (
	defaultFontFallbackMaxRunesPerText   = 100_000
	defaultFontFallbackMaxTotalRunes     = 10_000_000
	defaultFontFallbackMaxCoverageChecks = int64(50_000_000)
	defaultFontShapingMaxFacesPerText    = 64
	defaultFontShapingMaxRuns            = 100_000
	defaultFontShapingMaxGlyphs          = 10_000_000
	maxSceneFontCoverageEntries          = 4_096
)

// Keep this order aligned with d2fonts' shaping placeholders. U+2610 provides
// an outline-box fallback for D2's bundled Source Sans Pro, which omits U+FFFD
// and U+25A1.
var missingGlyphPlaceholders = [...]rune{'\ufffd', '\u25a1', '\u2610', '?'}

type fontFallbackWorkLimits struct {
	maxRunesPerText   int
	maxTotalRunes     int
	maxCoverageChecks int64
	maxFacesPerText   int
	maxShapingRuns    int
	maxShapedGlyphs   int
}

func ( sceneFontCoverage) ( context.Context,  rune) (bool, error) {
	if  := .Err();  != nil {
		return false, 
	}
	if  := .runes[];  != 0 {
		return  == sceneRuneSupported, nil
	}
	var  bool
	var  error
	if .source != nil {
		,  = .source.SupportsRenderableRune()
	} else {
		,  = .font.SupportsRenderableRune()
	}
	if  != nil {
		return false, 
	}
	if len(.runes) < maxSceneFontCoverageEntries {
		if  {
			.runes[] = sceneRuneSupported
		} else {
			.runes[] = sceneRuneUnsupported
		}
	}
	return , nil
}

// resolveFontFallbacks is deliberately a post-build stage: structured rows,
// highlighted code, Markdown spans, legends, and tooltips may render text that
// is not present in their source object's top-level Label field. Walking the
// completed typed scene guarantees every emitted TextRun gets the same policy.
func ( *builder) ( []*sceneTextReference) error {
	,  := normalizedFontFallbackWorkLimits(.options.Fonts)
	if  != nil {
		return 
	}
	 := make(map[d2scene.AssetID]sceneFontCoverage)
	 := make(map[rune]struct{})
	 := make(map[sceneFontFallbackKey]*sceneFontFallbackBucket)
	var  []*sceneFontFallbackBucket
	var  []d2scene.AssetID
	 := len() > 1
	var  []rune
	 := func( d2scene.AssetID,  rune) (bool, error) {
		if .fontCoverageChecks >= .maxCoverageChecks {
			return false, fmt.Errorf("scene: font coverage checks exceed limit %d", .maxCoverageChecks)
		}
		.fontCoverageChecks++
		,  := .fontCoverage(, )
		if  != nil {
			return false, 
		}
		return .supports(.ctx, )
	}
	for ,  := range  {
		if len(.value.Glyphs) != 0 || .value.Text == "" {
			continue
		}
		if !utf8.ValidString(.value.Text) {
			return fmt.Errorf("scene: text node %q is not valid UTF-8", .node.ID)
		}
		if len(.value.Fallbacks) >= .maxFacesPerText {
			return fmt.Errorf("scene: text node %q fallback font reference count %d exceeds per-text face limit %d", .node.ID, len(.value.Fallbacks), .maxFacesPerText)
		}
		 := make([]d2scene.AssetID, 0, 1+len(.value.Fallbacks))
		 = append(, .value.Font.Asset)
		 = append(, .value.Fallbacks...)
		 := sceneFontFallbackKey{
			family: .value.Font.Family, style: .value.Font.Style, weight: .value.Font.Weight,
		}
		var  *sceneFontFallbackBucket
		 := min(len(.value.Text), .maxRunesPerText)
		 := [:0]
		if ! || cap() <  {
			 = make([]rune, 0, )
		}
		for ,  := range .value.Text {
			if len() >= .maxRunesPerText {
				return fmt.Errorf("scene: text node %q rune count exceeds per-text limit %d", .node.ID, .maxRunesPerText)
			}
			if .fontRunes >= .maxTotalRunes {
				return fmt.Errorf("scene: text rune count exceeds total limit %d", .maxTotalRunes)
			}
			.fontRunes++
			if .fontRunes&1023 == 0 {
				if  := .ctx.Err();  != nil {
					return 
				}
			}
			 = append(, )
		}
		if  && cap() <= maxSceneFontCoverageEntries {
			 = 
		} else {
			 = nil
		}
		var  segmenter.Segmenter
		.Init()
		for  := .GraphemeIterator(); .Next(); {
			if  := .ctx.Err();  != nil {
				return 
			}
			 := .Grapheme()
			 := false
			for ,  := range  {
				 := true
				for ,  := range .Text {
					if fontRuneIsDefaultIgnorable() {
						continue
					}
					,  := (, )
					if  != nil {
						return fmt.Errorf("scene: text node %q: %w", .node.ID, )
					}
					if ! {
						 = false
						break
					}
				}
				if  {
					 = true
					break
				}
			}
			if  {
				continue
			}
			if .missing == nil {
				.missing = make(map[rune]struct{})
			}
			for ,  := range .Text {
				if !fontRuneIsDefaultIgnorable() {
					.missing[] = struct{}{}
					[] = struct{}{}
					if  == nil {
						 = []
						if  == nil {
							 = &sceneFontFallbackBucket{
								request: d2fonts.FallbackRequest{Family: .family, Style: .style, Weight: .weight},
								missing: make(map[rune]struct{}),
							}
							[] = 
							 = append(, )
						}
						.references = append(.references, )
					}
					.missing[] = struct{}{}
				}
			}
		}
	}
	, ,  := .retainedFontUsage()
	if  != nil {
		return 
	}
	if .options.Fonts != nil {
		if .options.Fonts.MaxAssets <= 0 || .options.Fonts.MaxBytes <= 0 {
			return fmt.Errorf("scene: font fallback resolver requires positive asset and byte limits")
		}
		if  > .options.Fonts.MaxAssets {
			return fmt.Errorf("scene: retained font asset count %d exceeds limit %d", , .options.Fonts.MaxAssets)
		}
		if  > .options.Fonts.MaxBytes {
			return fmt.Errorf("scene: retained font bytes %d exceed limit %d", , .options.Fonts.MaxBytes)
		}
	}
	if len() == 0 {
		return nil
	}
	for ,  := range  {
		 := make(map[rune]struct{}, len(.missing))
		for  := range .missing {
			[] = struct{}{}
		}
		 := make([]d2scene.AssetID, 0, len())
		for ,  := range  {
			 := false
			for  := range  {
				,  := (, )
				if  != nil {
					return 
				}
				if  {
					delete(, )
					 = true
				}
			}
			if  {
				 = appendUniqueAssetID(, )
			}
		}

		var  []fontfallback.SceneFont
		if .options.Fonts != nil && .options.Fonts.Resolver != nil && len() != 0 {
			.request.Runes = sortedRunes()
			, ,  := fontfallback.ResolveForScene(.ctx, .options.Fonts.Resolver, fontfallback.Request{
				Runes: .request.Runes, Family: .request.Family, Style: .request.Style, Weight: .request.Weight,
			})
			if  != nil {
				return fmt.Errorf("scene: font fallback for family %q style %q weight %d: %w", .request.Family, .request.Style, .request.Weight, )
			}
			if  {
				 = 
			} else {
				,  := .options.Fonts.Resolver.ResolveFallbacks(.ctx, .request)
				if  != nil {
					return fmt.Errorf("scene: font fallback for family %q style %q weight %d: %w", .request.Family, .request.Style, .request.Weight, )
				}
				 = make([]fontfallback.SceneFont, len())
				for  := range  {
					[].Font = fontfallback.Font{
						Name: [].Name, MIMEType: [].MIMEType,
						Data: [].Data, FaceIndex: [].FaceIndex,
					}
				}
			}
		}
		// Bound hostile resolver output before allocating IDs or hashing whole
		// font blobs. Repeated style buckets may legitimately return an already
		// retained resource, so actual remaining capacity is enforced after its
		// content-derived identity is known.
		if .options.Fonts != nil && len() > .options.Fonts.MaxAssets {
			return fmt.Errorf("scene: font fallback resolver returned %d resources, exceeding result limit %d", len(), .options.Fonts.MaxAssets)
		}
		 := int64(0)
		for ,  := range  {
			if int64(len(.Data)) > .options.Fonts.MaxBytes- {
				return fmt.Errorf("scene: font fallback resolver bytes exceed limit %d at resource %d (%s)", .options.Fonts.MaxBytes, , safeFallbackName(.Name))
			}
			 += int64(len(.Data))
		}

		for ,  := range  {
			if  := .ctx.Err();  != nil {
				return 
			}
			if len(.Data) == 0 {
				return fmt.Errorf("scene: font fallback %d (%s) has no data", , safeFallbackName(.Name))
			}
			if strings.TrimSpace(.MIMEType) == "" {
				return fmt.Errorf("scene: font fallback %d (%s) has no MIME type", , safeFallbackName(.Name))
			}
			 := .ID
			if .Shared {
				if  == "" || .Face == nil {
					return fmt.Errorf("scene: shared font fallback %d (%s) is incomplete", , safeFallbackName(.Name))
				}
			} else {
				,  = hashFallbackFont(.ctx, .Font)
				if  != nil {
					return 
				}
			}
			if ,  := .assets[]; ! {
				if  >= .options.Fonts.MaxAssets {
					return fmt.Errorf("scene: retained font asset count exceeds limit %d", .options.Fonts.MaxAssets)
				}
				if int64(len(.Data)) > .options.Fonts.MaxBytes- {
					return fmt.Errorf("scene: retained font bytes exceed limit %d", .options.Fonts.MaxBytes)
				}
				 := .Data
				if !.Shared {
					 = append([]byte(nil), .Data...)
				}
				.assets[] = d2scene.FontAsset{MIMEType: .MIMEType, Data: , FaceIndex: .FaceIndex}
				if .Shared {
					.fontFaces[] = .Face
				}
				++
				 += int64(len(.Data))
			}
			,  := .fontCoverage(, )
			if  != nil {
				return fmt.Errorf("scene: font fallback %d (%s): %w", , safeFallbackName(.Name), )
			}
			if .font.IsBundledNotoColorEmoji() {
				 = appendUniqueAssetID(, )
			}
			 = appendUniqueAssetID(, )
		}

		for ,  := range .references {
			 := sortedRunes(.missing)
			 := append([]d2scene.AssetID(nil), .value.Fallbacks...)
			for ,  := range  {
				for ,  := range  {
					,  := (, )
					if  != nil {
						return 
					}
					if  {
						 = appendUniqueAssetID(, )
						break
					}
				}
			}
			 := false
			for ,  := range append([]d2scene.AssetID{.value.Font.Asset}, ...) {
				for ,  := range missingGlyphPlaceholders {
					,  := (, )
					if  != nil {
						return 
					}
					if  {
						 = true
						break
					}
				}
				if  {
					break
				}
			}
			if ! {
				, ,  := .ensureMissingGlyphFont(, )
				if  != nil {
					return fmt.Errorf("scene: text node %q: %w", .node.ID, )
				}
				if  != 0 {
					++
					 += 
				}
				 = appendUniqueAssetID(, )
			}
			.setFallbacks()
		}
	}
	return .ctx.Err()
}

// shapeTextRuns materializes ordinary text after fallback resolution. Raster
// frames consume only these immutable glyph IDs and placements, so animation
// does not repeat bidi segmentation or HarfBuzz work for every frame.
func ( *builder) ( []*sceneTextReference) error {
	,  := normalizedFontFallbackWorkLimits(.options.Fonts)
	if  != nil {
		return 
	}
	 := func( d2scene.AssetID) (*fontface.ParsedFace, error) {
		return .fontFace()
	}
	var  fontface.ShapingWorkspace
	var  [8]d2scene.AssetID
	var  []fontface.ShapeFace
	 := len() > 1
	if  {
		 = make([]fontface.ShapeFace, 0, 8)
	}
	var  *previousShapedText
	if len() > 1 {
		 = new(previousShapedText)
	}
	for ,  := range  {
		 := .value
		if len(.Glyphs) != 0 || .Text == "" {
			continue
		}
		if  := .ctx.Err();  != nil {
			return 
		}
		if len(.Fallbacks) >= .maxFacesPerText {
			return fmt.Errorf("scene: text node %q fallback font reference count %d exceeds per-text face limit %d", .node.ID, len(.Fallbacks), .maxFacesPerText)
		}
		 := 1 + len(.Fallbacks)
		var  []d2scene.AssetID
		if ! {
			 = make([]d2scene.AssetID, 0, )
			 := make(map[d2scene.AssetID]bool, )
			for ,  := range append([]d2scene.AssetID{.Font.Asset}, .Fallbacks...) {
				if [] {
					continue
				}
				[] = true
				 = append(, )
			}
		} else if  <= len() {
			 = [:1]
		} else {
			 = make([]d2scene.AssetID, 1, )
		}
		if  {
			[0] = .Font.Asset
			var  map[d2scene.AssetID]struct{}
			for ,  := range .Fallbacks {
				 := false
				if  == nil {
					for ,  := range  {
						if  ==  {
							 = true
							break
						}
					}
					if ! && len() == len() {
						 = make(map[d2scene.AssetID]struct{}, )
						for ,  := range  {
							[] = struct{}{}
						}
					}
				} else {
					_,  = []
				}
				if  {
					continue
				}
				if  != nil {
					[] = struct{}{}
				}
				 = append(, )
			}
		}
		if len() > .maxFacesPerText {
			return fmt.Errorf("scene: text node %q font face count %d exceeds per-text limit %d", .node.ID, len(), .maxFacesPerText)
		}
		var  []fontface.ShapeFace
		if  != nil && len() <= cap() {
			 = [:0]
		} else {
			 = make([]fontface.ShapeFace, 0, len())
		}
		for ,  := range  {
			,  := ()
			if  != nil {
				return fmt.Errorf("scene: text node %q: %w", .node.ID, )
			}
			 = append(, fontface.ShapeFace{ID: string(), Face: })
		}

		 := .maxCoverageChecks - .fontCoverageChecks
		if  <= 0 {
			return fmt.Errorf("scene: font coverage checks exceed limit %d", .maxCoverageChecks)
		}
		 := .maxShapingRuns - .fontShapeRuns
		if  <= 0 {
			return fmt.Errorf("scene: text shaping run count exceeds limit %d", .maxShapingRuns)
		}
		 := .maxShapedGlyphs - .fontGlyphs
		if  <= 0 {
			return fmt.Errorf("scene: shaped glyph count exceeds limit %d", .maxShapedGlyphs)
		}
		if !finite(.Font.Size) || .Font.Size <= 0 || .Font.Size > float64(math.MaxInt32)/64 {
			return fmt.Errorf("scene: text node %q has an invalid font size for shaping", .node.ID)
		}
		 := fixed.Int26_6(math.Round(.Font.Size * 64))
		 :=  != nil && .hasShapedValue && .Text == .text &&  == .size && len() == .idCount
		if  {
			for ,  := range  {
				if .ids[] !=  {
					 = false
					break
				}
			}
		}
		if  && .coverage <=  && .runs <=  && len(.glyphs) <=  {
			 := make([]d2scene.Glyph, len(.glyphs))
			for  := 0;  < len();  += 1_024 {
				if  := .ctx.Err();  != nil {
					return 
				}
				 := min(+1_024, len())
				copy([:], .glyphs[:])
			}
			.fontCoverageChecks += .coverage
			.fontShapeRuns += .runs
			.fontGlyphs += len()
			.setGlyphs()
			continue
		}
		,  := .ShapeTextTransient(.ctx, .Text, , , fontface.ShapeLimits{
			Runes:          .maxRunesPerText,
			Faces:          .maxFacesPerText,
			CoverageChecks: ,
			Runs:           ,
			Glyphs:         ,
		})
		if  != nil {
			return fmt.Errorf("scene: text node %q: %w", .node.ID, )
		}
		 := make([]d2scene.Glyph, 0, len(.Glyphs))
		for ,  := range .Glyphs {
			if .Face < 0 || .Face >= len() {
				return fmt.Errorf("scene: text node %q shaper returned invalid face %d", .node.ID, .Face)
			}
			 := d2scene.Bounds{}
			if .HasInk {
				 = d2scene.NewBounds(
					fixedToSceneFloat(.Ink.Min.X), fixedToSceneFloat(.Ink.Min.Y),
					fixedToSceneFloat(.Ink.Max.X), fixedToSceneFloat(.Ink.Max.Y),
				)
			}
			 = append(, d2scene.Glyph{
				ID: .ID, Empty: .Empty, Asset: [.Face],
				Position: d2scene.Point{X: .PositionX, Y: .PositionY},
				Advance:  .Advance, Ink: ,
			})
		}
		if len() == 0 {
			return fmt.Errorf("scene: text node %q shaped non-empty text to no glyphs", .node.ID)
		}
		.fontCoverageChecks += .CoverageChecks
		.fontShapeRuns += .Runs
		.fontGlyphs += len()
		.setGlyphs()
		if  != nil && len() <= len(.ids) {
			clear(.ids[:])
			copy(.ids[:], )
			.text = .Text
			.glyphs = 
			.size = 
			.coverage = .CoverageChecks
			.runs = .Runs
			.idCount = len()
			.hasShapedValue = true
		} else if  != nil {
			.hasShapedValue = false
		}
	}
	return .ctx.Err()
}

func fixedToSceneFloat( fixed.Int26_6) float64 {
	return float64() / 64
}

func normalizedFontFallbackWorkLimits( *FontFallbackOptions) (fontFallbackWorkLimits, error) {
	 := fontFallbackWorkLimits{
		maxRunesPerText: defaultFontFallbackMaxRunesPerText, maxTotalRunes: defaultFontFallbackMaxTotalRunes,
		maxCoverageChecks: defaultFontFallbackMaxCoverageChecks,
		maxFacesPerText:   defaultFontShapingMaxFacesPerText, maxShapingRuns: defaultFontShapingMaxRuns,
		maxShapedGlyphs: defaultFontShapingMaxGlyphs,
	}
	if  == nil {
		return , nil
	}
	if .MaxRunesPerText < 0 || .MaxTotalRunes < 0 || .MaxCoverageChecks < 0 ||
		.MaxFontFacesPerText < 0 || .MaxShapingRuns < 0 || .MaxShapedGlyphs < 0 {
		return fontFallbackWorkLimits{}, fmt.Errorf("scene: font fallback work limits must not be negative")
	}
	if .MaxRunesPerText != 0 {
		.maxRunesPerText = .MaxRunesPerText
	}
	if .MaxTotalRunes != 0 {
		.maxTotalRunes = .MaxTotalRunes
	}
	if .MaxCoverageChecks != 0 {
		.maxCoverageChecks = .MaxCoverageChecks
	}
	if .MaxFontFacesPerText != 0 {
		.maxFacesPerText = .MaxFontFacesPerText
	} else if .MaxAssets > 0 {
		.maxFacesPerText = min(.maxFacesPerText, .MaxAssets)
	}
	if .MaxShapingRuns != 0 {
		.maxShapingRuns = .MaxShapingRuns
	}
	if .MaxShapedGlyphs != 0 {
		.maxShapedGlyphs = .MaxShapedGlyphs
	}
	return , nil
}

func ( *builder) () (int, int64, error) {
	 := 0
	 := int64(0)
	for ,  := range .assets {
		var  d2scene.FontAsset
		switch value := .(type) {
		case d2scene.FontAsset:
			 = 
		case *d2scene.FontAsset:
			if  == nil {
				return 0, 0, fmt.Errorf("scene: font asset %q is nil", )
			}
			 = *
		default:
			continue
		}
		if int64(len(.Data)) > math.MaxInt64- {
			return 0, 0, fmt.Errorf("scene: retained font bytes exceed the int64 domain")
		}
		++
		 += int64(len(.Data))
	}
	return , , nil
}

func ( *builder) ( d2scene.AssetID,  map[d2scene.AssetID]sceneFontCoverage) (sceneFontCoverage, error) {
	if ,  := [];  {
		return , nil
	}
	,  := .fontFace()
	if  != nil {
		return sceneFontCoverage{}, 
	}
	var  *fontface.BundledFaceSource
	if ,  := .assets[].(d2scene.FontAsset);  {
		var  bool
		, ,  = fontface.RegisteredBundledFace(.Data, .FaceIndex)
		if  == nil && ! {
			, _,  = fontface.RegisteredBundledNotoColorEmoji(.Data, .FaceIndex)
		}
		if  != nil {
			return sceneFontCoverage{}, 
		}
	}
	 := sceneFontCoverage{font: , source: , runes: make(map[rune]uint8)}
	[] = 
	return , nil
}

// fontFace owns the one parsed face for an asset throughout a scene build.
// Coverage and shaping are sequential stages on the same builder, so sharing
// this mutable go-text cache is safe and avoids parsing every font twice.
func ( *builder) ( d2scene.AssetID) (*fontface.ParsedFace, error) {
	if .fontFaces == nil {
		.fontFaces = make(map[d2scene.AssetID]*fontface.ParsedFace)
	}
	if  := .fontFaces[];  != nil {
		return , nil
	}
	,  := .assets[].(d2scene.FontAsset)
	if ! {
		return nil, fmt.Errorf("font asset %q is missing or invalid", )
	}
	var (
		 *fontface.ParsedFace
		  error
	)
	, ,  := fontface.RegisteredBundledFace(.Data, .FaceIndex)
	if  != nil {
		return nil, fmt.Errorf("parse face %d from font asset %q: %w", .FaceIndex, , )
	} else if  {
		,  = .CloneReadOnly()
	} else {
		,  = fontface.ParseFace(.Data, .FaceIndex)
	}
	if  != nil {
		return nil, fmt.Errorf("parse face %d from font asset %q: %w", .FaceIndex, , )
	}
	.fontFaces[] = 
	return , nil
}

func ( *builder) ( int,  int64) (d2scene.AssetID, int64, error) {
	 := d2scene.AssetID("font:" + string(d2fonts.SourceSansPro) + ":" + string(d2fonts.FONT_STYLE_REGULAR))
	if ,  := .assets[];  {
		,  := .fontFace()
		if  != nil {
			return "", 0, 
		}
		,  := supportsMissingGlyphPlaceholder()
		if  != nil || ! {
			if  != nil {
				return "", 0, 
			}
			return "", 0, fmt.Errorf("bundled missing-glyph font has no drawable placeholder")
		}
		return , 0, nil
	}
	 := d2fonts.Font{Family: d2fonts.SourceSansPro, Style: d2fonts.FONT_STYLE_REGULAR}
	,  := d2fonts.FontFaces.Lookup()
	if ! || len() == 0 {
		return "", 0, fmt.Errorf("bundled missing-glyph font is not loaded")
	}
	if .options.Fonts != nil {
		if  >= .options.Fonts.MaxAssets {
			return "", 0, fmt.Errorf("retained font asset count exceeds limit %d while adding missing-glyph font", .options.Fonts.MaxAssets)
		}
		if int64(len()) > .options.Fonts.MaxBytes- {
			return "", 0, fmt.Errorf("retained font bytes exceed limit %d while adding missing-glyph font", .options.Fonts.MaxBytes)
		}
	}
	.assets[] = d2scene.FontAsset{MIMEType: "font/ttf", Data: retainedFontBytes()}
	,  := .fontFace()
	if  != nil {
		delete(.assets, )
		delete(.fontFaces, )
		return "", 0, 
	}
	,  := supportsMissingGlyphPlaceholder()
	if  != nil || ! {
		delete(.assets, )
		delete(.fontFaces, )
		if  != nil {
			return "", 0, 
		}
		return "", 0, fmt.Errorf("bundled missing-glyph font has no drawable placeholder")
	}
	return , int64(len()), nil
}

func supportsMissingGlyphPlaceholder( *fontface.ParsedFace) (bool, error) {
	for ,  := range missingGlyphPlaceholders {
		,  := .SupportsRenderableRune()
		if  != nil {
			return false, 
		}
		if  {
			return true, nil
		}
	}
	return false, nil
}

func ( *builder) ( *d2scene.Node) ([]*sceneTextReference, error) {
	 := []*d2scene.Node{}
	 := make([]string, 0, len(.assets))
	for  := range .assets {
		 = append(, string())
	}
	sort.Strings()
	for ,  := range  {
		if ,  := .assets[d2scene.AssetID()].(d2scene.VectorAsset);  {
			 = append(, .Root)
		}
	}
	 := make(map[*d2scene.Node]bool)
	 := append([]*d2scene.Node(nil), ...)
	var  []*sceneTextReference
	for len() != 0 {
		if  := .ctx.Err();  != nil {
			return nil, 
		}
		 := [len()-1]
		 = [:len()-1]
		if  == nil || [] {
			continue
		}
		[] = true
		 = append(, .Children...)
		if .Mask != nil {
			 = append(, .Mask.Root)
		}
		switch primitive := .Primitive.(type) {
		case d2scene.TextRun:
			 = append(, &sceneTextReference{node: , value: })
			 = appendPaintRoots(, .Fill, .Stroke)
		case *d2scene.TextRun:
			if  != nil {
				 = append(, &sceneTextReference{node: , value: *, pointer: })
				 = appendPaintRoots(, .Fill, .Stroke)
			}
		case d2scene.Rect:
			 = appendPaintRoots(, .Fill, .Stroke)
		case *d2scene.Rect:
			if  != nil {
				 = appendPaintRoots(, .Fill, .Stroke)
			}
		case d2scene.Ellipse:
			 = appendPaintRoots(, .Fill, .Stroke)
		case *d2scene.Ellipse:
			if  != nil {
				 = appendPaintRoots(, .Fill, .Stroke)
			}
		case d2scene.Path:
			 = appendPaintRoots(, .Fill, .Stroke)
		case *d2scene.Path:
			if  != nil {
				 = appendPaintRoots(, .Fill, .Stroke)
			}
		}
	}
	return , nil
}

func appendPaintRoots( []*d2scene.Node,  d2scene.Paint,  *d2scene.Stroke) []*d2scene.Node {
	 := func( d2scene.Paint) {
		switch pattern := .(type) {
		case d2scene.PatternPaint:
			 = append(, .Root)
		case *d2scene.PatternPaint:
			if  != nil {
				 = append(, .Root)
			}
		}
	}
	()
	if  != nil {
		(.Paint)
	}
	return 
}

func hashFallbackFont( context.Context,  fontfallback.Font) (d2scene.AssetID, error) {
	return fontfallback.AssetID(, )
}

func fontRuneIsDefaultIgnorable( rune) bool {
	return fontface.IsDefaultIgnorableRune()
}

func appendUniqueAssetID( []d2scene.AssetID,  d2scene.AssetID) []d2scene.AssetID {
	for ,  := range  {
		if  ==  {
			return 
		}
	}
	return append(, )
}

func sortedRunes( map[rune]struct{}) []rune {
	 := make([]rune, 0, len())
	for  := range  {
		 = append(, )
	}
	sort.Slice(, func(,  int) bool { return [] < [] })
	return 
}

func safeFallbackName( string) string {
	 = strings.ToValidUTF8(, "?")
	 = strings.Map(func( rune) rune {
		if  < 0x20 ||  == 0x7f {
			return '?'
		}
		return 
	}, )
	 := []rune()
	if len() > 80 {
		 = string([:80])
	}
	if  == "" {
		return "unnamed font"
	}
	return 
}