diff --git a/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/canvas/drawPlanWorks.ts b/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/canvas/drawPlanWorks.ts index b1bbc57..e42ca6f 100644 --- a/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/canvas/drawPlanWorks.ts +++ b/services/pcp-tgu-ops-ui/src/features/tgu-map-2d/canvas/drawPlanWorks.ts @@ -2,6 +2,26 @@ import { statusStyle } from "../../tgu-planning/tguStatus"; import type { MapProjection } from "../geometry/mapProjection"; import type { MapPolygon } from "../model/mapTypes"; +type Pt = { x: number; y: number }; + +/** + * «Разворачивает» X-координаты так, чтобы соседние точки не расходились больше чем на + * полмира — иначе полигон, пересекающий антимеридиан (180°), рисуется через всю карту. + */ +function unwrapX(points: Pt[], worldSize: number): Pt[] { + if (points.length === 0) return []; + const out: Pt[] = [points[0]]; + let prevX = points[0].x; + for (let i = 1; i < points.length; i++) { + let x = points[i].x; + while (x - prevX > worldSize / 2) x -= worldSize; + while (prevX - x > worldSize / 2) x += worldSize; + out.push({ x, y: points[i].y }); + prevX = x; + } + return out; +} + export function drawPlanWorks( ctx: CanvasRenderingContext2D, polygons: MapPolygon[], @@ -11,26 +31,31 @@ export function drawPlanWorks( ctx.save(); for (const polygon of polygons) { if (polygon.points.length < 3) continue; - const screenPoints = polygon.points.map((point) => projection.project(point)); - if (!screenPoints.some((point) => point.x >= -32 && point.x <= viewport.width + 32 && point.y >= -32 && point.y <= viewport.height + 32)) { - continue; - } + const raw = polygon.points.map((point) => projection.project(point)); + const pts = unwrapX(raw, projection.worldSize); const color = statusStyle(polygon.status || "PLANNED").color; - ctx.beginPath(); - screenPoints.forEach((point, index) => { - if (index === 0) { - ctx.moveTo(point.x, point.y); - } else { - ctx.lineTo(point.x, point.y); - } - }); - ctx.closePath(); ctx.fillStyle = hexToRgba(color, 0.2); ctx.strokeStyle = hexToRgba(color, 0.72); ctx.lineWidth = 1.2; - ctx.fill(); - ctx.stroke(); + + // Рисуем полигон в трёх копиях мира (текущей и соседних), чтобы он был виден + // независимо от того, какая копия попала в видимую область при зуме/панораме. + for (const dx of [0, projection.worldSize, -projection.worldSize]) { + const visible = pts.some((point) => { + const x = point.x + dx; + return x >= -32 && x <= viewport.width + 32 && point.y >= -32 && point.y <= viewport.height + 32; + }); + if (!visible) continue; + ctx.beginPath(); + for (let i = 0; i < pts.length; i++) { + if (i === 0) ctx.moveTo(pts[i].x + dx, pts[i].y); + else ctx.lineTo(pts[i].x + dx, pts[i].y); + } + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } } ctx.restore(); }