Files
galaxy-game/client/world/renderer_lines.go
T
2026-03-07 12:35:18 +03:00

236 lines
6.8 KiB
Go

package world
// renderLinesStageB performs a full expanded-canvas redraw but renders ONLY Line primitives.
// It uses the Stage A render plan: tiles + per-tile clip + per-tile candidates.
// func (w *World) renderLinesStageB(drawer PrimitiveDrawer, params RenderParams) error {
// plan, err := w.buildRenderPlanStageA(params)
// if err != nil {
// return err
// }
// allowWrap := params.Options == nil || !params.Options.DisableWrapScroll
// drawLinesFromPlan(drawer, plan, w.W, w.H, allowWrap)
// return nil
// }
// lineSeg is one canonical segment (endpoints in [0..W) x [0..H)) to be drawn.
// It represents part of the torus-shortest polyline for a Line primitive after wrap splitting.
type lineSeg struct {
x1, y1 int
x2, y2 int
}
// drawLinesFromPlan executes a lines-only draw from an already built render plan.
func drawLinesFromPlan(drawer PrimitiveDrawer, plan RenderPlan, worldW, worldH int, allowWrap bool) {
for _, td := range plan.Tiles {
if td.ClipW <= 0 || td.ClipH <= 0 {
continue
}
// Filter only lines; skip tiles that have no lines.
lines := make([]Line, 0, len(td.Candidates))
for _, it := range td.Candidates {
l, ok := it.(Line)
if !ok {
continue
}
lines = append(lines, l)
}
if len(lines) == 0 {
continue
}
// Collect segments that actually intersect this tile's canonical rect.
segsToDraw := make([]lineSeg, 0, len(lines))
for _, l := range lines {
var segs []lineSeg
if allowWrap {
segs = torusShortestLineSegments(l, worldW, worldH)
} else {
segs = []lineSeg{{x1: l.X1, y1: l.Y1, x2: l.X2, y2: l.Y2}}
}
for _, s := range segs {
if segmentIntersectsRect(s, td.Tile.Rect) {
segsToDraw = append(segsToDraw, s)
}
}
}
if len(segsToDraw) == 0 {
continue
}
drawer.Save()
drawer.ClipRect(float64(td.ClipX), float64(td.ClipY), float64(td.ClipW), float64(td.ClipH))
for _, s := range segsToDraw {
// Project endpoints for this tile copy by adding tile offset.
x1Px := worldSpanFixedToCanvasPx((s.x1+td.Tile.OffsetX)-plan.WorldRect.minX, plan.ZoomFp)
y1Px := worldSpanFixedToCanvasPx((s.y1+td.Tile.OffsetY)-plan.WorldRect.minY, plan.ZoomFp)
x2Px := worldSpanFixedToCanvasPx((s.x2+td.Tile.OffsetX)-plan.WorldRect.minX, plan.ZoomFp)
y2Px := worldSpanFixedToCanvasPx((s.y2+td.Tile.OffsetY)-plan.WorldRect.minY, plan.ZoomFp)
drawer.AddLine(float64(x1Px), float64(y1Px), float64(x2Px), float64(y2Px))
}
drawer.Stroke()
drawer.Restore()
}
}
// torusShortestLineSegments converts a Line primitive into 1..4 canonical segments
// inside [0..worldW) x [0..worldH) that represent the torus-shortest polyline.
//
// IMPORTANT: when a segment crosses a torus boundary, the second segment starts
// on the opposite boundary (e.g. x=0 jump to x=worldW), preserving continuity on the torus.
// We must NOT wrap endpoints independently at the end, otherwise the topology changes.
func torusShortestLineSegments(l Line, worldW, worldH int) []lineSeg {
// Step 1: choose the torus-shortest representation in unwrapped space.
ax, bx := shortestWrappedDelta(l.X1, l.X2, worldW)
ay, by := shortestWrappedDelta(l.Y1, l.Y2, worldH)
// Step 2: shift so that A is inside canonical [0..W) x [0..H).
shiftX := floorDiv(ax, worldW) * worldW
shiftY := floorDiv(ay, worldH) * worldH
ax -= shiftX
bx -= shiftX
ay -= shiftY
by -= shiftY
segs := []lineSeg{{x1: ax, y1: ay, x2: bx, y2: by}}
// Step 3: split by X boundary if needed (jump-aware).
segs = splitSegmentsByX(segs, worldW)
// Step 4: split by Y boundary if needed (jump-aware).
segs = splitSegmentsByY(segs, worldH)
// Now all segments are canonical and torus-continuous. Endpoints may legally be 0 or worldW/worldH.
return segs
}
func splitSegmentsByX(segs []lineSeg, worldW int) []lineSeg {
out := make([]lineSeg, 0, len(segs)*2)
for _, s := range segs {
x1, y1, x2, y2 := s.x1, s.y1, s.x2, s.y2
// After normalization, x1 is expected inside [0..worldW). Only x2 may be outside.
if x2 >= 0 && x2 < worldW {
out = append(out, s)
continue
}
dx := x2 - x1
dy := y2 - y1
if dx == 0 {
// Degenerate; keep as-is (should not happen with normalized x1 unless x2==x1).
out = append(out, s)
continue
}
if x2 >= worldW {
// Crosses the right boundary at x=worldW, then reappears at x=0.
bx := worldW
num := bx - x1
iy := y1 + (dy*num)/dx
// Segment 1: [x1..worldW]
// Segment 2: [0..x2-worldW]
s1 := lineSeg{x1: x1, y1: y1, x2: worldW, y2: iy}
s2 := lineSeg{x1: 0, y1: iy, x2: x2 - worldW, y2: y2}
out = append(out, s1, s2)
continue
}
// x2 < 0: crosses the left boundary at x=0, then reappears at x=worldW.
bx := 0
num := bx - x1
iy := y1 + (dy*num)/dx
// Segment 1: [x1..0]
// Segment 2: [worldW..x2+worldW]
s1 := lineSeg{x1: x1, y1: y1, x2: 0, y2: iy}
s2 := lineSeg{x1: worldW, y1: iy, x2: x2 + worldW, y2: y2}
out = append(out, s1, s2)
}
return out
}
func splitSegmentsByY(segs []lineSeg, worldH int) []lineSeg {
out := make([]lineSeg, 0, len(segs)*2)
for _, s := range segs {
x1, y1, x2, y2 := s.x1, s.y1, s.x2, s.y2
// After normalization, y1 is expected inside [0..worldH). Only y2 may be outside.
if y2 >= 0 && y2 < worldH {
out = append(out, s)
continue
}
dx := x2 - x1
dy := y2 - y1
if dy == 0 {
out = append(out, s)
continue
}
if y2 >= worldH {
// Crosses the top boundary at y=worldH, then reappears at y=0.
by := worldH
num := by - y1
ix := x1 + (dx*num)/dy
s1 := lineSeg{x1: x1, y1: y1, x2: ix, y2: worldH}
s2 := lineSeg{x1: ix, y1: 0, x2: x2, y2: y2 - worldH}
out = append(out, s1, s2)
continue
}
// y2 < 0: crosses the bottom boundary at y=0, then reappears at y=worldH.
by := 0
num := by - y1
ix := x1 + (dx*num)/dy
s1 := lineSeg{x1: x1, y1: y1, x2: ix, y2: 0}
s2 := lineSeg{x1: ix, y1: worldH, x2: x2, y2: y2 + worldH}
out = append(out, s1, s2)
}
return out
}
// segmentIntersectsRect is a coarse bbox intersection check between a segment and a half-open rect.
// It is used only as an optimization to avoid drawing clearly irrelevant segments.
//
// NOTE: Segment endpoints may legally be exactly on the world boundary (x==worldW or y==worldH).
// Rect is half-open [min, max), so we treat the segment bbox as inclusive and intersect it with
// [r.min, r.max-1] to avoid dropping boundary-touching segments.
func segmentIntersectsRect(s lineSeg, r Rect) bool {
minX := min(s.x1, s.x2)
maxX := max(s.x1, s.x2)
minY := min(s.y1, s.y2)
maxY := max(s.y1, s.y2)
// Treat degenerate as having 1-unit thickness for indexing-like behavior.
if minX == maxX {
maxX++
}
if minY == maxY {
maxY++
}
// Rect inclusive bounds for intersection purposes.
rMaxX := r.maxX - 1
rMaxY := r.maxY - 1
if rMaxX < r.minX || rMaxY < r.minY {
return false
}
// Inclusive overlap check.
return !(maxX-1 < r.minX || minX > rMaxX || maxY-1 < r.minY || minY > rMaxY)
}