added auto measuraments
This commit is contained in:
1 parent
44fc4cdf36
commit
0eceaf537b
7 files changed
+2182
-55
No files matched your search
@@ -201,10 +201,94 @@ Two lines of the self-check output are assertions in disguise:
|
||||
2 kHz at about 6%. **A `y=[-1, 1]` there means the filter is gone** and the 2 kHz sine has
|
||||
aliased to a full-amplitude 125 Hz one, which looks entirely plausible on screen. This is
|
||||
the only check that distinguishes averaging from striding.
|
||||
- `[layout-probe] <action> rows=N buttons=B rows_y=[..] close=[..] add=[..] traces=[..]`
|
||||
- `[cursors-probe] <action> mode=.. on=D pane=D t0=.. t1=.. dt=.. T=[..] lines=N [..]`
|
||||
(stderr, `UDPSCOPE_PROBE_CURSORS_MS`) — walks `vertical` → `horizontal` → `both` →
|
||||
`mix`, then moves a marker *through the plot* (`Plot::set_cursor_x(..., Free)`, which is
|
||||
what a drag does) and reports again. **The times must survive every mode change**: they
|
||||
are the `RowSet`'s one pair, and a mode that parks the markers must not read them back
|
||||
(a parked cursor reports a real number, so a read-back would adopt it as a gesture — the
|
||||
bug that made `T0` read 0). **`lines`** must be 0 for `vertical`, one per row for
|
||||
`horizontal`/`both` (and the time row exists only in the modes that show the vertical
|
||||
pair — `T=[]` in `horizontal` is the assertion that it does not), and one per bound trace
|
||||
for `mix`; in `mix` the values must be the
|
||||
traces' own (`--` when a marker is past the newest sample, `partial` when only one
|
||||
marker is over samples). After the move, `t0` must have changed and every row's marker
|
||||
must have followed it (the report's `t0` is the shared value). **`marks`** is the `mix`
|
||||
mode's crossing annotations: **0 in every other mode, and one per bound trace for each
|
||||
marker that is over samples in `mix`** (so a move that brings the second marker into the
|
||||
data doubles it). The `first mark at x=.. y=..` line that follows must agree with the
|
||||
pane's *first* value for the same trace (the pane's lines are `chip Y1 Y2 dY`, with
|
||||
the values formatted as measurements -- two decimals, an SI prefix, the channel's unit --
|
||||
so the comparison is against the first number): both are read through the same
|
||||
`Plot::crosshair_value()`, and that line is the check that they stay the same number.
|
||||
A **ruler** drag is the other direction and the other hazard: the pane's `A`/`B` cells
|
||||
for a ruler row must change when the ruler moves (`after ruler drag` in the probe walks
|
||||
the first ruler to 80% of its y range), because nothing shares a ruler and the read-back
|
||||
in `RowSet::sync_cursors()` is the only thing that can notice. The dump's `cursors in plot
|
||||
0:` line is the other half: it reads the markers back out of
|
||||
the plot (`cursor(id).x/.visible`), which is what the renderer draws from — the pane's
|
||||
numbers and the plot's state agreeing is the difference between "the app placed them" and
|
||||
"the app placed them and they are on screen".
|
||||
- `[measures-probe]` + the dump's `measure N: 'sig' type thr=..% above=D show=D -> ok|invalid value text=[..]
|
||||
[duty=..] [marks=..]` and `measures pane: n=N revealed=D marks_shown=M pane_rows=R pane_w=W
|
||||
pane_h=H`
|
||||
(stderr, `UDPSCOPE_PROBE_MEASURES_MS`) — adds one measurement of every type on the first
|
||||
plottable signal and lets the tick compute them, so what it prints is what a user reads.
|
||||
On the test stream's 1 Hz ±1 sine the values must be: average ≈ 0, `top` ≈ 0.64 and
|
||||
`bottom` ≈ -0.64 (the mean of each half of a sine), max/min ≈ ±1, `pulse duration` ≈ 0.5 s
|
||||
with `duty` ≈ 0.5, `period` ≈ 1 s and `frequency` ≈ 1 Hz. `marks=` lists each mark with its kind, and the **mix is the point**:
|
||||
`level:V` alone for average/top/bottom, `level:V,time:T` for min/max (a ruler at the value
|
||||
*and* a marker at the first sample that reached it — two level marks at both extremes was
|
||||
the bug), `time:A,time:B` for pulse/period/frequency. `text=` is the pane's own string with
|
||||
its unit, so `frequency` must read `Hz` (not the signal's unit), `period`/`pulse duration`
|
||||
seconds, and the rest the channel's unit. `marks_shown` counts two per shown measurement
|
||||
(a min/max showing both its marks counts two of its own). `measures window:` is the
|
||||
timeliness assertion: **`drift` must stay at (or near) zero** -- the window the numbers
|
||||
were computed over against the one on screen -- and every *time* mark must lie inside it
|
||||
(`marks_in_window=K/K`). A measurement is recomputed when the window moves (2 % of its
|
||||
span) or the held/live state changes, never on a wall-clock timer: a timer is what let a
|
||||
marker sit outside the viewport and a dragged one survive a frame. **`marks_selectable` must be 0**: the
|
||||
measurement marks are created unselectable (`Plot::set_cursor_selectable()`, which the
|
||||
library gained for this), read back out of the plot here -- re-placing a marker every frame
|
||||
does **not** stop a drag, because the controller holds it for the gesture's life and the
|
||||
marker follows the pointer between frames. The library's own controller test
|
||||
(`an_unselectable_cursor_is_neither_hovered_nor_grabbed`) is the other half. The probe's last
|
||||
step still drags a mark *programmatically* and requires the next frame to put it back
|
||||
(`restored=1`), which now checks the placement path rather than the read-only property. The dialog report's `type= thr=
|
||||
side= show=` is the per-type behaviour: `period` and `frequency` must show **neither** the
|
||||
split nor the side row (they compute from the mid level's rising crossings, and an option
|
||||
there is one the user can only get wrong), `average`/`min`/`max` no split either, and only
|
||||
`pulse duration` a side. A measurement whose signal is not acquired, or
|
||||
a window with too few crossings, must read `invalid`/`--` rather than a made-up number
|
||||
(`frequency` on a window holding one edge is the case: `duty` is reported only when a
|
||||
period exists to divide by). The probe then **walks the dialog's lifecycle**: open the edit
|
||||
dialog on measurement 0, click its `Save`, open it again, click `Delete` — `measures=` must
|
||||
be unchanged after the save and one lower after the delete, with the process still alive.
|
||||
That is the check for the two things a dialog of this shape gets wrong: two buttons owning
|
||||
one context (a double free aborts on edit/delete, and only those paths have two buttons) and
|
||||
a `const char*` array built from a `std::vector<std::string>` as it grew (every pointer
|
||||
dangling, the first entry the most visibly). The report's `signals=[..]` and `drop_h` are the second and third:
|
||||
the list must be the **plotted traces** (real names, deduplicated, in signal-list order —
|
||||
*not* every channel, and never a stream's timestamp signal, which is what an acquired-but-
|
||||
unplotted list put first and auto-selected), and `drop_h` must be a combo box's height
|
||||
(tens of pixels — a scrolled-window wrapper asked it to grow to 160).
|
||||
- `[fit-probe] <action> window=.. x=[..] span=.. y=[..]` (stderr, `UDPSCOPE_PROBE_FIT_MS`) —
|
||||
the two fits, through the same `RowSet` calls the right-click menu wires. It first puts
|
||||
the y scale and the window where the data is *not* (a fixed `[-100, 100]`, a 50 ms
|
||||
window), so the assertions have something to be wrong about: **`fit x` must widen the
|
||||
window to the data's span and leave `y` exactly as it was**, and **`fit all` must then
|
||||
move `y` to the data's own range**. A `y` that changed on the `fit x` line is the two
|
||||
paths aliased.
|
||||
- `[layout-probe] <action> rows=N buttons=B done=D done_box=[x,y,WxH] rows_y=[..] close=[..] add=[..] traces=[..]`
|
||||
(stderr, `UDPSCOPE_PROBE_LAYOUT_MS`) — **`buttons` must be `2N+1`** while the editor is
|
||||
up (one `×` per row, one `+` per boundary) and **0 on the `editor off` line**: the
|
||||
buttons exist only while the editor does. **`traces=` is the real assertion**: an insert
|
||||
buttons exist only while the editor does. `done` follows the same rule for the editor's
|
||||
own exit button (1 up, 0 off), and its `done_box` on the `placed` line must sit in the
|
||||
area's top-right corner: `x + W` within a few pixels of the area width and `y` within a
|
||||
few of 0 (the on-switch line reports a box from before the first frame, so read the
|
||||
`placed` one). The last probe step leaves the editor by **clicking that button** — the
|
||||
toolbar toggle is still exercised by the first step — so `buttons=0 done=0` with the
|
||||
rows and traces untouched is the assertion that the way out works. **`traces=` is the real assertion**: an insert
|
||||
or a remove moves the rows' *contents* with them, so `add above 0` must read
|
||||
`[0:-|1:sine,…]` — row 0's signals now on row 1, not renumbered onto the new empty row —
|
||||
and `remove 0` must pull them back to `0:`. A `traces=` where the signals stayed on row 0
|
||||
@@ -229,6 +313,14 @@ Two lines of the self-check output are assertions in disguise:
|
||||
first run's values, and the signals it names must come back *acquired* (a `row 0 slot 0
|
||||
-> channel ... samples=N` line). Delete the file before a run whose layout you did not
|
||||
mean to inherit (`127.0.0.1_9540.toml` and friends accumulate otherwise).
|
||||
- `[udpscope] starved: connected (config=D), reconnects=N, no new frames in the last
|
||||
second ...` (stderr, once per second while the state holds, plus a status-line notice) —
|
||||
"connected but nothing is arriving after a reconnect", which over unicast is the second
|
||||
instance taking the source's single session. Two ways to see it without a second client:
|
||||
point the app at a UDP port where nothing answers (the client CONNECTs, times out,
|
||||
reconnects; `config=0`), and check it does **not** fire against a live source. The status
|
||||
line carries ` | N reconnects` only when N > 0, which is the same counter without the
|
||||
prose.
|
||||
- **The export's `time`/`stream_time` columns must be strictly increasing and evenly
|
||||
spaced.** Check the written file, not just the probe: `np.diff(t)` with no `<= 0` and
|
||||
`rows / (t[-1] - t[0])` equal to the stored rate (the `ch` line's figure). Duplicate or
|
||||
@@ -363,6 +455,13 @@ far the largest file.
|
||||
again until the next latch. `PlotArea::create()` is handed `App::placeholder_plot`,
|
||||
never a row: the area keeps a reference to the plot it was created with, and a rebuild
|
||||
destroys every row.
|
||||
- **The cursors' on/off is one flag on the `RowSet`, never a copy per row.** It is one
|
||||
piece of state (the toolbar toggle) and the mode is one value; a per-row copy is something
|
||||
`insert_row()`'s `clear_row()`, or any rebuild, can clear behind the user's back, which is
|
||||
how a row added while the cursors were already on came up with its instruments parked
|
||||
while every other row showed them. The same goes for the vertical pair's `x`: it is one
|
||||
stream time for the figure, so it lives on the `RowSet` too, and `Row` keeps only what is
|
||||
genuinely per row (the rulers' values, the traces, the colours).
|
||||
- **A row's contents are state of the `RowSet`, not of the figure.** `insert_row()` and
|
||||
`remove_row()` shift that state with `carry_row()` and let `rebuild_all()` re-deal the
|
||||
figure handles, so inserting above row 1 moves what row 1 plotted down to row 2 rather
|
||||
|
||||
@@ -41,6 +41,7 @@ add_executable(udpscope
|
||||
src/plot_host.cpp
|
||||
src/export.cpp
|
||||
src/layout_store.cpp
|
||||
src/measure.cpp
|
||||
src/ui.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -58,6 +58,68 @@ frame and the GUI thread takes once per frame around binding the traces.
|
||||
Samples cross the boundary through `plot::data::RingBuffer` (single producer,
|
||||
single consumer), never through the lock.
|
||||
|
||||
## Measurements
|
||||
|
||||
**Measurements** (overflow menu) reveals a pane with a `[+]` and one row per measurement:
|
||||
`[badge] <type> <value> [edit]`. `[+]` opens a dialog with the signal, the type, the options
|
||||
that type actually uses, and "show on plot"; `edit` opens the same dialog on that
|
||||
measurement, with a `Delete` beside `Save`.
|
||||
|
||||
The signal list is the signals the **plots are showing**, in signal-list order: a
|
||||
measurement reads the window a plot displays and its marks go on the row that plots the
|
||||
signal, so the candidates are the traces on screen. That is also what keeps everything else
|
||||
out of it — a signal nobody acquires, and a stream's own timestamp channel, which is acquired
|
||||
but is not something to measure. A measurement whose signal is no longer plotted keeps it in
|
||||
the list while it is being edited, so saving does not silently re-point it at whatever the
|
||||
dropdown happened to be showing. The badge is the measurement's own colour, and
|
||||
the marks it draws on the plot use it, so a readout and its ink are recognisably the same
|
||||
measurement.
|
||||
|
||||
| type | what it is | options | shown on the plot as |
|
||||
|---|---|---|---|
|
||||
| `average` | the mean of the visible samples | — | a ruler at the mean |
|
||||
| `top` / `bottom` | the high / low level: the mean of the samples above / below the split | split % | a ruler at the level |
|
||||
| `max` / `min` | the extremes | — | a ruler at the value **and** a marker where it happened |
|
||||
| `pulse duration` | the mean width of the pulse-side runs | split %, which side is the pulse | markers at the pulse's two edges |
|
||||
| `period` / `frequency` | the spacing of the threshold crossings / its reciprocal, in Hz | — | markers at two edges of a cycle |
|
||||
|
||||
`period` and `frequency` deliberately take **no options**: the engine counts *rising*
|
||||
crossings of the *mid* level, one edge per cycle, and a split or a side to choose from could
|
||||
only make that count wrong — a split near either extreme counts noise as crossings, and
|
||||
counting both edges halves the period.
|
||||
|
||||
The split is a share of the **window's own** min..max, not volts: a measurement is about this
|
||||
signal in this window, and a level in volts would have to be typed for every signal. A
|
||||
crossing has to be *strict* — a sample exactly at the level is on neither side — so a signal
|
||||
that merely touches the level does not count every sample as an event.
|
||||
|
||||
Each measurement's row carries **two badges**: its own colour (the same one its marks use on
|
||||
the plot) and the signal's chip, in the palette colour every other view names that signal
|
||||
with. The dialog's fields and their wording follow the type — the split row is absent for
|
||||
`average`, `min` and `max`, the side row only for `pulse duration`, and the "show on plot"
|
||||
line says what would be drawn.
|
||||
|
||||
The values are **window statistics over exactly the visible range, at the stored rate**: the
|
||||
same cut the export makes, so a measurement describes the data on screen rather than the ring
|
||||
behind it — and the marks it draws belong to that window, so they stay in the viewport.
|
||||
|
||||
They are recomputed when the **window moves**, not on a timer: a timer decouples the two, and
|
||||
with a scrolling window the stored marks then describe a range the plot has already left (the
|
||||
min/max marker ends up outside the viewport, and a dragged marker survives until the next
|
||||
tick). The trigger is 2 % of the span, so the numbers track the window within a few pixels
|
||||
while the cost stays bounded by the window's motion rather than by the frame rate; one pass
|
||||
copies the visible samples of each *channel* once, however many measurements read it.
|
||||
|
||||
**Show on plot** marks the result on the row that plots the signal: a ruler at the level
|
||||
(`top`/`bottom`), both rulers at the extremes (`min`/`max`), the pulse's or the cycle's two
|
||||
edge times as vertical markers. The marks are **readouts, not instruments**: a fixed pool of
|
||||
cursors per row, created **unselectable** (`Plot::set_cursor_selectable()`, added for this), so
|
||||
the controller's hit test never offers the pointer one — it can neither hover nor drag a
|
||||
measurement's ruler. They are also re-placed every frame, because a mark is only correct for
|
||||
the window its measurement was computed over. Re-placing alone was not enough: a drag is held
|
||||
for the gesture's life, so the marker followed the pointer *between* frames and the readout
|
||||
looked draggable.
|
||||
|
||||
## The session layout file
|
||||
|
||||
What you arranged is remembered per data source, in
|
||||
@@ -409,8 +471,12 @@ figure so the legend name is right (there is one widget, so the row a drop lands
|
||||
on is `PlotArea::row_at()`). The status line reports the outcome of a drop
|
||||
(`plotting 'sine' on plot 1`, `plot 1 already holds four signals`), so an action
|
||||
that changes nothing still says why. Each plot's y axis has a right-click menu
|
||||
with auto scale, a fixed range, and one **Remove '<signal>' from plot N** entry per
|
||||
occupied trace slot (a plot with nothing on it says so instead).
|
||||
with auto scale, a fixed range, **Fit X axis**, **Fit all**, and one
|
||||
**Remove '<signal>' from plot N** entry per occupied trace slot (a plot with nothing on
|
||||
it says so instead). The two fits are the same pair the overflow menu and the `f` key
|
||||
offer, split by what they touch: **Fit X axis** frames the window on the data the ring
|
||||
holds and leaves every y scale exactly as the user set it, **Fit all** does that and
|
||||
auto-scales the y axes too.
|
||||
|
||||
**The plot does not twitch.** Two independent causes were fixed, both of which
|
||||
showed up as the x axis moving a few pixels every frame — including while paused,
|
||||
@@ -518,6 +584,55 @@ switches them on as well, because a reset nobody can see reads as a dead button.
|
||||
The two panels collapse vertically (`SLIDE_DOWN`), so hiding one gives its space
|
||||
back instead of leaving an invisible gap in the column.
|
||||
|
||||
With the cursors on, a **readout panel** appears in the side column, and a dropdown
|
||||
chooses what the instruments are:
|
||||
|
||||
| mode | instruments | the pane reads |
|
||||
|---|---|---|
|
||||
| `vertical` | two x markers, one pair for the whole figure | `T0`, `T1`, `dT` |
|
||||
| `horizontal` | two y rulers on **each** row | `Y1`, `Y2`, `dY` per row |
|
||||
| `both` | both of the above | the times, then a line per row |
|
||||
| `mix` | the x pair, plus where each marker **crosses each trace** | the times, then `Y1`, `Y2`, `dY` per plotted signal |
|
||||
|
||||
The vertical pair is one stream time shared by every row, so the markers line up down
|
||||
the column and dragging one on any row moves them all — the app reads the position back
|
||||
out of the plot each frame, because a drag writes the *plot* and that is the only place
|
||||
it is visible. A ruler is per row: each row has its own y axis, so two rows' rulers are
|
||||
two different values, and the pane names the row.
|
||||
|
||||
`mix` needs no rulers: it reports the value the trace actually has at each marker, read
|
||||
through the library's `Plot::crosshair_value()` — the two bracketing samples,
|
||||
interpolated — so it is the answer to "what is this signal at these two times" rather
|
||||
than "drag a ruler until it crosses the trace". A readout is `--` when the trace has
|
||||
nothing at that time and `partial` when only one of the two markers is over samples.
|
||||
|
||||
The readouts are a **table**: a header naming the columns, then one row per instrument —
|
||||
`| who | A | B | delta |`. The first row is the two times and their difference, and the
|
||||
rows below it are the rulers (one per row, in `horizontal`/`both`) or the traces (in
|
||||
`mix`). The time row is there only where the time cursors are: in `horizontal` the pair is
|
||||
parked, and a row of times nothing on the plot can move would be a readout of nothing. Columns size to their widest cell, so a column of numbers is a column; the two
|
||||
cursors are `A` and `B` and their difference is `delta`, whichever of the three the mode
|
||||
puts in them.
|
||||
|
||||
Every readout is a **measurement**, not a raw number: at most two decimals, scaled by an
|
||||
SI prefix so the digits sit in `1..999`, and the unit the stream declared appended —
|
||||
`10.52 mV`, `-9.8 V`, `dT -398.67 ms`, `1.06 pV`. Two decimals is the resolution the
|
||||
instruments can claim (a marker lands on a sample, and the sample is an `f32`), and a
|
||||
trailing zero is dropped, so `1.50 V` reads `1.5 V`. A row's rulers use its signals' unit
|
||||
when they agree on one and print bare digits when they do not: a ruler is on the row's y
|
||||
axis, and two units on one axis make "the" unit a guess. A trace's line carries the
|
||||
signal's **colour chip** instead of its name — the chip identifies it exactly as the signal
|
||||
panel does — and the values are bare: `● 616.49 mV -24.57 mV -641.06 mV`, because the
|
||||
pane is a readout beside a plot and `Y1:` labels there are noise.
|
||||
|
||||
`mix` also *draws* the crossings: a mark on each trace where each marker meets it, in the
|
||||
marker's own colour, placed with the same call the pane reads (so the mark and the number
|
||||
cannot disagree), and shown only where the trace actually has samples at that time. The
|
||||
marks are annotations the plot holds from construction and that are moved, never added:
|
||||
`Plot` appends annotations only, so a host that re-added them would grow the plot every
|
||||
frame — which is why the library gained `Plot::set_annotation()` for this. Nothing marks
|
||||
the ruler modes: a ruler *is* the mark.
|
||||
|
||||
**The time window is typed, not picked off a ladder.** The header bar's `window`
|
||||
button reads the current span and opens onto `÷10` · a number · a unit (µs, ms, s,
|
||||
min) · `×10`. A ladder of round numbers could only ever offer the spans someone had
|
||||
@@ -573,9 +688,11 @@ logged with both rates and the new anti-alias corner. A window change that leave
|
||||
`time_stride` alone rebuilds nothing. Per-channel `stride` is still always 1 (see the
|
||||
limitations below); only the shared clock is decimated.
|
||||
|
||||
**Fit to data** (overflow menu) does what the plot's own `f` key does: auto-scales every row's y
|
||||
axis and re-frames the x window on the data the ring holds, propagated to every
|
||||
row. The status line acknowledges it (`fitted to the buffered data (1.25 s)`).
|
||||
**Fit to data** (overflow menu, the `f` key, or **Fit all** in a plot's right-click menu)
|
||||
auto-scales every row's y axis and re-frames the x window on the data the ring holds,
|
||||
propagated to every row. **Fit X axis** is the same re-framing without the y scaling, for
|
||||
the common case where the window should be the data's but the levels were placed by hand.
|
||||
The status line acknowledges either (`x fitted to the buffered data (1.25 s)`).
|
||||
|
||||
**Shared x axis.** The rows are the rows of **one `plot::Figure`**, drawn by one
|
||||
`plot::gtk4::PlotArea` in figure mode, and they all join a single x link group.
|
||||
@@ -607,6 +724,11 @@ removes it, a `+` on each boundary inserts one there — so the row an action na
|
||||
row you are pointing at. `--plots` is now only the count to start with, and `RowSet::kMaxRows`
|
||||
(8) is a ceiling rather than a menu: past about that a row is chrome and no plot.
|
||||
|
||||
The editor also carries its own way out: a **`Done`** button in the plot area's top-right
|
||||
corner while it is up, because the toggle that opened it lives in the overflow menu — a
|
||||
long way from the plot being edited. It turns that toggle off, so there is still exactly
|
||||
one piece of state, and the buttons (its own included) go with it.
|
||||
|
||||
An insert or a remove **moves the rows' contents with them**: what row 1 plotted, in the
|
||||
colours it plotted it in and with its cursors where they were, is on row 2 after inserting
|
||||
above row 1. Only that state moves; the figure's `Plot`s are rebuilt and re-dealt, which is
|
||||
@@ -711,6 +833,19 @@ now — they are listed so they are not mistaken for anything else:
|
||||
hundred samples every fifth of a second made every frame span microseconds while
|
||||
the frames were 100 ms apart).
|
||||
|
||||
**One source, one client, over unicast.** A UDPStreamer over unicast keeps a single
|
||||
session: a second `udpscope`'s CONNECT displaces the first, the displaced client sees its
|
||||
`silence_timeout_s` elapse, reconnects and displaces the other, and the two take turns
|
||||
being served — which reads as "the second UI gets no data" (and both windows look alike
|
||||
because they share the saved layout). Nothing on the client side can give both of them the
|
||||
stream: the wire has one destination. Watching one source from several UIs is what
|
||||
`--multicast <group> [--interface <local ip>]` is for — every instance joins the group and
|
||||
the kernel hands each of them a copy. When the app sees the fight (a reconnect with no
|
||||
frames delivered in the same second — the client's own retry counter is also shown in the
|
||||
status line as `N reconnects` once it is nonzero) it says so in the status line rather than
|
||||
leaving an empty plot to be puzzled over, and prints a `starved:` line to stderr while the
|
||||
state holds.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Multi-element (vector/matrix) signals are acquired with a zero-order hold.**
|
||||
@@ -727,11 +862,22 @@ now — they are listed so they are not mistaken for anything else:
|
||||
- **Calibration is applied at ingest**, so changing gain or offset rebuilds the
|
||||
buffers (and drops the history). Applying it at read time would need the LOD
|
||||
pyramid to be gain-aware.
|
||||
- **Cursors are per row** and not synchronised with each other; show/hide parks
|
||||
them outside the axis range rather than removing them, because `Plot` has no
|
||||
`remove_cursor`. The trigger marker is the one exception: it has its own cursor
|
||||
and is independent of the cursor toggle.
|
||||
- **The horizontal rulers are per row**, since a y value means nothing without the
|
||||
y axis it is on; the vertical pair is shared, so the same two times are on every
|
||||
row. Show/hide *parks* every instrument outside the axis range rather than removing
|
||||
it, because `Plot` has no `remove_cursor` — which is why switching mode re-places
|
||||
rather than rebuilds, and why the frame tick reads the markers back: a parked
|
||||
cursor's snapped position is a real number, so nothing may be inferred from it. The
|
||||
trigger marker is separate: its own cursor, independent of the cursor toggle.
|
||||
- **Verified headless only.** Acquisition, partitioning, the trigger state
|
||||
machine and its integration with the buffers, and rendering under Xvfb
|
||||
(llvmpipe) were exercised; the interactive paths (drag and drop, the dialogs,
|
||||
the right-click menu) were not driven by an automated test.
|
||||
|
||||
**A debug-build assertion in the library's suite.** `deps/plotter`'s debug build prints
|
||||
`TextBatch::add_cell`'s `glyph_is_cell` assertion once while running its tests — from a
|
||||
forked child of the text/atlas cases (`a_backend_without_an_atlas_uploads_nothing` is the
|
||||
one that precedes it), not from a failing test: the suite still reports `355 cases,
|
||||
0 failed`, and this is pre-existing rather than something the cursors work introduced. The
|
||||
new ruler test asserts the *strokes* (`figure_rows_draw_their_own_horizontal_rulers`) with
|
||||
the harness's no-atlas backend, so it neither needs nor trips the text path.
|
||||
Vendored
+1
-1
Submodule deps/plotter updated: e337c6a60c...211c5b97bc.
+227
-24
@@ -26,6 +26,10 @@ constexpr plot::u32 kUnplottedColor = 0x6E6E6Eu;
|
||||
constexpr plot::u32 kCursorAColor = 0xFFD24Fu;
|
||||
constexpr plot::u32 kCursorBColor = 0x7FE0FFu;
|
||||
constexpr plot::u32 kRulerColor = 0xB0B0B0u;
|
||||
constexpr plot::u32 kRulerBColor = 0x8FA8C8u;
|
||||
/// The measurements' own colour, distinct from both cursor rulers so a readout mark cannot be
|
||||
/// mistaken for something the user placed.
|
||||
constexpr plot::u32 kMeasureColor = 0x9BE8A0u;
|
||||
constexpr plot::u32 kTriggerColor = 0xFF5B5Bu;
|
||||
|
||||
constexpr double kParked = -1.0e12; ///< hides an instrument without removing it
|
||||
@@ -59,10 +63,9 @@ void RowSet::carry_row(Row& dst, const Row& src) {
|
||||
dst.name = src.name;
|
||||
dst.channel = src.channel;
|
||||
dst.color_index = src.color_index;
|
||||
dst.cursors_visible = src.cursors_visible;
|
||||
dst.cursor_a_x = src.cursor_a_x;
|
||||
dst.cursor_b_x = src.cursor_b_x;
|
||||
|
||||
dst.ruler_y = src.ruler_y;
|
||||
dst.ruler_b_y = src.ruler_b_y;
|
||||
// The slot's series stays where it is, but what it is bound to belonged to the row
|
||||
// this state came *from*. `bind()` re-resolves every slot next frame; clearing it here
|
||||
// is what stops the frame in between from drawing the moved row's data under the new
|
||||
@@ -75,9 +78,6 @@ void RowSet::clear_row(Row& r) {
|
||||
r.name = {};
|
||||
r.channel = {-1, -1, -1, -1};
|
||||
r.color_index = {0, 1, 2, 3};
|
||||
r.cursors_visible = false;
|
||||
r.cursor_a_x = 0.0;
|
||||
r.cursor_b_x = 0.0;
|
||||
r.ruler_y = 0.0;
|
||||
for (SlotSeries& s : r.slots) s.bind(nullptr);
|
||||
}
|
||||
@@ -115,6 +115,30 @@ void RowSet::install(Row& r, int /*row_index*/, plot::Plot& p) {
|
||||
r.cursor_a = r.plot->add_cursor(0.0, plot::Color::hex(kCursorAColor));
|
||||
r.cursor_b = r.plot->add_cursor(0.0, plot::Color::hex(kCursorBColor));
|
||||
r.ruler = r.plot->add_h_ruler(0.0, plot::Color::hex(kRulerColor));
|
||||
r.ruler_b = r.plot->add_h_ruler(0.0, plot::Color::hex(kRulerBColor));
|
||||
// The `mix` crossings, one pair per trace slot, hidden until that mode is on. Created
|
||||
// here because a `Plot` fixes its annotation set the same way it fixes its traces --
|
||||
// `add_annotation()` only appends -- so the markers are made up front and moved.
|
||||
for (int s = 0; s < kSlotsPerRow; ++s) {
|
||||
plot::Annotation a{0.0, 0.0, "", plot::Color::hex(kCursorAColor), r.axis_x, r.axis_y, false};
|
||||
plot::Annotation b{0.0, 0.0, "", plot::Color::hex(kCursorBColor), r.axis_x, r.axis_y, false};
|
||||
r.crossing_a[static_cast<std::size_t>(s)] = r.plot->add_annotation(a);
|
||||
r.crossing_b[static_cast<std::size_t>(s)] = r.plot->add_annotation(b);
|
||||
}
|
||||
// The measurements' readout marks: a fixed pool, parked. Levels are rulers (they read a y)
|
||||
// and times are markers (they read an x), so the pool has one of each per slot.
|
||||
for (int m = 0; m < kMeasureMarks; ++m) {
|
||||
const std::size_t mi = static_cast<std::size_t>(m);
|
||||
r.measure_h[mi] = r.plot->add_h_ruler(kParked, plot::Color::hex(kMeasureColor));
|
||||
r.measure_v[mi] = r.plot->add_cursor(kParked, plot::Color::hex(kMeasureColor));
|
||||
// Readouts, not instruments: the pointer must not be able to hover or grab one. Parking
|
||||
// them outside the axis and re-placing every frame does not stop a *drag* -- the
|
||||
// controller holds the marker for the gesture's life, so it follows the pointer between
|
||||
// frames and the readout looks draggable. Excluding it from the hit test is the only
|
||||
// place that can say no.
|
||||
r.plot->set_cursor_selectable(r.measure_h[mi], false);
|
||||
r.plot->set_cursor_selectable(r.measure_v[mi], false);
|
||||
}
|
||||
r.trigger_marker = r.plot->add_cursor(kParked, plot::Color::hex(kTriggerColor));
|
||||
// A marker, not an instrument: it must sit exactly on the edge's time, so its
|
||||
// snap is Free and the controller never rounds it onto a sample.
|
||||
@@ -303,13 +327,190 @@ void RowSet::set_y_range(int row_index, double lo, double hi) {
|
||||
|
||||
void RowSet::place_cursors(Row& r) {
|
||||
if (!r.plot) return;
|
||||
const bool show = r.cursors_visible;
|
||||
const double a = show ? r.cursor_a_x : kParked;
|
||||
const double b = show ? r.cursor_b_x : kParked;
|
||||
const double h = show ? r.ruler_y : kParked;
|
||||
r.plot->set_cursor_x(r.cursor_a, a);
|
||||
r.plot->set_cursor_x(r.cursor_b, b);
|
||||
r.plot->set_cursor_y(r.ruler, h);
|
||||
const bool on = cursors_on_;
|
||||
// Which instruments the mode uses; the rest are parked (a `Plot` cannot drop a
|
||||
// cursor, so hiding is the only way to take one off screen).
|
||||
const bool vertical = on && cursor_mode_ != CursorMode::Horizontal;
|
||||
const bool horizontal = on && (cursor_mode_ == CursorMode::Horizontal ||
|
||||
cursor_mode_ == CursorMode::Both);
|
||||
r.plot->set_cursor_x(r.cursor_a, vertical ? cursor_a_x_ : kParked);
|
||||
r.plot->set_cursor_x(r.cursor_b, vertical ? cursor_b_x_ : kParked);
|
||||
r.plot->set_cursor_y(r.ruler, horizontal ? r.ruler_y : kParked);
|
||||
r.plot->set_cursor_y(r.ruler_b, horizontal ? r.ruler_b_y : kParked);
|
||||
}
|
||||
|
||||
void RowSet::set_cursor_mode(CursorMode mode) {
|
||||
if (mode == cursor_mode_) return;
|
||||
cursor_mode_ = mode;
|
||||
for (int i = 0; i < kMaxRows; ++i) place_cursors(row(i));
|
||||
place_crossings();
|
||||
}
|
||||
|
||||
void RowSet::place_crossings() {
|
||||
const bool mix = cursor_mode_ == CursorMode::Mix && cursors_visible();
|
||||
for (int i = 0; i < kMaxRows; ++i) {
|
||||
Row& r = row(i);
|
||||
if (!r.plot) continue;
|
||||
for (int s = 0; s < kSlotsPerRow; ++s) {
|
||||
const std::size_t si = static_cast<std::size_t>(s);
|
||||
const plot::u32 ida = r.crossing_a[si];
|
||||
const plot::u32 idb = r.crossing_b[si];
|
||||
if (ida >= r.plot->annotation_count() || idb >= r.plot->annotation_count()) continue;
|
||||
plot::Annotation a = r.plot->annotation(ida);
|
||||
plot::Annotation b = r.plot->annotation(idb);
|
||||
double ya = 0.0;
|
||||
double yb = 0.0;
|
||||
const bool has_a = mix && r.plot->crosshair_value(static_cast<plot::u32>(s), cursor_a_x_, ya);
|
||||
const bool has_b = mix && r.plot->crosshair_value(static_cast<plot::u32>(s), cursor_b_x_, yb);
|
||||
a.visible = has_a;
|
||||
b.visible = has_b;
|
||||
if (has_a) {
|
||||
a.x = cursor_a_x_;
|
||||
a.y = ya;
|
||||
}
|
||||
if (has_b) {
|
||||
b.x = cursor_b_x_;
|
||||
b.y = yb;
|
||||
}
|
||||
r.plot->set_annotation(ida, a);
|
||||
r.plot->set_annotation(idb, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double RowSet::ruler_y(int r) const {
|
||||
return r >= 0 && r < rows_ ? row(r).ruler_y : 0.0;
|
||||
}
|
||||
|
||||
double RowSet::ruler_b_y(int r) const {
|
||||
return r >= 0 && r < rows_ ? row(r).ruler_b_y : 0.0;
|
||||
}
|
||||
|
||||
int RowSet::crossings_visible() const {
|
||||
int n = 0;
|
||||
for (int i = 0; i < rows_; ++i) {
|
||||
const Row& r = row(i);
|
||||
if (!r.plot) continue;
|
||||
for (int s = 0; s < kSlotsPerRow; ++s) {
|
||||
const std::size_t si = static_cast<std::size_t>(s);
|
||||
if (r.crossing_a[si] < r.plot->annotation_count() &&
|
||||
r.plot->annotation(r.crossing_a[si]).visible) {
|
||||
++n;
|
||||
}
|
||||
if (r.crossing_b[si] < r.plot->annotation_count() &&
|
||||
r.plot->annotation(r.crossing_b[si]).visible) {
|
||||
++n;
|
||||
}
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
bool RowSet::crossing_at(int i, int slot, bool second, double& x, double& y) const {
|
||||
if (i < 0 || i >= rows_ || slot < 0 || slot >= kSlotsPerRow) return false;
|
||||
const Row& r = row(i);
|
||||
if (!r.plot) return false;
|
||||
const std::size_t si = static_cast<std::size_t>(slot);
|
||||
const plot::u32 id = second ? r.crossing_b[si] : r.crossing_a[si];
|
||||
if (id >= r.plot->annotation_count()) return false;
|
||||
const plot::Annotation& a = r.plot->annotation(id);
|
||||
if (!a.visible) return false;
|
||||
x = a.x;
|
||||
y = a.y;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RowSet::set_measure_marks(const std::vector<MeasureMark>& marks) {
|
||||
measure_marks_placed_ = static_cast<int>(marks.size());
|
||||
for (int i = 0; i < kMaxRows; ++i) {
|
||||
Row& r = row(i);
|
||||
if (!r.plot) continue;
|
||||
int h = 0;
|
||||
int v = 0;
|
||||
for (const MeasureMark& m : marks) {
|
||||
if (m.row != i) continue;
|
||||
if (m.level) {
|
||||
if (h >= kMeasureMarks) continue;
|
||||
const std::size_t mi = static_cast<std::size_t>(h++);
|
||||
r.plot->set_cursor_y(r.measure_h[mi], m.value, plot::CursorPlacement::Free);
|
||||
r.plot->set_cursor_color(r.measure_h[mi], plot::Color::hex(m.color));
|
||||
} else {
|
||||
if (v >= kMeasureMarks) continue;
|
||||
const std::size_t mi = static_cast<std::size_t>(v++);
|
||||
r.plot->set_cursor_x(r.measure_v[mi], m.value, plot::CursorPlacement::Free);
|
||||
r.plot->set_cursor_color(r.measure_v[mi], plot::Color::hex(m.color));
|
||||
}
|
||||
}
|
||||
// What the measurements left over is parked rather than kept: a stale readout at the old
|
||||
// window's position is worse than none.
|
||||
for (; h < kMeasureMarks; ++h) {
|
||||
r.plot->set_cursor_y(r.measure_h[static_cast<std::size_t>(h)], kParked);
|
||||
}
|
||||
for (; v < kMeasureMarks; ++v) {
|
||||
r.plot->set_cursor_x(r.measure_v[static_cast<std::size_t>(v)], kParked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RowSet::fit_x() {
|
||||
Row& first = row(0);
|
||||
if (!first.plot || !figure_) return;
|
||||
first.plot->auto_fit_x();
|
||||
const plot::Range fitted = first.plot->x_range(first.axis_x);
|
||||
const double span = fitted.span();
|
||||
if (!std::isfinite(span) || !(span > 0.0)) return;
|
||||
// The same two paths `fit_all()` takes: a held event owns its window and states it
|
||||
// through the link group, a live view re-arms the follow and frames the newest span.
|
||||
if (pinned_) {
|
||||
window_s_ = span;
|
||||
figure_->set_x_link_range(x_group_, fitted);
|
||||
} else {
|
||||
set_time_window(span);
|
||||
}
|
||||
}
|
||||
|
||||
void RowSet::sync_cursors() {
|
||||
if (!cursors_visible()) return;
|
||||
// Only a mode that *shows* the vertical pair reads them back. With them parked (the
|
||||
// `horizontal` mode) whatever the plot reports is our own park coming back -- and a
|
||||
// snapped cursor parked far outside the axis reads back as something finite, which
|
||||
// would be adopted as a gesture and destroy the shared times.
|
||||
// Anything within this much of what we last pushed is our own placement coming back
|
||||
// (the plot stores f64, so an exact compare would be brittle across a rebuild).
|
||||
const auto moved = [](double a, double b) {
|
||||
return std::fabs(a - b) > 1.0e-9 * std::max(1.0, std::fabs(b));
|
||||
};
|
||||
bool push = false;
|
||||
for (int i = 0; i < rows_; ++i) {
|
||||
Row& r = row(i);
|
||||
if (!r.plot) continue;
|
||||
// A gesture writes the *plot*; this is the only place its result is visible. A
|
||||
// ruler is its own row's value -- nothing to share, but it still has to be read
|
||||
// back or the pane would keep showing wherever the app last put it.
|
||||
const bool rulers = cursor_mode_ == CursorMode::Horizontal || cursor_mode_ == CursorMode::Both;
|
||||
if (rulers) {
|
||||
const double y = r.plot->cursor(r.ruler).y;
|
||||
const double yb = r.plot->cursor(r.ruler_b).y;
|
||||
if (y > kParked * 0.5) r.ruler_y = y;
|
||||
if (yb > kParked * 0.5) r.ruler_b_y = yb;
|
||||
}
|
||||
if (cursor_mode_ == CursorMode::Horizontal) continue; // the pair is parked: not a value
|
||||
const double a = r.plot->cursor(r.cursor_a).x;
|
||||
const double b = r.plot->cursor(r.cursor_b).x;
|
||||
if (a < kParked * 0.5 || b < kParked * 0.5) continue; // parked: not a value
|
||||
if (moved(a, cursor_a_x_)) {
|
||||
cursor_a_x_ = a;
|
||||
push = true;
|
||||
}
|
||||
if (moved(b, cursor_b_x_)) {
|
||||
cursor_b_x_ = b;
|
||||
push = true;
|
||||
}
|
||||
}
|
||||
if (push) {
|
||||
for (int i = 0; i < rows_; ++i) place_cursors(row(i));
|
||||
}
|
||||
place_crossings();
|
||||
}
|
||||
|
||||
void RowSet::clamp_to_data() {
|
||||
@@ -442,11 +643,8 @@ void RowSet::set_trigger_marker(double x, bool visible) {
|
||||
}
|
||||
|
||||
void RowSet::set_cursors_visible(bool on) {
|
||||
for (int i = 0; i < kMaxRows; ++i) {
|
||||
Row& r = row(i);
|
||||
r.cursors_visible = on;
|
||||
place_cursors(r);
|
||||
}
|
||||
cursors_on_ = on;
|
||||
for (int i = 0; i < kMaxRows; ++i) place_cursors(row(i));
|
||||
}
|
||||
|
||||
void RowSet::reset_cursors() {
|
||||
@@ -455,13 +653,18 @@ void RowSet::reset_cursors() {
|
||||
if (!r.plot) continue;
|
||||
const plot::Range xr = r.plot->x_range(r.axis_x);
|
||||
const double span = xr.span() > 0.0 ? xr.span() : window_s_;
|
||||
r.cursor_a_x = xr.min + 0.25 * span;
|
||||
r.cursor_b_x = xr.min + 0.75 * span;
|
||||
r.ruler_y = r.plot->y_range(r.axis_y).min +
|
||||
0.5 * (r.plot->y_range(r.axis_y).span() > 0.0 ? r.plot->y_range(r.axis_y).span()
|
||||
: 1.0);
|
||||
place_cursors(r);
|
||||
cursor_a_x_ = xr.min + 0.25 * span;
|
||||
cursor_b_x_ = xr.min + 0.75 * span;
|
||||
const double y_lo = r.plot->y_range(r.axis_y).min;
|
||||
const double y_span = r.plot->y_range(r.axis_y).span();
|
||||
r.ruler_y = y_lo + 0.35 * (y_span > 0.0 ? y_span : 1.0);
|
||||
r.ruler_b_y = y_lo + 0.65 * (y_span > 0.0 ? y_span : 1.0);
|
||||
}
|
||||
// Every row's instruments are written from what was just set. Without this the plots
|
||||
// keep their build-time cursors (x = 0) and `sync_cursors()` -- which reads the plots
|
||||
// because a *gesture* writes them -- adopts that zero as a moved cursor.
|
||||
for (int i = 0; i < kMaxRows; ++i) place_cursors(row(i));
|
||||
place_crossings();
|
||||
}
|
||||
|
||||
} // namespace udpscope
|
||||
+102
-5
@@ -48,9 +48,27 @@ class RowSet {
|
||||
/// about this many a row is chrome and no plot.
|
||||
static constexpr int kMaxRows = 8;
|
||||
static constexpr int kSlotsPerRow = 4;
|
||||
/// Readout marks per row per orientation: enough for a handful of level and time
|
||||
/// measurements to be shown at once, and a fixed number because a `Plot` fixes its cursor
|
||||
/// set at construction.
|
||||
static constexpr int kMeasureMarks = 6;
|
||||
|
||||
RowSet();
|
||||
|
||||
/// Which cursor instruments are up, from the toolbar's toggle. The two vertical
|
||||
/// markers are shared by every row (one stream time each, so they line up down the
|
||||
/// column); a horizontal ruler is per row, because each row has its own y axis.
|
||||
///
|
||||
/// `Mix` is the vertical pair with the *per-trace* readout instead of rulers: the
|
||||
/// pane then reports where each cursor crosses each trace, which is the value a
|
||||
/// ruler would have to be dragged to express.
|
||||
enum class CursorMode {
|
||||
Vertical, ///< two x markers, shared by all rows
|
||||
Horizontal, ///< two y rulers per row
|
||||
Both, ///< both instruments
|
||||
Mix, ///< the x pair, read off each trace
|
||||
};
|
||||
|
||||
struct Row {
|
||||
/// The figure's row, borrowed. Null for a row beyond `row_count()`, which is how
|
||||
/// every loop here and in the UI stays inside the rows that exist. Invalidated by
|
||||
@@ -82,16 +100,28 @@ class RowSet {
|
||||
plot::u32 cursor_a = 0;
|
||||
plot::u32 cursor_b = 0;
|
||||
plot::u32 ruler = 0;
|
||||
plot::u32 ruler_b = 0;
|
||||
/// The vertical line marking the trigger instant, drawn whenever an event is
|
||||
/// on screen (and parked outside the axis otherwise). Not the user's cursor:
|
||||
/// it keeps its position whatever the cursor toggle says.
|
||||
plot::u32 trigger_marker = 0;
|
||||
/// Off by default: the cursors are switched on from the toolbar, and a rebuild
|
||||
/// (any drop, any row-count change) must not switch them back off.
|
||||
bool cursors_visible = false;
|
||||
double cursor_a_x = 0.0;
|
||||
double cursor_b_x = 0.0;
|
||||
|
||||
/// The measurements' readout marks: a pool created with the plot (a `Plot` cannot add or
|
||||
/// drop a cursor after the fact) and parked unless a measurement is using the slot.
|
||||
std::array<plot::u32, kMeasureMarks> measure_h{};
|
||||
std::array<plot::u32, kMeasureMarks> measure_v{};
|
||||
|
||||
/// One marker per trace per cursor: where each vertical cursor crosses that trace's
|
||||
/// series, drawn only in `mix` mode. Two per slot because the pair crosses a trace
|
||||
/// at two places, and an annotation cannot be shared between them.
|
||||
std::array<plot::u32, kSlotsPerRow> crossing_a{};
|
||||
std::array<plot::u32, kSlotsPerRow> crossing_b{};
|
||||
|
||||
/// The two rulers' values. The vertical pair is *not* here: one stream time is
|
||||
/// shared by every row, so it lives on the `RowSet` where it cannot drift between
|
||||
/// rows.
|
||||
double ruler_y = 0.0;
|
||||
double ruler_b_y = 0.0;
|
||||
};
|
||||
|
||||
int row_count() const noexcept { return rows_; }
|
||||
@@ -152,6 +182,67 @@ class RowSet {
|
||||
void set_y_range(int row, double lo, double hi);
|
||||
|
||||
void set_cursors_visible(bool on);
|
||||
/// What the cursors are (see `CursorMode`). Switching parks the instruments the mode
|
||||
/// does not use rather than removing them: a `Plot` cannot drop a cursor.
|
||||
void set_cursor_mode(CursorMode mode);
|
||||
/// Places (or hides) the `mix` mode's crossing markers: for every plotted trace, the
|
||||
/// point where each cursor meets it. Called from `sync_cursors()` and when the mode
|
||||
/// changes, so the markers are never a frame behind the markers that cast them.
|
||||
void place_crossings();
|
||||
CursorMode cursor_mode() const noexcept { return cursor_mode_; }
|
||||
|
||||
/// The two vertical cursors' stream times; shared by every row.
|
||||
double cursor_a_x() const noexcept { return cursor_a_x_; }
|
||||
double cursor_b_x() const noexcept { return cursor_b_x_; }
|
||||
/// One row's ruler values, or 0 for a row that does not exist.
|
||||
double ruler_y(int row) const;
|
||||
double ruler_b_y(int row) const;
|
||||
/// How many `mix` crossing markers are on screen, and the first one's position: the
|
||||
/// probe's window onto "the marks follow the cursors".
|
||||
int crossings_visible() const;
|
||||
bool crossing_at(int row, int slot, bool second, double& x, double& y) const;
|
||||
|
||||
/// Whether the cursors are on. One flag for the whole set, not a copy per row: it is one
|
||||
/// piece of state (the toolbar's toggle), and a per-row copy is something a rebuild, an
|
||||
/// insert or a carried row can clear behind the user's back -- which is how a row could
|
||||
/// end up with its instruments parked while every other row showed them.
|
||||
bool cursors_visible() const noexcept { return cursors_on_; }
|
||||
|
||||
/// One measurement's readout mark on a row: a *level* (drawn as a horizontal ruler) or a
|
||||
/// *time* (a vertical marker). Which one a measurement needs is the measurement's own
|
||||
/// business; the row is the one that plots its signal.
|
||||
struct MeasureMark {
|
||||
int row = 0;
|
||||
bool level = false;
|
||||
double value = 0.0;
|
||||
plot::u32 color = 0;
|
||||
};
|
||||
|
||||
/// Places the measurements' marks and parks every pool slot they do not use. The marks are
|
||||
/// created unselectable (`Plot::set_cursor_selectable()`), so the pointer can neither hover
|
||||
/// nor drag them; this runs every frame because a mark is only correct for the window its
|
||||
/// measurement was computed over.
|
||||
void set_measure_marks(const std::vector<MeasureMark>& marks);
|
||||
/// How many marks the last `set_measure_marks()` placed: the probe's window onto "the
|
||||
/// measurement is drawn", as opposed to merely computed.
|
||||
int measure_marks_placed() const noexcept { return measure_marks_placed_; }
|
||||
|
||||
/// Frames the x window on the data row 0 holds, leaving every y axis alone. The x
|
||||
/// counterpart of `fit_all()`, for the one thing the y axes cannot say: how wide the
|
||||
/// window should be.
|
||||
void fit_x();
|
||||
|
||||
/// Adopts the instruments a gesture moved. The vertical pair is one time for the whole
|
||||
/// figure, so a drag on any row's marker moves every row's; a ruler belongs to its own
|
||||
/// row and is only read back. Either way the plot is where a drag writes, so this is the
|
||||
/// one place the app can see it -- and the pane reads what is stored here, which is why a
|
||||
/// ruler that was not read back showed a stale value while its marker on the plot moved.
|
||||
/// Called once a frame from the tick.
|
||||
void sync_cursors();
|
||||
|
||||
/// Puts both vertical cursors at a quarter and three quarters of the x window, and
|
||||
/// the two rulers at a third and two thirds of each row's y range. Also switches the
|
||||
/// cursors on, so the button cannot look dead.
|
||||
void reset_cursors();
|
||||
|
||||
/// Places the trigger marker at stream time `x` (seconds since the epoch), or
|
||||
@@ -237,6 +328,12 @@ class RowSet {
|
||||
double y_gutter_px_ = 0.0;
|
||||
bool y_gutter_reserved_ = false;
|
||||
std::array<Row, static_cast<std::size_t>(kMaxRows)> storage_{};
|
||||
CursorMode cursor_mode_ = CursorMode::Vertical;
|
||||
bool cursors_on_ = false;
|
||||
int measure_marks_placed_ = 0;
|
||||
/// The one stream time each vertical cursor is at, whatever row's copy was last moved.
|
||||
double cursor_a_x_ = 0.0;
|
||||
double cursor_b_x_ = 0.0;
|
||||
std::array<bool, static_cast<std::size_t>(kMaxRows)> row_dirty_{};
|
||||
};
|
||||
|
||||
|
||||
+1595
-14
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user