ui: basic map scroller
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
package world
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
errIncrementalZoomMismatch = errors.New("incremental: zoom/viewport/margins changed; full redraw required")
|
||||
errIncrementalStateNotReady = errors.New("incremental: state not initialized; full redraw required")
|
||||
errIncrementalInvalidZoomFp = errors.New("incremental: invalid zoom")
|
||||
errIncrementalInvalidCanvasPx = errors.New("incremental: invalid canvas size")
|
||||
)
|
||||
|
||||
// rendererIncrementalState stores the minimum state needed for incremental pan.
|
||||
type rendererIncrementalState struct {
|
||||
initialized bool
|
||||
|
||||
// Last render geometry key.
|
||||
lastZoomFp int
|
||||
|
||||
lastViewportW int
|
||||
lastViewportH int
|
||||
lastMarginX int
|
||||
lastMarginY int
|
||||
lastCanvasW int
|
||||
lastCanvasH int
|
||||
|
||||
// Last unwrapped expanded world rect used for rendering.
|
||||
lastWorldRect Rect
|
||||
|
||||
// Remainders in numerator space to make world->px conversion stable across many small pans.
|
||||
// We keep them per axis and update them during conversion.
|
||||
remXNum int64
|
||||
remYNum int64
|
||||
|
||||
// Last measured render duration (nanoseconds). Used for overload heuristics.
|
||||
lastRenderDurationNs int64
|
||||
|
||||
// Pending dirty areas accumulated during shift-only frames.
|
||||
// These are in current canvas pixel coordinates.
|
||||
pendingDirty []RectPx
|
||||
}
|
||||
|
||||
// Reset clears incremental state, forcing next frame to use full redraw.
|
||||
func (s *rendererIncrementalState) Reset() {
|
||||
*s = rendererIncrementalState{}
|
||||
}
|
||||
|
||||
// incrementalKeyFromParams extracts the geometry key that must match for incremental pan.
|
||||
func incrementalKeyFromParams(params RenderParams, zoomFp int) (vw, vh, mx, my, cw, ch, z int) {
|
||||
vw = params.ViewportWidthPx
|
||||
vh = params.ViewportHeightPx
|
||||
mx = params.MarginXPx
|
||||
my = params.MarginYPx
|
||||
cw = params.CanvasWidthPx()
|
||||
ch = params.CanvasHeightPx()
|
||||
z = zoomFp
|
||||
return
|
||||
}
|
||||
|
||||
// worldDeltaFixedToCanvasPx converts a world-fixed delta into a pixel delta using zoomFp,
|
||||
// carrying a signed remainder in numerator space to avoid cumulative drift.
|
||||
//
|
||||
// The conversion is:
|
||||
//
|
||||
// px = floor((deltaWorldFp*zoomFp + rem) / (SCALE*SCALE))
|
||||
//
|
||||
// and rem is updated to the exact remainder.
|
||||
//
|
||||
// This function works for negative deltas too and uses floor division semantics.
|
||||
func worldDeltaFixedToCanvasPx(deltaWorldFp int, zoomFp int, remNum *int64) int {
|
||||
if zoomFp <= 0 {
|
||||
panic("worldDeltaFixedToCanvasPx: invalid zoom")
|
||||
}
|
||||
|
||||
den := int64(SCALE) * int64(SCALE)
|
||||
num := int64(deltaWorldFp)*int64(zoomFp) + *remNum
|
||||
|
||||
q, r := floorDivRem64(num, den)
|
||||
*remNum = r
|
||||
return int(q)
|
||||
}
|
||||
|
||||
// floorDivRem64 returns (q,r) such that:
|
||||
//
|
||||
// q = floor(a / b), r = a - q*b
|
||||
//
|
||||
// with b > 0 and r in [0, b) for a>=0, or r in (-b, 0] for a<0 (signed remainder).
|
||||
func floorDivRem64(a, b int64) (q int64, r int64) {
|
||||
if b <= 0 {
|
||||
panic("floorDivRem64: non-positive divisor")
|
||||
}
|
||||
|
||||
q = a / b
|
||||
r = a % b
|
||||
if r != 0 && a < 0 {
|
||||
q--
|
||||
r = a - q*b
|
||||
}
|
||||
return q, r
|
||||
}
|
||||
|
||||
// ComputePanShiftPx computes the pixel shift that must be applied to the existing backing image
|
||||
// when ONLY camera pan changed (no zoom/viewport/margins changes).
|
||||
//
|
||||
// Returned dxPx/dyPx are shifts to apply to the already rendered image:
|
||||
//
|
||||
// dxPx > 0 => shift image right
|
||||
// dxPx < 0 => shift image left
|
||||
//
|
||||
// This function updates internal incremental state when possible.
|
||||
// If it returns an error, the caller should fall back to a full redraw and call
|
||||
// CommitFullRedrawState afterward.
|
||||
func (w *World) ComputePanShiftPx(params RenderParams) (dxPx, dyPx int, err error) {
|
||||
zoomFp, zerr := params.CameraZoomFp()
|
||||
if zerr != nil {
|
||||
return 0, 0, zerr
|
||||
}
|
||||
if zoomFp <= 0 {
|
||||
return 0, 0, errIncrementalInvalidZoomFp
|
||||
}
|
||||
|
||||
canvasW := params.CanvasWidthPx()
|
||||
canvasH := params.CanvasHeightPx()
|
||||
if canvasW <= 0 || canvasH <= 0 {
|
||||
return 0, 0, errIncrementalInvalidCanvasPx
|
||||
}
|
||||
|
||||
newRect, rerr := params.ExpandedCanvasWorldRect()
|
||||
if rerr != nil {
|
||||
return 0, 0, rerr
|
||||
}
|
||||
|
||||
s := &w.renderState
|
||||
|
||||
// First call: no prior state => must full redraw.
|
||||
if !s.initialized {
|
||||
return 0, 0, errIncrementalStateNotReady
|
||||
}
|
||||
|
||||
vw, vh, mx, my, cw, ch, z := incrementalKeyFromParams(params, zoomFp)
|
||||
if s.lastZoomFp != z ||
|
||||
s.lastViewportW != vw || s.lastViewportH != vh ||
|
||||
s.lastMarginX != mx || s.lastMarginY != my ||
|
||||
s.lastCanvasW != cw || s.lastCanvasH != ch {
|
||||
return 0, 0, errIncrementalZoomMismatch
|
||||
}
|
||||
|
||||
// Compute how much the unwrapped world rect moved.
|
||||
dMinX := newRect.minX - s.lastWorldRect.minX
|
||||
dMinY := newRect.minY - s.lastWorldRect.minY
|
||||
|
||||
// Convert world movement to pixel movement of the world content.
|
||||
// If world rect moved +X (camera moved right), content appears shifted left,
|
||||
// so the old image must be shifted left: shiftPx = -deltaPx.
|
||||
deltaPxX := worldDeltaFixedToCanvasPx(dMinX, zoomFp, &s.remXNum)
|
||||
deltaPxY := worldDeltaFixedToCanvasPx(dMinY, zoomFp, &s.remYNum)
|
||||
|
||||
dxPx = -deltaPxX
|
||||
dyPx = -deltaPxY
|
||||
|
||||
// Update stored rect for the next incremental computation.
|
||||
s.lastWorldRect = newRect
|
||||
|
||||
return dxPx, dyPx, nil
|
||||
}
|
||||
|
||||
// CommitFullRedrawState updates incremental state after a full redraw.
|
||||
// Call this after you finish a full Render() that draws the entire expanded canvas.
|
||||
func (w *World) CommitFullRedrawState(params RenderParams) error {
|
||||
zoomFp, err := params.CameraZoomFp()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if zoomFp <= 0 {
|
||||
return errIncrementalInvalidZoomFp
|
||||
}
|
||||
|
||||
rect, err := params.ExpandedCanvasWorldRect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s := &w.renderState
|
||||
vw, vh, mx, my, cw, ch, z := incrementalKeyFromParams(params, zoomFp)
|
||||
|
||||
s.initialized = true
|
||||
s.lastZoomFp = z
|
||||
s.lastViewportW = vw
|
||||
s.lastViewportH = vh
|
||||
s.lastMarginX = mx
|
||||
s.lastMarginY = my
|
||||
s.lastCanvasW = cw
|
||||
s.lastCanvasH = ch
|
||||
s.lastWorldRect = rect
|
||||
|
||||
// Reset remainders on a full redraw to avoid stale accumulation when geometry changes.
|
||||
s.remXNum = 0
|
||||
s.remYNum = 0
|
||||
|
||||
s.pendingDirty = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user