package world import ( "errors" "fmt" "math" ) var ( errInvalidCameraZoom = errors.New("invalid camera zoom") ) const ( // SCALE is the fixed-point multiplier used across the package. // A real value of 1.0 is represented as SCALE. SCALE = 1000 // MIN_ZOOM and MAX_ZOOM define the supported zoom range in fixed-point form. // They are reserved for future validation/clamping logic. MIN_ZOOM = int(SCALE / 4) // 0.25x MAX_ZOOM = int(SCALE * 32) // 32x // cellSizeMin and cellSizeMax bound the automatically selected grid cell size. cellSizeMin = 16 * SCALE cellSizeMax = 512 * SCALE ) // Rect is a half-open rectangle in fixed-point world coordinates: // [minX, maxX) x [minY, maxY). type Rect struct { minX, maxX int minY, maxY int } // wrap maps value into the half-open interval [0, size). // It supports negative input values and is used for torus coordinates. func wrap(value, size int) int { r := value % size if r < 0 { r += size } return r } // clamp limits value to the closed interval [minValue, maxValue]. func clamp(value, minValue, maxValue int) int { if value < minValue { return minValue } if value > maxValue { return maxValue } return value } // ceilDiv returns ceil(a / b) for positive integers. func ceilDiv(a, b int) int { return (a + b - 1) / b } // floorDiv returns floor(a / b) for b > 0 and supports negative a. func floorDiv(a, b int) int { if b <= 0 { panic("floorDiv: non-positive divisor") } q := a / b r := a % b if r != 0 && a < 0 { q-- } return q } // fixedPoint converts a real value into the package fixed-point representation // using nearest-integer rounding. func fixedPoint(v float64) int { return int(math.Round(v * SCALE)) } // abs returns the absolute value of v. func abs(v int) int { if v < 0 { return -v } return v } // viewportPxToWorldFixed converts a viewport size in pixels into the visible // world size in fixed-point coordinates for the given fixed-point zoom. func viewportPxToWorldFixed(viewportWidthPx, viewportHeightPx, cameraZoomFp int) (int, int) { return PixelSpanToWorldFixed(viewportWidthPx, cameraZoomFp), PixelSpanToWorldFixed(viewportHeightPx, cameraZoomFp) } // worldToCell maps a world coordinate to a grid cell index. // The input coordinate is wrapped on the torus before the cell is computed. // The function panics when the grid configuration is invalid. func worldToCell(value, worldSize, cells, cellSize int) int { if cells <= 0 || cellSize <= 0 { panic(fmt.Sprintf("worldToCell: cells=%d cellSize=%d", cells, cellSize)) } wrappedValue := wrap(value, worldSize) cell := wrappedValue / cellSize if cell >= cells { cell = cells - 1 } return cell } // splitByWrap splits a half-open rectangle by torus wrap into 1..4 rectangles // fully contained inside [0, W) x [0, H). func splitByWrap(W, H, minX, maxX, minY, maxY int) []Rect { width := maxX - minX height := maxY - minY if width <= 0 || height <= 0 { return nil } if width >= W { minX = 0 maxX = W } if height >= H { minY = 0 maxY = H } type xPart struct { minX, maxX int } xParts := make([]xPart, 0, 2) if minX >= 0 && maxX <= W { xParts = append(xParts, xPart{minX: minX, maxX: maxX}) } else { wrappedMinX := wrap(minX, W) wrappedMaxX := wrap(maxX, W) if wrappedMinX < wrappedMaxX { xParts = append(xParts, xPart{minX: wrappedMinX, maxX: wrappedMaxX}) } else { xParts = append(xParts, xPart{minX: wrappedMinX, maxX: W}) if wrappedMaxX > 0 { xParts = append(xParts, xPart{minX: 0, maxX: wrappedMaxX}) } } } result := make([]Rect, 0, 4) for _, xp := range xParts { if minY >= 0 && maxY <= H { result = append(result, Rect{ minX: xp.minX, maxX: xp.maxX, minY: minY, maxY: maxY, }) continue } wrappedMinY := wrap(minY, H) wrappedMaxY := wrap(maxY, H) if wrappedMinY < wrappedMaxY { result = append(result, Rect{ minX: xp.minX, maxX: xp.maxX, minY: wrappedMinY, maxY: wrappedMaxY, }) } else { result = append(result, Rect{ minX: xp.minX, maxX: xp.maxX, minY: wrappedMinY, maxY: H, }) if wrappedMaxY > 0 { result = append(result, Rect{ minX: xp.minX, maxX: xp.maxX, minY: 0, maxY: wrappedMaxY, }) } } } return result } // PixelSpanToWorldFixed converts a span in pixels into a span in fixed-point // world coordinates for the given fixed-point zoom. func PixelSpanToWorldFixed(spanPx int, zoomFp int) int { return (spanPx * SCALE * SCALE) / zoomFp } // shortestWrappedDelta returns a canonical torus representation of the pair // (from, to) along a single axis of length size. // // The resulting delta (b - a) is normalized into the half-open interval // [-size/2, size/2). This makes the tie-case deterministic: // when the points are exactly half a world apart, wrap is always applied in // the direction that produces the negative delta. func shortestWrappedDelta(from, to, size int) (a int, b int) { a, b = from, to delta := to - from half := size / 2 if delta >= half { a += size return } if delta < -half { b += size return } return } // CameraZoomToWorldFixed converts a UI-facing zoom multiplier into the package // fixed-point representation used by world-space calculations. // // The input zoom is expected to be a finite positive real value where 1.0 means // the neutral zoom level. The result is rounded to the nearest fixed-point value. // // An error is returned when the input is invalid or when rounding would produce // a non-positive fixed-point zoom. func CameraZoomToWorldFixed(cameraZoom float64) (int, error) { if cameraZoom <= 0 || math.IsNaN(cameraZoom) || math.IsInf(cameraZoom, 0) { return 0, errInvalidCameraZoom } zoomFp := int(math.Round(cameraZoom * SCALE)) if zoomFp <= 0 { return 0, errInvalidCameraZoom } return zoomFp, nil } // mustCameraZoomToWorldFixed is the panic-on-error variant of // cameraZoomToWorldFixed. It is intended for internal code paths where invalid // zoom is considered a programmer or integration error and must fail fast. func mustCameraZoomToWorldFixed(cameraZoom float64) int { zoomFp, err := CameraZoomToWorldFixed(cameraZoom) if err != nil { panic(err) } return zoomFp }