client refactor
This commit is contained in:
+82
-36
@@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"github.com/fogleman/gg"
|
||||
@@ -39,13 +40,8 @@ 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 {
|
||||
func (e *client) GetParams() world.RenderParams {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
return *e.wp
|
||||
@@ -53,70 +49,83 @@ func (e *editor) GetParams() world.RenderParams {
|
||||
|
||||
// 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)) {
|
||||
func (e *client) 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
|
||||
p := *e.wp
|
||||
e.mu.Unlock()
|
||||
|
||||
e.co.Request(*p)
|
||||
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() {
|
||||
func (e *client) RequestRefresh() {
|
||||
e.mu.RLock()
|
||||
p := e.wp
|
||||
p := *e.wp
|
||||
e.mu.RUnlock()
|
||||
e.co.Request(*p)
|
||||
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 {
|
||||
func (e *client) draw(wPx, hPx int) image.Image {
|
||||
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 {
|
||||
func (e *client) renderRasterImage(viewportW, viewportH int, p world.RenderParams) image.Image {
|
||||
if e.world == nil {
|
||||
return blankImage
|
||||
return image.NewRGBA(image.Rect(0, 0, 0, 0))
|
||||
}
|
||||
// 1) Viewport sizes come from raster draw callback.
|
||||
|
||||
// Record current raster pixel size (used for event coordinate conversion).
|
||||
e.metaMu.Lock()
|
||||
e.lastRasterPxW = viewportW
|
||||
e.lastRasterPxH = viewportH
|
||||
e.metaMu.Unlock()
|
||||
|
||||
// Fill viewport/margins derived from 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
|
||||
// Correct zoom for viewport/world constraints, and clamp camera for no-wrap.
|
||||
p.CameraZoom = e.world.CorrectCameraZoom(p.CameraZoom, viewportW, viewportH)
|
||||
|
||||
// Ensure indexing is up-to-date when viewport size or zoom changes.
|
||||
zoomFp, err := p.CameraZoomFp()
|
||||
if err == nil {
|
||||
if viewportW != e.lastIndexedViewportW || viewportH != e.lastIndexedViewportH || zoomFp != e.lastIndexedZoomFp {
|
||||
e.world.IndexOnViewportChange(viewportW, viewportH, p.CameraZoom)
|
||||
e.lastIndexedViewportW = viewportW
|
||||
e.lastIndexedViewportH = viewportH
|
||||
e.lastIndexedZoomFp = zoomFp
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Ensure GG backing canvas is sized for expanded canvas.
|
||||
e.world.ClampRenderParamsNoWrap(&p)
|
||||
|
||||
// Ensure backing expanded canvas (gg context) is sized properly.
|
||||
canvasW := p.CanvasWidthPx()
|
||||
canvasH := p.CanvasHeightPx()
|
||||
e.ensureDrawerCanvas(canvasW, canvasH)
|
||||
|
||||
// Savety clamp
|
||||
e.world.ClampRenderParamsNoWrap(&p)
|
||||
// Render into expanded canvas backing.
|
||||
_ = e.world.Render(e.drawer, p) // TODO: handle error
|
||||
|
||||
// 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
|
||||
// Save snapshot of params actually used for this render (for HitTest consistency).
|
||||
e.lastRenderedMu.Lock()
|
||||
e.lastRenderedParams = p
|
||||
e.lastRenderedMu.Unlock()
|
||||
|
||||
// 5) Copy viewport ROI into reusable viewport buffer and return it.
|
||||
// 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))
|
||||
}
|
||||
|
||||
@@ -125,7 +134,7 @@ func (e *editor) renderRasterImage(viewportW, viewportH int, p world.RenderParam
|
||||
}
|
||||
|
||||
// ensureDrawerCanvas ensures drawer has a gg.Context sized to canvasW x canvasH.
|
||||
func (e *editor) ensureDrawerCanvas(canvasW, canvasH int) {
|
||||
func (e *client) ensureDrawerCanvas(canvasW, canvasH int) {
|
||||
if e.drawer.DC != nil && e.lastCanvasW == canvasW && e.lastCanvasH == canvasH {
|
||||
return
|
||||
}
|
||||
@@ -133,11 +142,9 @@ func (e *editor) ensureDrawerCanvas(canvasW, canvasH int) {
|
||||
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) {
|
||||
func (e *client) ensureViewportBuffer(w, h int) {
|
||||
if e.viewportImg != nil && e.viewportW == w && e.viewportH == h {
|
||||
return
|
||||
}
|
||||
@@ -146,6 +153,45 @@ func (e *editor) ensureViewportBuffer(w, h int) {
|
||||
e.viewportH = h
|
||||
}
|
||||
|
||||
func (e *client) getLastRenderedParams() world.RenderParams {
|
||||
e.lastRenderedMu.RLock()
|
||||
defer e.lastRenderedMu.RUnlock()
|
||||
return e.lastRenderedParams
|
||||
}
|
||||
|
||||
// eventPosToPixel converts event logical coordinates (Fyne units) into pixel coordinates,
|
||||
// using the last known raster logical size and the last draw callback pixel size.
|
||||
//
|
||||
// pixelX = floor(eventX * rasterPixelWidth / rasterLogicalWidth)
|
||||
func (e *client) eventPosToPixel(eventX, eventY float32) (xPx, yPx int, ok bool) {
|
||||
e.metaMu.RLock()
|
||||
logW := e.lastRasterLogicW
|
||||
logH := e.lastRasterLogicH
|
||||
pxW := e.lastRasterPxW
|
||||
pxH := e.lastRasterPxH
|
||||
e.metaMu.RUnlock()
|
||||
|
||||
if logW <= 0 || logH <= 0 || pxW <= 0 || pxH <= 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
x := int(math.Floor(float64(eventX) * float64(pxW) / float64(logW)))
|
||||
y := int(math.Floor(float64(eventY) * float64(pxH) / float64(logH)))
|
||||
|
||||
// Clamp to viewport bounds.
|
||||
if x < 0 {
|
||||
x = 0
|
||||
} else if x > pxW {
|
||||
x = pxW
|
||||
}
|
||||
if y < 0 {
|
||||
y = 0
|
||||
} else if y > pxH {
|
||||
y = pxH
|
||||
}
|
||||
return x, y, true
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
Reference in New Issue
Block a user