package world // renderCirclesStageA performs a full expanded-canvas redraw but renders ONLY Circle primitives. func (w *World) renderCirclesStageA(drawer PrimitiveDrawer, params RenderParams) error { plan, err := w.buildRenderPlanStageA(params) if err != nil { return err } drawCirclesFromPlan(drawer, plan, w.W, w.H) return nil } // drawCirclesFromPlan executes a circles-only draw from an already built render plan. func drawCirclesFromPlan(drawer PrimitiveDrawer, plan RenderPlan, worldW, worldH 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 { shifts := circleWrapShifts(c, worldW, worldH) for _, s := range shifts { if circleCopyIntersectsTile(c, 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(c Circle, 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 c.Radius >= worldW || c.Radius >= worldH { return []wrapShift{{dx: 0, dy: 0}} } xShifts := []int{0} yShifts := []int{0} if c.X+c.Radius >= worldW { xShifts = append(xShifts, -worldW) } if c.X-c.Radius < 0 { xShifts = append(xShifts, worldW) } if c.Y+c.Radius >= worldH { yShifts = append(yShifts, -worldH) } if c.Y-c.Radius < 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(c Circle, 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 := c.X + tile.OffsetX + dx cy := c.Y + tile.OffsetY + dy minX := cx - c.Radius maxX := cx + c.Radius minY := cy - c.Radius maxY := cy + c.Radius // Treat bbox as half-open for intersection checks. if maxX <= segMinX || minX >= segMaxX || maxY <= segMinY || minY >= segMaxY { return false } return true }