229 lines
6.6 KiB
Go
229 lines
6.6 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
|
|
}
|
|
|
|
drawLinesFromPlan(drawer, plan, w.W, w.H)
|
|
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) {
|
|
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 {
|
|
segs := torusShortestLineSegments(l, worldW, worldH)
|
|
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)
|
|
}
|