package world // worldFixedToCameraZoom converts a fixed-point zoom value back into the // UI-facing floating-point representation where 1.0 means neutral zoom. func worldFixedToCameraZoom(zoomFp int) float64 { return float64(zoomFp) / float64(SCALE) } // requiredZoomToFitWorld returns the minimum fixed-point zoom needed so that // a viewport span of viewportSpanPx pixels does not exceed a world span of // worldSpanFp fixed-point units. // // The result is rounded up, not down, because the fit constraint must be // satisfied conservatively: after correction, the visible world span must // never be larger than the actual world span. func requiredZoomToFitWorld(viewportSpanPx, worldSpanFp int) int { if viewportSpanPx < 0 { panic("requiredZoomToFitWorld: negative viewport span") } if worldSpanFp <= 0 { panic("requiredZoomToFitWorld: non-positive world span") } if viewportSpanPx == 0 { return 0 } return ceilDiv(viewportSpanPx*SCALE*SCALE, worldSpanFp) } // correctCameraZoomFp corrects a fixed-point zoom value using two groups // of constraints: // // 1. Fit-to-world constraints derived from viewport and world sizes. // These have the highest priority and prevent the viewport from becoming // larger than the world on any axis, which would otherwise expose wrap // on the visible user area. // // 2. Optional UI zoom bounds [minZoomFp, maxZoomFp]. // A zero bound means "ignore this bound". // If fit-to-world requires a zoom larger than maxZoomFp, the fit constraint // wins and maxZoomFp is ignored for that case. // // The function returns either the corrected zoom or currentZoomFp unchanged // when no correction is required. func correctCameraZoomFp( currentZoomFp int, viewportWidthPx, viewportHeightPx int, worldWidthFp, worldHeightFp int, minZoomFp, maxZoomFp int, ) int { if currentZoomFp <= 0 { panic("correctCameraZoomFp: non-positive current zoom") } if viewportWidthPx < 0 || viewportHeightPx < 0 { panic("correctCameraZoomFp: negative viewport size") } if worldWidthFp <= 0 || worldHeightFp <= 0 { panic("correctCameraZoomFp: non-positive world size") } if minZoomFp < 0 || maxZoomFp < 0 { panic("correctCameraZoomFp: negative zoom bound") } if minZoomFp > 0 && maxZoomFp > 0 && minZoomFp > maxZoomFp { panic("correctCameraZoomFp: min zoom greater than max zoom") } // Start from the user zoom. result := currentZoomFp // Apply min bound first (only increases zoom, always valid). if minZoomFp > 0 && result < minZoomFp { result = minZoomFp } // Apply max bound tentatively. This can be overridden later by the anti-wrap constraint. if maxZoomFp > 0 && result > maxZoomFp { result = maxZoomFp } // If viewport is larger than the world on any axis at the current result zoom, // increase zoom to the minimum value that prevents wrap in the visible area. requiredFitX := requiredZoomToFitWorld(viewportWidthPx, worldWidthFp) requiredFitY := requiredZoomToFitWorld(viewportHeightPx, worldHeightFp) requiredFit := max(requiredFitX, requiredFitY) if requiredFit > 0 && result < requiredFit { result = requiredFit } // Re-apply max bound only if it does not conflict with the anti-wrap requirement. // If anti-wrap requires zoom > maxZoomFp, anti-wrap wins. if maxZoomFp > 0 && result > maxZoomFp && requiredFit <= maxZoomFp { result = maxZoomFp } return result } // CorrectCameraZoom adapts fixed-point zoom correction for UI code. // // currentZoom is the user-facing zoom multiplier in floating-point form. // The result is returned in the same representation. func (w *World) CorrectCameraZoom( currentZoom float64, viewportWidthPx int, viewportHeightPx int, ) float64 { currentZoomFp := mustCameraZoomToWorldFixed(currentZoom) correctedZoomFp := correctCameraZoomFp( currentZoomFp, viewportWidthPx, viewportHeightPx, w.W, w.H, MIN_ZOOM, MAX_ZOOM, ) return worldFixedToCameraZoom(correctedZoomFp) }