169 lines
5.7 KiB
Go
169 lines
5.7 KiB
Go
package client
|
|
|
|
import (
|
|
"image"
|
|
|
|
"fyne.io/fyne/v2"
|
|
// adjust import path to your world package
|
|
// "your/module/world"
|
|
"github.com/fogleman/gg"
|
|
"github.com/iliadenisov/galaxy/client/world"
|
|
)
|
|
|
|
/*
|
|
Fyne integration notes:
|
|
|
|
- canvas.NewRaster calls draw(w,h) on the UI thread.
|
|
- We MUST keep draw() cheap and never loop re-rendering inside it.
|
|
- Coalescing must therefore schedule refreshes and render at most once per draw call.
|
|
- The world renderer expects:
|
|
- RenderParams.ViewportWidthPx/HeightPx: the size of the visible viewport.
|
|
- RenderParams.MarginXPx/MarginYPx: margins around viewport.
|
|
- RenderParams.CameraXWorldFp/YWorldFp: camera center in world-fixed units.
|
|
- RenderParams.CameraZoom: float zoom (converted inside world).
|
|
- world.Render draws on the full expanded canvas (viewport + 2*margins on each axis).
|
|
|
|
This adapter enforces:
|
|
- viewport sizes come from draw(w,h)
|
|
- margins are computed from viewport sizes (w/4 and h/4)
|
|
- gg context backing image is resized to the expanded canvas size
|
|
- IndexOnViewportChange is called when viewport sizes changed (you can also include zoom if desired)
|
|
*/
|
|
|
|
// FyneExecutor posts functions onto the Fyne UI thread.
|
|
type FyneExecutor struct{}
|
|
|
|
func (FyneExecutor) Post(fn func()) {
|
|
fyne.Do(fn)
|
|
}
|
|
|
|
// Widget returns the fyne CanvasObject to add into your UI.
|
|
func (e *editor) Widget() fyne.CanvasObject {
|
|
return e.raster
|
|
}
|
|
|
|
// GetParams returns a copy of current render params for external reads.
|
|
func (e *editor) GetParams() world.RenderParams {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
return *e.wp
|
|
}
|
|
|
|
// UpdateParams applies a modification function to render params and schedules a refresh.
|
|
// This is a safe way to mutate camera/zoom from event handlers.
|
|
func (e *editor) UpdateParams(fn func(p *world.RenderParams)) {
|
|
e.mu.Lock()
|
|
fn(e.wp)
|
|
|
|
// IMPORTANT: clamp camera if no-wrap
|
|
e.world.ClampRenderParamsNoWrap(e.wp)
|
|
|
|
p := e.wp // snapshot
|
|
e.mu.Unlock()
|
|
|
|
e.co.Request(*p)
|
|
}
|
|
|
|
// RequestRefresh schedules a refresh with the current params snapshot.
|
|
// Useful if you changed world objects and want to redraw.
|
|
func (e *editor) RequestRefresh() {
|
|
e.mu.RLock()
|
|
p := e.wp
|
|
e.mu.RUnlock()
|
|
e.co.Request(*p)
|
|
}
|
|
|
|
// draw is the raster callback. It must be cheap and must not block on multiple re-renders.
|
|
// It delegates coalescing + rendering decision to RasterCoalescer.
|
|
func (e *editor) draw(wPx, hPx int) image.Image {
|
|
// Snapshot latest params and render once.
|
|
// e.mu.RLock()
|
|
// p := e.wp
|
|
// e.mu.RUnlock()
|
|
|
|
// Request() already scheduled refreshes; Draw() actually renders for this callback.
|
|
// We bypass co.Draw(w,h) because we need to pass our snapshot to coalescer in a controlled way.
|
|
// The simplest pattern: keep coalescer as the sole driver: call co.Draw(w,h) here.
|
|
// But then coalescer uses its internal latest. So make sure we always call co.Request on updates.
|
|
//
|
|
// In normal operation you can just: return e.co.Draw(wPx,hPx)
|
|
// and never use p above. We'll do that to keep a single source of truth.
|
|
// _ = p
|
|
return e.co.Draw(wPx, hPx)
|
|
}
|
|
|
|
// renderRasterImage renders the expanded canvas into the GGDrawer backing image,
|
|
// then copies only the viewport ROI into a reusable viewport buffer and returns it.
|
|
func (e *editor) renderRasterImage(viewportW, viewportH int, p world.RenderParams) image.Image {
|
|
// 1) Viewport sizes come from raster draw callback.
|
|
p.ViewportWidthPx = viewportW
|
|
p.ViewportHeightPx = viewportH
|
|
p.MarginXPx = viewportW / 4
|
|
p.MarginYPx = viewportH / 4
|
|
|
|
// 2) Ensure indexing is up-to-date when viewport size changed.
|
|
if viewportW != e.lastViewportW || viewportH != e.lastViewportH {
|
|
e.world.IndexOnViewportChange(viewportW, viewportH, p.CameraZoom)
|
|
e.lastViewportW = viewportW
|
|
e.lastViewportH = viewportH
|
|
}
|
|
|
|
// 3) Ensure GG backing canvas is sized for expanded canvas.
|
|
canvasW := p.CanvasWidthPx()
|
|
canvasH := p.CanvasHeightPx()
|
|
e.ensureDrawerCanvas(canvasW, canvasH)
|
|
|
|
// 4) Render into expanded canvas backing (full or incremental is decided inside world.Render).
|
|
_ = e.world.Render(e.drawer, p) // handle error in your real code
|
|
|
|
// 5) Copy viewport ROI into reusable viewport buffer and return it.
|
|
e.ensureViewportBuffer(viewportW, viewportH)
|
|
|
|
src, ok := e.drawer.DC.Image().(*image.RGBA)
|
|
if !ok || src == nil {
|
|
// Should not happen if GGDrawer is backed by RGBA.
|
|
return image.NewRGBA(image.Rect(0, 0, viewportW, viewportH))
|
|
}
|
|
|
|
copyViewportRGBA(e.viewportImg, src, p.MarginXPx, p.MarginYPx, viewportW, viewportH)
|
|
return e.viewportImg
|
|
}
|
|
|
|
// ensureDrawerCanvas ensures drawer has a gg.Context sized to canvasW x canvasH.
|
|
func (e *editor) ensureDrawerCanvas(canvasW, canvasH int) {
|
|
if e.drawer.DC != nil && e.lastCanvasW == canvasW && e.lastCanvasH == canvasH {
|
|
return
|
|
}
|
|
// world.NewGGContextRGBA should return *gg.Context backed by *image.RGBA (gg.NewContext does).
|
|
e.drawer.DC = NewGGContextRGBA(canvasW, canvasH)
|
|
e.lastCanvasW = canvasW
|
|
e.lastCanvasH = canvasH
|
|
// e.wp.CameraXWorldFp = e.wp.ViewportWidthPx / 2 * world.SCALE
|
|
// e.wp.CameraYWorldFp = e.wp.ViewportHeightPx / 2 * world.SCALE
|
|
}
|
|
|
|
func (e *editor) ensureViewportBuffer(w, h int) {
|
|
if e.viewportImg != nil && e.viewportW == w && e.viewportH == h {
|
|
return
|
|
}
|
|
e.viewportImg = image.NewRGBA(image.Rect(0, 0, w, h))
|
|
e.viewportW = w
|
|
e.viewportH = h
|
|
}
|
|
|
|
// copyViewportRGBA copies a viewport rectangle from src RGBA into dst RGBA.
|
|
// dst must be sized exactly (0,0)-(vw,vh). This is allocation-free.
|
|
// It avoids SubImage aliasing issues: dst becomes independent from src backing memory.
|
|
func copyViewportRGBA(dst, src *image.RGBA, marginX, marginY, vw, vh int) {
|
|
for y := 0; y < vh; y++ {
|
|
srcOff := (marginY+y)*src.Stride + marginX*4
|
|
dstOff := y * dst.Stride
|
|
n := vw * 4
|
|
copy(dst.Pix[dstOff:dstOff+n], src.Pix[srcOff:srcOff+n])
|
|
}
|
|
}
|
|
|
|
func NewGGContextRGBA(w, h int) *gg.Context {
|
|
return gg.NewContext(w, h)
|
|
}
|