Корректное отображение витков

This commit is contained in:
Дмитрий Соловьев
2026-06-01 13:57:13 +03:00
parent e5fd2a4586
commit 2bdc9ad64f
6 changed files with 66 additions and 19 deletions
@@ -296,16 +296,25 @@ export function Tgu2DMapView({
>
<canvas className="tgu-2d-map__canvas" ref={canvasRef} />
<svg className="tgu-2d-map__overlay" width={size.width} height={size.height} aria-hidden="true">
{overlayPolygons.map((polygon) => (
<polygon
key={polygon.id}
points={polygon.points.map((point) => {
const screen = projection.project(point);
return `${screen.x},${screen.y}`;
}).join(" ")}
className={polygon.id === selectedPolygon?.id ? "is-selected" : "is-hovered"}
/>
))}
{overlayPolygons.flatMap((polygon) => {
const raw = polygon.points.map((point) => projection.project(point));
const pts = unwrapX(raw, projection.worldSize);
const cls = polygon.id === selectedPolygon?.id ? "is-selected" : "is-hovered";
return ([0, projection.worldSize, -projection.worldSize] as const).flatMap((dx) => {
const visible = pts.some((p) => {
const x = p.x + dx;
return x >= -64 && x <= size.width + 64 && p.y >= -64 && p.y <= size.height + 64;
});
if (!visible) return [];
return (
<polygon
key={`${polygon.id}:${dx}`}
points={pts.map((p) => `${p.x + dx},${p.y}`).join(" ")}
className={cls}
/>
);
});
})}
{targets.map((t) => {
const screen = projection.project(t);
const isSelected = t.id === selectedTargetId;
@@ -365,6 +374,20 @@ function layerTitle(layer: MapPolygon["layer"]): string {
}
}
function unwrapX(points: { x: number; y: number }[], worldSize: number): { x: number; y: number }[] {
if (points.length === 0) return [];
const out: { x: number; y: number }[] = [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;
}
function uniquePolygons(polygons: Array<MapPolygon | undefined>): MapPolygon[] {
const result: MapPolygon[] = [];
const ids = new Set<string>();
@@ -24,10 +24,21 @@ export function hitTestPolygons(index: PolygonSpatialIndex, point: GeoPoint): Ma
let selected: MapPolygon | undefined;
for (const entry of index.entries) {
if (!bboxContainsPoint(entry.bbox, point)) continue;
if (!pointInPolygon(point, entry.polygon.points)) continue;
// Check bbox allowing ±360° shifts so antimeridian-crossing polygons are not skipped.
const inBbox =
bboxContainsPoint(entry.bbox, point) ||
bboxContainsPoint(entry.bbox, { lon: point.lon + 360, lat: point.lat }) ||
bboxContainsPoint(entry.bbox, { lon: point.lon - 360, lat: point.lat });
if (!inBbox) continue;
if (!selected || entry.polygon.zIndex >= selected.zIndex) {
// Normalize polygon lons to be continuous (same idea as unwrapX for screen coords).
const normalized = unwrapLon(entry.polygon.points);
const hit =
pointInPolygon(point, normalized) ||
pointInPolygon({ lon: point.lon + 360, lat: point.lat }, normalized) ||
pointInPolygon({ lon: point.lon - 360, lat: point.lat }, normalized);
if (hit && (!selected || entry.polygon.zIndex >= selected.zIndex)) {
selected = entry.polygon;
}
}
@@ -42,3 +53,17 @@ export function queryPolygonsByBBox(index: PolygonSpatialIndex, bbox: BBox): Map
.map((entry) => entry.polygon);
}
function unwrapLon(points: GeoPoint[]): GeoPoint[] {
if (points.length === 0) return points;
const out: GeoPoint[] = [points[0]];
let prevLon = points[0].lon;
for (let i = 1; i < points.length; i++) {
let lon = points[i].lon;
while (lon - prevLon > 180) lon -= 360;
while (prevLon - lon > 180) lon += 360;
out.push({ ...points[i], lon });
prevLon = lon;
}
return out;
}