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 } // circleWrapShiftsInto appends required torus-copy shifts for a circle into dst and returns the resulting slice. // It never allocates if dst has enough capacity. // // The 0-shift is always included. Additional copies are included when the circle's bbox crosses world edges. func circleWrapShiftsInto(dst []wrapShift, cx, cy, radiusFp, worldW, worldH int) []wrapShift { dst = dst[:0] // Always include the original. dst = append(dst, wrapShift{dx: 0, dy: 0}) if radiusFp <= 0 { return dst } minX := cx - radiusFp maxX := cx + radiusFp minY := cy - radiusFp maxY := cy + radiusFp needLeft := minX < 0 needRight := maxX > worldW needTop := minY < 0 needBottom := maxY > worldH // X-only copies. if needLeft { dst = append(dst, wrapShift{dx: +worldW, dy: 0}) } if needRight { dst = append(dst, wrapShift{dx: -worldW, dy: 0}) } // Y-only copies. if needTop { dst = append(dst, wrapShift{dx: 0, dy: +worldH}) } if needBottom { dst = append(dst, wrapShift{dx: 0, dy: -worldH}) } // Corner copies (combine X and Y). if (needLeft || needRight) && (needTop || needBottom) { var dxs [2]int dxn := 0 if needLeft { dxs[dxn] = +worldW dxn++ } if needRight { dxs[dxn] = -worldW dxn++ } var dys [2]int dyn := 0 if needTop { dys[dyn] = +worldH dyn++ } if needBottom { dys[dyn] = -worldH dyn++ } for i := 0; i < dxn; i++ { for j := 0; j < dyn; j++ { dst = append(dst, wrapShift{dx: dxs[i], dy: dys[j]}) } } } return dst } // circleWrapShifts is a compatibility wrapper that allocates. // Prefer circleWrapShiftsInto in hot paths. func circleWrapShifts(cx, cy, radiusFp, worldW, worldH int) []wrapShift { var dst []wrapShift return circleWrapShiftsInto(dst, cx, cy, radiusFp, worldW, worldH) } // 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 }