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) }