neu_draw.backends.legend

The legend: a clickable strip drawn in the canvas beside the scene, so it is part of every snapshot and saved figure.

One row per label (a plate, a glyph for the kind of drawable, and the text); a row may stand for many drawables. Left-click a row to hide or show it, right-click to highlight it. Hidden rows are dimmed, not removed, and a partly hidden group has a third look. The highlight is a display override: drawable.color is never changed.

How it works:

  • A second render pass into the strip’s rect (render(..., rect=, flush=False)) after the main scene. Legend materials use depth_test=False.

  • Picking: the second pass writes pick ids inside the strip, so a click there hits the legend. Only the plate writes them, so the whole row is the click target.

  • The camera controller is bound to a viewport covering the main rect, so a click on the legend does not also rotate the camera.

  • Both rects are computed from the renderer’s current size (_Part), so resizing needs no handler.

Sizes are logical pixels: one world unit is one pixel. Label widths are measured from pygfx.Text’s laid-out bounding box, so the strip fits the longest label in one pass. When entries do not fit, the legend adds columns up to a width budget, then shrinks rather than clipping.

Attributes

Classes

LegendEntry

One legend row and every drawable it stands for: those sharing a

LegendOverlay

The legend for one Scene, bound to one renderer.

Functions

split_rects(size, location, width) → tuple[tuple, tuple]

(main_rect, strip_rect) for a canvas of size, both (x, y, w, h).

Module Contents

class neu_draw.backends.legend.LegendEntry(label: str, indices: Sequence[int], drawables: Sequence[Any], world_objects: Sequence[pygfx.WorldObject | None], spec: neu_draw.scene.Legend, is_group: bool = False)

One legend row and every drawable it stands for: those sharing a label_of(), or a declared group.

Holds the drawables (whose visible and color are authoritative) and the WorldObject built for each, paired by position, so a toggle updates both.

layout(column: int, row: int, column_width: float) → None

Place this entry at (column, row) of a grid whose columns are that wide.

Rows run downward from y = 0 (y is minus the pixels from the top); columns run rightward from x = PAD.

refresh(lit: set | None = None) → None

Push this entry’s state onto the objects: colour, highlight, visibility.

lit is the set of scene indices to highlight, computed by the overlay over every row, since a drawable can be on its own row and on group rows; None uses this row’s own flag. The highlight only changes the drawn material, never drawable.color, so recolouring a highlighted row takes effect when the highlight comes off. Everything is recomputed on each call.

toggle() → bool

Hide the whole group, or show it. Returns whether it is now visible.

If anything is showing, hide all; if nothing is, show all (rather than inverting each member).

toggle_highlight() → bool
drawables
glyph
group
highlighted = False
indices
is_group = False
property kinds: set[str]
label
property label_width: float

The label’s laid-out width in px, available before any draw.

property names: list[str]

The member drawables’ names.

plate
property row_text: str

The label, with a member count when the row stands for more than one drawable.

spec
text = ''
property visibility: str

"all", "some" or "none" of the members are showing.

A group can be partly shown, for instance after hiding one member by name.

world_objects
class neu_draw.backends.legend.LegendOverlay(scene: neu_draw.scene.Scene, group: pygfx.Group, renderer: Any)

The legend for one Scene, bound to one renderer.

One row per distinct label_of(), in first-appearance order; a drawable with neither label nor name gets no row. Drawables are paired with their world objects by position (build() makes one per drawable, in order), not by name, since a drawable may have a label and no name.

build_entries(strict: bool = True) → None

(Re)group the scene’s drawables into rows, discarding any rows there were.

Relabelling can merge and split rows, so every label change rebuilds them here. Highlights carry over by member: a rebuilt row is lit if every member was, so a split keeps both halves lit and a lit row merged into an unlit one goes dark.

Group rows (Legend.groups) come first, carry their highlight over by text, and resolve members by name. With strict, an unknown member or a group text that collides with a label row raises; sync() passes False, since it runs inside a draw, and warns and skips instead.

clear_highlights() → LegendOverlay
draw(renderer: Any = None, rect: Sequence[float] | None = None, flush: bool = True) → None

Render the strip. renderer defaults to the one this was built against.

Pass a renderer for an offscreen snapshot. Re-reads the scene first (sync()) and re-lays out only when the column count changes.

entry(label: str) → LegendEntry
highlight(*labels: str, exclusive: bool = False) → LegendOverlay

Draw these rows in the highlight colour, leaving their real colours alone.

exclusive=True clears other highlights first; by default highlights accumulate, as right-clicking several rows does.

layout() → None

Re-measure and re-place at the current column count. Idempotent.

measure() → None

Re-read the label widths, which the grid is derived from. Independent of the canvas size, unlike relayout().

plan(size: Sequence[float]) → tuple[int, float]

(columns, strip_width) for a canvas of size.

As many columns as the entries need to fit the height, up to what the width budget allows; any remainder is shrunk to fit. (A single tall column shrunk to fit would leave tiny text beside empty margin.) An explicit Legend.width fixes the strip, and the column count follows.

recolor(label: str, color: Any) → LegendOverlay

Recolour every drawable on a row. On a highlighted row the new colour appears when the highlight comes off.

rects_for(size: Sequence[float]) → tuple[tuple, tuple]
refresh() → LegendOverlay

Rebuild the rows from the scene, including their text, for changes made to the scene directly (such as scene.rename).

relabel(labels: str | Mapping[str, str], new: str | None = None) → LegendOverlay

Change rows’ text, keyed on their current label. Mapping or (old, new).

Giving two rows the same new label merges them:

view.legend.relabel({"1401 mesh": "Tm2", "1402 mesh": "Tm2"})

Sets the members’ label and leaves their names alone, so scene.get("1401 mesh") still works.

relayout(columns: int) → None

Place every entry into a grid columns wide, filling each column top-down.

set_visible(label: str, visible: bool) → LegendOverlay

Show or hide a whole row, by label.

sync() → bool

Catch up with the scene if it changed since the last draw; cheap when it did not.

Called from draw(), so direct field edits such as scene.get("a").label = "Tm2" show up, in snapshots too. A changed structure rebuilds the rows; a changed appearance only reassigns materials. Returns whether anything was done.

toggle(label: str) → bool
toggle_highlight(label: str) → bool

What right-clicking a row does.

unhighlight(*labels: str) → LegendOverlay

Put these rows back to their own colours. No labels means all of them.

backdrop
camera
columns = 1
entries: list[LegendEntry] = []
group
property highlighted: list[str]

The row labels currently drawn in the highlight colour.

property labels: list[str]

This legend’s rows, in order. The methods here take these labels, not drawable names.

main
renderer
scene
scene_data
spec
viewport
property width: float

The strip’s width for the renderer it is attached to, in px.

neu_draw.backends.legend.split_rects(size: Sequence[float], location: str, width: float) → tuple[tuple, tuple]

(main_rect, strip_rect) for a canvas of size, both (x, y, w, h).

Docked beside the scene rather than over it, so it never hides the data.

neu_draw.backends.legend.CAMERA_Z = 100.0
neu_draw.backends.legend.DEFAULT_PANEL: neu_draw.colors.RGBA = (0.07, 0.07, 0.09, 1.0)
neu_draw.backends.legend.DIM = 0.35
neu_draw.backends.legend.GAP = 7.0
neu_draw.backends.legend.GLYPH_WIDTH = 1.5
neu_draw.backends.legend.HIGHLIGHT_ROW_ALPHA = 0.38
neu_draw.backends.legend.MAX_WIDTH_FRACTION = 0.45
neu_draw.backends.legend.MIN_WIDTH = 80.0
neu_draw.backends.legend.PAD = 9.0
neu_draw.backends.legend.ROW_HEIGHT = 1.95