fix(ui): packet flow lines meet the marks on the OLED and e-ink map

- The newest lines are three strokes centred on the line, not two to one
  side.
- A flow's points are drawn where the contacts and you are now, not where
  they were when it was heard; dots and lines round the same way.
- With the GPS off, the position set in Privacy is your mark on the map
  and where the lines end.
- Far ends are projected and clipped in floats: zoomed in, a far point no
  longer overflows an int and turns a line around.
- Rings are clipped to the map and as thick as a line; the scale and the
  north mark get a background; on e-ink the ring here stays 15 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-15 15:12:59 +02:00
co-authored by Claude Opus 5.5
parent 5f531fad60
commit 649d499d6f
2 changed files with 79 additions and 21 deletions
+26 -5
View File
@@ -6,6 +6,7 @@
#include <helpers/ui/DisplayDriver.h>
#include <stdlib.h> // abs
#include <math.h> // lroundf
#include "../Trail.h"
namespace gfx {
@@ -26,19 +27,21 @@ static inline void drawLine(DisplayDriver& d, int x0, int y0, int x1, int y1) {
}
// The part of a line inside a box (x, y, w, h), Liang-Barsky; nothing when
// it misses the box. For lines whose ends may lie far off the screen.
static inline void drawLineClipped(DisplayDriver& d, int x0, int y0, int x1, int y1, int bx, int by, int bw, int bh) {
// it misses the box. For lines whose ends may lie far off the screen: the
// ends are floats, so a point projected thousands of screens away (a map
// zoomed in) neither overflows an int nor turns the line around.
static inline void drawLineClipped(DisplayDriver& d, float x0, float y0, float x1, float y1, int bx, int by, int bw, int bh) {
float t0 = 0, t1 = 1;
const float dx = (float)(x1 - x0), dy = (float)(y1 - y0);
const float dx = x1 - x0, dy = y1 - y0;
const float p[4] = { -dx, dx, -dy, dy };
const float q[4] = { (float)(x0 - bx), (float)(bx + bw - 1 - x0), (float)(y0 - by), (float)(by + bh - 1 - y0) };
const float q[4] = { x0 - bx, bx + bw - 1 - x0, y0 - by, by + bh - 1 - y0 };
for (int i = 0; i < 4; i++) {
if (p[i] == 0) { if (q[i] < 0) return; continue; }
float r = q[i] / p[i];
if (p[i] < 0) { if (r > t1) return; if (r > t0) t0 = r; }
else { if (r < t0) return; if (r < t1) t1 = r; }
}
drawLine(d, x0 + (int)(t0 * dx), y0 + (int)(t0 * dy), x0 + (int)(t1 * dx), y0 + (int)(t1 * dy));
drawLine(d, (int)lroundf(x0 + t0 * dx), (int)lroundf(y0 + t0 * dy), (int)lroundf(x0 + t1 * dx), (int)lroundf(y0 + t1 * dy));
}
// Midpoint-circle outline.
@@ -55,6 +58,24 @@ static inline void drawCircle(DisplayDriver& d, int cx, int cy, int r) {
}
}
// The same outline, only the part inside a box (x, y, w, h), as thick as a
// line (sepH: 2px on a doubled panel).
static inline void drawCircleClipped(DisplayDriver& d, int cx, int cy, int r, int bx, int by, int bw, int bh) {
const int t = d.sepH();
auto dot = [&](int x, int y) {
if (x - t / 2 >= bx && y - t / 2 >= by && x - t / 2 + t <= bx + bw && y - t / 2 + t <= by + bh)
d.fillRect(x - t / 2, y - t / 2, t, t);
};
int x = r, y = 0, err = 1 - r;
while (x >= y) {
dot(cx + x, cy + y); dot(cx + y, cy + x); dot(cx - y, cy + x); dot(cx - x, cy + y);
dot(cx - x, cy - y); dot(cx - y, cy - x); dot(cx + y, cy - x); dot(cx + x, cy - y);
y++;
if (err < 0) err += 2 * y + 1;
else { x--; err += 2 * (y - x) + 1; }
}
}
// Walks a TrailStore and renders it as a connected polyline via drawLine(),
// breaking the line at each TRAIL_FLAG_SEG_START point (trail paused/
// restarted) instead of bridging the dead-time gap. Shared by the full Trail
+53 -16
View File
@@ -287,7 +287,9 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
bool _nav = false; // full-screen navigate-to-node view (over detail)
bool _map = false; // the nodes with a position on a map (under detail)
static const uint32_t FLOW_SHOW_MS = 60000; // packet flow lines stay this long
static const uint32_t FLOW_HERE_MS = 3000; // and a ring here, for one placed nowhere else
// and a ring here, for one placed nowhere else: on e-ink longer, each
// change being a refresh
static const uint32_t FLOW_HERE_MS = Features::IS_EINK ? 15000 : 3000;
uint32_t _flow_seen = 0;
int _map_zoom = 0; // 0: all of them in view; each step halves it, on the selected
navview::EtaTracker _nav_eta; // closing-speed/ETA for the navigate view
@@ -904,8 +906,12 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
if (la < mnla) mnla = la; if (la > mxla) mxla = la;
if (lo < mnlo) mnlo = lo; if (lo > mxlo) mxlo = lo;
};
// You: the GPS, else the position set in Privacy -- where the packet
// flow's lines end too.
int32_t mla = _own_lat, mlo = _own_lon;
const bool me = _own_gps || _task->core().fixedPosition(mla, mlo);
for (int i = 0; i < _count; i++) if (hasPos(_entries[i])) fold(_entries[i].lat_e6, _entries[i].lon_e6);
if (_own_gps) fold(_own_lat, _own_lon);
if (me) fold(mla, mlo);
if (!init || _sel >= _count || !hasPos(_entries[_sel])) {
d.drawTextCentered(W / 2, H / 2 - lh / 2, "No node positions");
return;
@@ -922,9 +928,14 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
float scale = fh / span_la < fw / span_lo ? fh / span_la : fw / span_lo;
float cla = (mnla + mxla) / 2.0f, clo = (mnlo + mxlo) / 2.0f;
if (_map_zoom > 0) { scale *= (float)(1 << _map_zoom); cla = sel.lat_e6; clo = sel.lon_e6; }
auto project = [&](int32_t la, int32_t lo, int& x, int& y) {
x = ax + aw / 2 + (int)(((float)lo - clo) * kx * scale);
y = ay + ah / 2 - (int)(((float)la - cla) * scale);
auto projectF = [&](int32_t la, int32_t lo, float& x, float& y) { // unrounded, for far ends
x = ax + aw / 2 + ((float)lo - clo) * kx * scale;
y = ay + ah / 2 - ((float)la - cla) * scale;
};
auto project = [&](int32_t la, int32_t lo, int& x, int& y) { // clamped far off the screen, never past an int
float fx, fy; projectF(la, lo, fx, fy);
x = (int)lroundf(fx < -30000 ? -30000 : fx > 30000 ? 30000 : fx); // rounded as the lines are
y = (int)lroundf(fy < -30000 ? -30000 : fy > 30000 ? 30000 : fy);
};
auto inside = [&](int x, int y) { return x >= ax && x < ax + aw && y >= ay && y < ay + ah; };
@@ -938,29 +949,51 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
miniIconDrawCentered(d, x, y, _entries[i].is_live ? ICON_MAP_CONTACT : ICON_MAP_DOT);
}
// Packet flow (ui-core/PacketFlow.h): the lines of the packets heard in
// the last minute, the newest drawn twice as thick; a ring for a packet
// sent, or heard on its way to a repeater.
// the last minute, the newest two drawn three strokes wide, centred on
// the line; a ring for a packet sent, or heard on its way to a repeater.
// Its points are drawn where the contacts and you are now, not where they
// were when it was heard, so the lines meet on the marks.
const PacketFlow& fl = _task->core().flow;
for (int i = 0; fl.enabled() && i < fl.count(); i++) {
const int t = d.sepH();
for (int i = 0, lines = 0; fl.enabled() && i < fl.count(); i++) {
const PacketFlow::Flow& f = fl.at(i);
if (millis() - f.t_ms > FLOW_SHOW_MS) break; // newest first
int px[PacketFlow::MAX_PTS], py[PacketFlow::MAX_PTS];
for (int k = 0; k < f.n; k++) project(f.lat[k], f.lon[k], px[k], py[k]);
float px[PacketFlow::MAX_PTS], py[PacketFlow::MAX_PTS];
for (int k = 0; k < f.n; k++) {
int32_t la = f.lat[k], lo = f.lon[k];
ContactInfo c;
if (f.node[k] >= 0) { // a contact: where its dot is now
if (the_mesh.getContactByIdx(f.node[k], c) && (c.gps_lat || c.gps_lon)) { la = c.gps_lat; lo = c.gps_lon; }
} else if (me && k == f.n - 1) { // the last point, no contact: here
la = mla; lo = mlo;
}
projectF(la, lo, px[k], py[k]);
}
if (f.n == 1) { // a packet nothing of which could be placed: a ring here, briefly
if (f.here && millis() - f.t_ms > FLOW_HERE_MS) continue;
if (inside(px[0], py[0])) gfx::drawCircle(d, px[0], py[0], 6 * s);
if (fabsf(px[0]) < 30000 && fabsf(py[0]) < 30000)
gfx::drawCircleClipped(d, (int)lroundf(px[0]), (int)lroundf(py[0]), 6 * s, ax, ay, aw, ah);
continue;
}
const bool bold = i < 2;
const bool bold = lines++ < 2;
for (int k = 1; k < f.n; k++) {
gfx::drawLineClipped(d, px[k - 1], py[k - 1], px[k], py[k], ax, ay, aw, ah);
if (bold) gfx::drawLineClipped(d, px[k - 1] + 1, py[k - 1], px[k] + 1, py[k], ax, ay, aw, ah);
if (!bold) continue;
// a stroke either side of the first, across the line's run
const bool flat = fabsf(px[k] - px[k - 1]) >= fabsf(py[k] - py[k - 1]);
const float ox = flat ? 0 : t, oy = flat ? t : 0;
gfx::drawLineClipped(d, px[k - 1] + ox, py[k - 1] + oy, px[k] + ox, py[k] + oy, ax, ay, aw, ah);
gfx::drawLineClipped(d, px[k - 1] - ox, py[k - 1] - oy, px[k] - ox, py[k] - oy, ax, ay, aw, ah);
}
}
if (_own_gps) { int x, y; project(_own_lat, _own_lon, x, y); if (inside(x, y)) miniIconDrawCentered(d, x, y, ICON_MAP_CURRENT); }
if (target) { int x, y; project(tla, tlo, x, y); if (inside(x, y)) miniIconDrawCentered(d, x, y, ICON_MAP_TARGET); }
if (me) { int x, y; project(mla, mlo, x, y); if (inside(x, y)) miniIconDrawCentered(d, x, y, ICON_MAP_CURRENT); }
if (target) { int x, y; project(tla, tlo, x, y); if (inside(x, y)) miniIconDrawCentered(d, x, y, ICON_MAP_TARGET); }
const int r = 4 * s;
d.drawRect(sx - r, sy - r, 2 * r + 1, 2 * r + 1);
// The north mark and the scale stay readable over the lines.
d.setColor(UIColor::window_bkg);
d.fillRect(ax + aw - ICON_MAP_NORTH.w * s - 2, ay, ICON_MAP_NORTH.w * s + 2, ICON_MAP_NORTH.h * s + 1);
d.setColor(UIColor::primary_txt);
miniIconDrawTop(d, ax + aw - ICON_MAP_NORTH.w * s - 1, ay, ICON_MAP_NORTH);
// The scale: a round distance about a quarter of the width, bottom right.
@@ -972,7 +1005,11 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
char sc[12];
geo::fmtDist(sc, sizeof(sc), m / 1000.0f, useImperial());
if (len > 2 && len < aw) {
const int ly = ay + ah - 2, lx = ax + aw - len - 1;
const int ly = ay + ah - 2, lx = ax + aw - len - 1, tw = d.getTextWidth(sc);
d.setColor(UIColor::window_bkg);
d.fillRect(lx + len - tw - 1, ly - lh - 1, tw + 2, lh + 1);
d.fillRect(lx - 1, ly - 1, len + 2, s + 2);
d.setColor(UIColor::primary_txt);
d.fillRect(lx, ly, len, s);
d.setCursor(lx + len - d.getTextWidth(sc), ly - lh);
d.print(sc);