package world import ( "sort" ) // drawKind is used only for stable tie-breaking when priorities are equal. type drawKind int const ( drawKindLine drawKind = iota drawKindCircle drawKindPoint ) type drawItem struct { kind drawKind priority int id PrimitiveID styleID StyleID // Exactly one of these is set. p Point c Circle l Line } // drawPlanSinglePass renders a plan using a single ordered pass per tile. // Items in each tile are sorted by (Priority asc, Kind asc, ID asc) for determinism. // // allowWrap controls torus behavior: // - true: circles/points produce wrap copies, lines use torus-shortest segments // - false: no copies, lines drawn directly as stored // tileClipEnabled controls whether per-tile ClipRect is applied. // When an outer clip is already set (e.g. dirty rect), disable tile clips for speed. func (w *World) drawPlanSinglePass(drawer PrimitiveDrawer, plan RenderPlan, allowWrap bool, tileClipEnabled bool, isDirtyPass bool) { var lastStyleID StyleID = StyleIDInvalid var lastStyle Style applyStyle := func(styleID StyleID) { if styleID == lastStyleID { return } s, ok := w.styles.Get(styleID) if !ok { panic("render: unknown style ID") } if s.FillColor != nil { drawer.SetFillColor(s.FillColor) } if s.StrokeColor != nil { drawer.SetStrokeColor(s.StrokeColor) } drawer.SetLineWidth(s.StrokeWidthPx) if len(s.StrokeDashes) > 0 { drawer.SetDash(s.StrokeDashes...) } else { drawer.SetDash() } drawer.SetDashOffset(s.StrokeDashOffset) lastStyleID = styleID lastStyle = s } for _, td := range plan.Tiles { if td.ClipW <= 0 || td.ClipH <= 0 { continue } // Per-tile clip is optional. When outer-clip is used (dirty rect), // tileClipEnabled must be false to avoid resetting the outer clip. if tileClipEnabled { drawer.Save() drawer.ResetClip() drawer.ClipRect(float64(td.ClipX), float64(td.ClipY), float64(td.ClipW), float64(td.ClipH)) } items := w.scratchDrawItems[:0] if cap(items) < len(td.Candidates) { items = make([]drawItem, 0, len(td.Candidates)) } for _, it := range td.Candidates { id := it.ID() cur, ok := w.objects[id] if !ok { continue } switch v := cur.(type) { case Point: items = append(items, drawItem{ kind: drawKindPoint, priority: v.Priority, id: v.Id, styleID: v.StyleID, p: v, }) case Circle: items = append(items, drawItem{ kind: drawKindCircle, priority: v.Priority, id: v.Id, styleID: v.StyleID, c: v, }) case Line: items = append(items, drawItem{ kind: drawKindLine, priority: v.Priority, id: v.Id, styleID: v.StyleID, l: v, }) default: panic("render: unknown map item type") } } if len(items) == 0 { if tileClipEnabled { drawer.Restore() } w.scratchDrawItems = items[:0] continue } sort.Slice(items, func(i, j int) bool { a, b := items[i], items[j] if a.priority != b.priority { return a.priority < b.priority } if a.kind != b.kind { return a.kind < b.kind } return a.id < b.id }) // If this is not a dirty pass (full redraw), keep the old behavior for lines: // stroke per segment. This is usually faster for gg on huge scenes. if !isDirtyPass { for i := 0; i < len(items); i++ { di := items[i] applyStyle(di.styleID) switch di.kind { case drawKindPoint: w.drawPointInTile(drawer, plan, td, di.p, allowWrap, lastStyle) case drawKindCircle: w.drawCircleInTile(drawer, plan, td, di.c, allowWrap, lastStyle) case drawKindLine: // Old behavior: drawLineInTile includes Stroke() per segment. w.drawLineInTile(drawer, plan, td, di.l, allowWrap) default: panic("render: unknown draw kind") } } } else { // Dirty pass: batch lines to reduce overhead while panning. inLineRun := false var lineRunStyleID StyleID lineSegCount := 0 flushLineRun := func() { if !inLineRun { return } drawer.Stroke() inLineRun = false lineSegCount = 0 } for i := 0; i < len(items); i++ { di := items[i] if inLineRun { if di.kind != drawKindLine || di.styleID != lineRunStyleID { flushLineRun() } } switch di.kind { case drawKindLine: if !inLineRun { lineRunStyleID = di.styleID applyStyle(lineRunStyleID) inLineRun = true } else { // style matches by construction; keep style state valid if code changes later applyStyle(di.styleID) } added := w.drawLineInTilePath(drawer, plan, td, di.l, allowWrap) lineSegCount += added if lineSegCount >= maxLineSegmentsPerStroke { drawer.Stroke() lineSegCount = 0 // keep run active } case drawKindPoint: flushLineRun() applyStyle(di.styleID) w.drawPointInTile(drawer, plan, td, di.p, allowWrap, lastStyle) case drawKindCircle: flushLineRun() applyStyle(di.styleID) w.drawCircleInTile(drawer, plan, td, di.c, allowWrap, lastStyle) default: flushLineRun() panic("render: unknown draw kind") } } flushLineRun() } if tileClipEnabled { drawer.Restore() } // Reuse buffer for next tile. w.scratchDrawItems = items[:0] } }