181 lines
4.6 KiB
Go
181 lines
4.6 KiB
Go
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
|
|
}
|