package world // drawCirclesFromPlan executes a circles-only draw from an already built render plan. func drawCirclesFromPlan(drawer PrimitiveDrawer, plan RenderPlan, worldW, worldH int, allowWrap bool, circleRadiusScaleFp int) { for _, td := range plan.Tiles { if td.ClipW <= 0 || td.ClipH <= 0 { continue } // Filter only circles; skip tiles that have no circles. circles := make([]Circle, 0, len(td.Candidates)) for _, it := range td.Candidates { c, ok := it.(Circle) if !ok { continue } circles = append(circles, c) } if len(circles) == 0 { continue } // Determine which circle copies actually intersect this tile segment. type circleCopy struct { c Circle dx int dy int } copiesToDraw := make([]circleCopy, 0, len(circles)) for _, c := range circles { var shifts []wrapShift effRadius := circleRadiusEffFp(c.Radius, circleRadiusScaleFp) if allowWrap { shifts = circleWrapShifts(c.X, c.Y, effRadius, worldW, worldH) } else { shifts = []wrapShift{{dx: 0, dy: 0}} } for _, s := range shifts { if circleCopyIntersectsTile(c.X, c.Y, effRadius, s.dx, s.dy, td.Tile, worldW, worldH) { copiesToDraw = append(copiesToDraw, circleCopy{c: c, dx: s.dx, dy: s.dy}) } } } if len(copiesToDraw) == 0 { continue } drawer.Save() drawer.ClipRect(float64(td.ClipX), float64(td.ClipY), float64(td.ClipW), float64(td.ClipH)) for _, cc := range copiesToDraw { c := cc.c // Project the circle center for this tile copy (tile offset + wrap shift). cxPx := worldSpanFixedToCanvasPx((c.X+td.Tile.OffsetX+cc.dx)-plan.WorldRect.minX, plan.ZoomFp) cyPx := worldSpanFixedToCanvasPx((c.Y+td.Tile.OffsetY+cc.dy)-plan.WorldRect.minY, plan.ZoomFp) // Radius is a world span. rPx := worldSpanFixedToCanvasPx(c.Radius, plan.ZoomFp) drawer.AddCircle(float64(cxPx), float64(cyPx), float64(rPx)) } drawer.Fill() drawer.Restore() } } type wrapShift struct { dx int dy int } // circleWrapShifts returns 1..4 wrap shifts (multiples of worldW/worldH) required to render // all torus copies of the circle inside the canonical world domain. // The (0,0) shift is always present. func circleWrapShifts(cx, cy, radiusFp, worldW, worldH int) []wrapShift { // If radius covers the whole axis, additional copies are not useful. // (One copy already covers everything under any reasonable clip.) if radiusFp >= worldW || radiusFp >= worldH { return []wrapShift{{dx: 0, dy: 0}} } xShifts := []int{0} yShifts := []int{0} if cx+radiusFp >= worldW { xShifts = append(xShifts, -worldW) } if cx-radiusFp < 0 { xShifts = append(xShifts, worldW) } if cy+radiusFp >= worldH { yShifts = append(yShifts, -worldH) } if cy-radiusFp < 0 { yShifts = append(yShifts, worldH) } out := make([]wrapShift, 0, len(xShifts)*len(yShifts)) for _, dx := range xShifts { for _, dy := range yShifts { out = append(out, wrapShift{dx: dx, dy: dy}) } } return out } // circleCopyIntersectsTile checks whether the circle copy (shifted by dx/dy) intersects the tile segment. // We use the tile's unwrapped segment bounds: [offset+rect.min, offset+rect.max) per axis. func circleCopyIntersectsTile(cx, cy, radiusFp, dx, dy int, tile WorldTile, worldW, worldH int) bool { // Unwrapped tile segment bounds. segMinX := tile.OffsetX + tile.Rect.minX segMaxX := tile.OffsetX + tile.Rect.maxX segMinY := tile.OffsetY + tile.Rect.minY segMaxY := tile.OffsetY + tile.Rect.maxY // Circle bbox in the same unwrapped space (apply shift + tile offset). cx = cx + tile.OffsetX + dx cy = cy + tile.OffsetY + dy minX := cx - radiusFp maxX := cx + radiusFp minY := cy - radiusFp maxY := cy + radiusFp // Treat bbox as half-open for intersection checks. if maxX <= segMinX || minX >= segMaxX || maxY <= segMinY || minY >= segMaxY { return false } return true }