Diagrams

Interactive SysML v2 diagrams for Jupyter, rendered with ipyelk/ELK.

Requires the vendored ipyelk (pip install -e vendor/ipyelk; the pixi environments install it automatically). Three views, one dispatcher:

  • structure_diagram() – packages, definitions (with attribute compartments), nested usages; specialization / typing / redefinition / subsetting / membership / connection edges with spec-notation glyphs: the specialization family draws solid lines into a closed hollow triangle at the general end, adorned on the shaft per relationship (colon dots = typing, bar tick = redefinition, double colon dots = reference subsetting); composite/referential membership draws a filled/hollow diamond at the whole end with end multiplicities. Connector-family notation: port usages render as small squares ON the owning box’s border (direction arrows inside, ~T conjugation textual), interface / connection / binding / flow ends attach square-to-square, connector ends naming undrawn nested features draw the spec’s proxy dot on the shallowest drawn ancestor, connections typed by a definition with directed (source/target) ends grow an open-V head, 3+-end connects meet at a filled junction dot, flow connections run pin-to-pin (filled arrowhead at the target), binding connectors ride an = glyph, anonymous allocations draw the «allocate» keyword arrow (named ones the «allocation» box), dependencies draw dashed open-V client->supplier (n-ary via a filled junction dot), satisfies draw the «satisfy» keyword edge or – for named satisfy usages – the reference-subsetting head into the «requirement» box; aliases draw a hollow circle at the referencing end, portion usages (timeslice/snapshot) a filled notched ball at their individual, and actors render as the spec’s stick figure (name below; the actor_style="box" kwarg keeps the «actor» keyword-box alternative) while stakeholders render as «stakeholder» keyword boxes. View usages – saved diagram recipes (longeron.views) – draw as «view» keyword boxes. Packages carry the spec’s folder tab. membership="edges" swaps package nesting for the spec’s ALTERNATIVE owned-membership presentation: members as sibling nodes, solid edges with a circle-plus at the owning namespace end. annotations=True adds comment/doc notes with dashed anchor lines and «@Type» metadata adornments.

  • state_diagram() – hierarchical states, entry markers, transitions labeled trigger [guard] / effect; state usages typed by a state def expand into the definition’s submachine (submachine_depth bounds the expansion).

  • action_diagram() – the succession control-flow graph (the same one the interpreter executes), with the spec behavior glyphs: start dot, done bullseye, terminate circle-X, fork/join bars, decision/merge rhombi, accept/send badge boxes; successions render dashed. Control glyphs converge their edge fans on single anchor points; lanes= partitions the flow into dashed «performer» swim lanes.

  • diagram() – picks a view based on the element’s kind.

Every view ships a compact toolbar (longeron.toolbar): icon-only Fit / Center / Toggle-Collapse buttons, an edge-routing button that cycles orthogonal / polyline / splines re-layouts (also available as the routing= kwarg on every view constructor for headless renders), an orientation button that toggles the layout flow left-to-right / top-to-bottom (the direction= kwarg seeds it), plus a live search box that highlights matching elements without touching the selection; pass toolbar=False to keep ipyelk’s stock text buttons. On STRUCTURE views the collapse button CYCLES the selected node through the three legal renditions – nested child boxes, textual name : Type rows under the ‘parts’ compartment header, and the name compartment alone – while every compartment header carries its own fold twist (click the header row to fold that one compartment); the level() / fold() kernel API mirrors both (see CollapseTool; state/action views keep ipyelk’s stock hide-the-children collapse). Every widget (with or without the compact toolbar) fits-and-centers itself ONCE when its first layout arrives – a small margin, never zoomed past 1:1 – and later relayouts keep the user’s viewport (longeron.toolbar.AutoFitTool).

Compartment rows cap their display width at max_label_width px (default 480; a kwarg on every view constructor): overlong rows – calculation/expression attributes are the usual offenders – draw END-ellipsized with the full text on the row’s hover tooltip, so one absurd expression no longer makes its whole node absurd. Pass max_label_width=None to draw every row at full width.

Node ids are qualified names, so browser-side selections map back to model elements: use on_select() to react to clicks. Compartment ROWS are first-class selectable elements too: each row is the textual projection of a model element (an attribute usage, a part usage in the collapsed presentation, a constraint…), carries that element’s qualified name as its id, and clicks on it flow through the SAME selection seam as node and edge clicks. Structure boxes group their rows into the spec’s labeled compartments – separator rule + italic name (‘attributes’, ‘parts’, …) per 8.2.3.6 (printed p.199) – and structure_diagram(parts="rows") swaps nested part boxes for the spec’s collapsed textual presentation; the same swap is available PER NODE, interactively (the toolbar’s collapse button cycles a selected box through expanded / partial / name-only renditions; header twists fold single compartments) and from the kernel (level() / fold(), or structure_diagram( levels=..., folded=...)), with connector edges re-anchoring on the shrunken box as the spec’s proxy dots (printed p.67).

longeron.diagrams.ActorStyle

actor notation: the spec’s stick figure, or the «actor» keyword box

alias of Literal[‘figure’, ‘box’]

class longeron.diagrams.CollapseTool(diagram, element, options, builder, **kwargs)[source]

Bases: Tool

Per-node collapse through the THREE levels of a structure box, plus per-compartment folds.

The structure view replaces ipyelk’s stock ToggleCollapsedTool with this one – same toolbar slot, same select-then-click gesture (the affordance users already know). Each click on the button CYCLES the selected node one step DOWN in detail, then wraps back to full (documented cycle: each click shows less, the click after the smallest form restores everything):

  • expanded – nested child boxes (the full form, the default);

  • partial – the children leave the canvas and reappear as selectable name : Type rows under their spec compartment headers (‘parts’ printed p.60) – skipped when the node has no rowable members (packages, boxes of non-rowable children) and under the diagram-wide parts="rows" (everything is rows already);

  • collapsed – the smallest legal rendition: the name compartment alone (kind chip + name, no compartment stack, no drawn children; boundary port squares stay – the black-box view).

Connector edges that anchored on undrawn children re-anchor as proxy dots on the box itself (printed p.67) at both shrunken levels. Selection survives level changes because rows carry the SAME id (the qualified name) their boxes carried.

Independently of the level, every compartment header carries a FOLD affordance (the explorer tree’s twist, part of the header text): clicking the header row in the browser folds that ONE compartment to its header while the node stays at its level. Headers are presentation artifacts, not model elements – the click is consumed before sprotty sees it (the toolbar fit-sentinel reports it on a dedicated channel), so it can never enter the model-selection seam.

levels (qualified name -> level) and folded (qualified name -> folded compartment names) are the state seams: the toolbar button and header clicks toggle them, the level() / fold() kernel API edits them, view persistence captures them (longeron.views.capture_presentation()) and re-seeds them through structure_diagram(levels=..., folded=...). Every change REBUILDS the diagram’s source tree through the same builder the constructor used (_build_structure_root(), then the _prepare_root() + loader-defaults preparation of the birth tree) and re-runs the pipeline with the birth flow – so cycling a node back to expanded is payload-identical BY CONSTRUCTION.

levels

per-node collapse level (qualified name -> ‘partial’ | ‘collapsed’)

folded

per-node folded compartments (qualified name -> tuple of names)

apply()[source]

Rebuild the diagram’s source tree with the active collapse state and re-run the pipeline (the routing/direction tools’ refresh path, with the birth new flow).

Return type:

None

cycle(*qnames)[source]

Cycle each named node one level down (expanded -> partial -> collapsed -> expanded), skipping levels that change nothing. A compartment ROW of a shrunken node cycles its owner (the row IS that child’s collapsed presentation); nodes with nothing to collapse are no-ops.

Return type:

None

fold(qname, section, folded=True)[source]

Fold (or unfold) ONE compartment of one node: the rows leave, the header stays (closed twist). The node keeps its level.

Return type:

None

longeron.diagrams.CompartmentSection

the spec compartment names, in stacking order down the node (the keyword strings are the spec’s own compartment names; printed-page citations at _SECTION_ORDER, which derives from this alias)

alias of Literal[‘attributes’, ‘enums’, ‘occurrences’, ‘individuals’, ‘timeslices’, ‘snapshots’, ‘items’, ‘parts’, ‘directed features’, ‘allocations’, ‘actions’, ‘parameters’, ‘states’, ‘constraints’, ‘assert constraints’, ‘require constraints’, ‘assume constraints’, ‘requirements’, ‘satisfy requirements’, ‘subject’, ‘actors’, ‘stakeholders’, ‘views’]

longeron.diagrams.CompositionMode

definition-level composition edges: drawn ("defs") or suppressed

alias of Literal[‘defs’, ‘none’]

longeron.diagrams.MembershipMode

how package membership presents: members nested inside the package box, or as sibling nodes joined by circle-plus owned-membership edges

alias of Literal[‘nested’, ‘edges’]

longeron.diagrams.NodeLevel

one node’s collapse level, in cycling order (each toolbar click REDUCES detail one step, then wraps back to full): nested child boxes -> textual rows -> the name compartment alone

alias of Literal[‘expanded’, ‘partial’, ‘collapsed’]

longeron.diagrams.PartsMode

how nested part-family usages present: nested child boxes (the spec’s primary presentation) or textual name : Type rows in their compartments (the spec’s collapsed presentation)

alias of Literal[‘nested’, ‘rows’]

longeron.diagrams.action_diagram(action, *, lanes=None, toolbar=True, routing='orthogonal', direction='right', max_label_width=480.0, height=None)[source]

The succession control-flow graph the interpreter executes.

Successions render dashed with open-V arrows and the behavior nodes use the spec glyphs (spec 8.2.3 printed p.227-228; figures pp.90-92): start = filled dot, done = bullseye, terminate = circle-X, fork/join = thick filled bar, decision/merge = empty rhombus, accept/send = the standard rounded action box with a filled top-left badge. Control glyphs carry single convergence anchors: every incoming edge joins at one point and every outgoing edge leaves from one point (fork/join bars excepted – their edges distribute along the bar, which is the bar’s semantic).

lanes (default off) partitions the flow into dashed-boundary «performer» swim lanes (spec “Perform Actions Swimlanes”, printed p.90): pass a mapping of lane title -> step names, or True to derive lanes from perform targets (perform part1.action1 lands in lane part1). Lanes are content-sized dashed containers ordered left-to-right via ELK layer partitioning – an honest approximation of the spec’s full-height, shared-boundary lanes. Steps in no lane stay outside (like the spec’s start/done markers). toolbar=False keeps ipyelk’s stock toolbar; routing picks the edge routing style (orthogonal / polyline / splines); direction the layout flow ("right", the flow-reading default, or "down"). max_label_width caps compartment-row display width exactly like structure_diagram() (behavior boxes carry no rows today, so the cap is future-proofing); height pins the widget’s rendered height to a CSS length exactly like structure_diagram() (default: the 400px-floor bare-cell behavior).

Return type:

Diagram

longeron.diagrams.diagram(element, **kwargs)[source]

Pick a view by element kind: state machines, actions, else structure.

Return type:

Diagram

longeron.diagrams.fold(widget, element, section, folded=True)[source]

Fold (or unfold, with folded=False) ONE compartment of one node on a structure diagram: the compartment’s rows leave, its header stays with the closed twist, and the node keeps its collapse level. section is the spec compartment name exactly as the header writes it (‘attributes’, ‘parts’, ‘constraints’, …). The kernel mirror of clicking the header row in the browser.

Return type:

None

longeron.diagrams.level(widget, element, to=None)[source]

Get or set one node’s collapse level on a structure diagram.

to=None returns the current level ("expanded" when the node was never collapsed). Otherwise set it: "expanded" restores the nested child boxes, "partial" rows the node’s parts under their compartment headers, "collapsed" draws the smallest legal rendition – the name compartment alone (see CollapseTool). Accepts a model element or a qualified name; returns the resulting level. The kernel mirror of the toolbar’s collapse button.

Return type:

Literal['expanded', 'partial', 'collapsed']

longeron.diagrams.on_select(diagram_widget, model, callback)[source]

Invoke callback with the model elements selected in the browser.

Node ids are qualified names, so selections resolve directly. Compartment ROWS carry the same identity (the projected element’s qualified name – an attribute usage, a part usage, a constraint…), so a row click arrives here exactly like a node click; port squares likewise. Synthetic transport ids (edges, markers) skip resolution – relationship edges resolve through the widget’s _lgn_rel_edges seam instead.

Return type:

None

longeron.diagrams.state_diagram(machine, *, submachine_depth=None, toolbar=True, routing='orthogonal', direction='right', max_label_width=480.0, height=None)[source]

A hierarchical state machine: states, entry markers, transitions.

A state usage typed by a state def (state swap : ToteSwap;) is expanded into the definition’s full submachine – states, entry marker, transitions – the same member view the interpreter executes (StateMachine descends through members_of). Expansion is recursive and cycle-safe: a definition reached again through its own submachine draws as a collapsed leaf.

submachine_depth bounds how many typing hops to expand: None (the default) is unlimited, 0 draws typed states as plain leaves (the pre-0.8 behavior). Plain nested states are always shown. toolbar=False keeps ipyelk’s stock toolbar; routing picks the edge routing style (orthogonal / polyline / splines); direction the layout flow ("right" or "down"); max_label_width caps compartment-row display width exactly like structure_diagram() (state boxes carry no rows today, so the cap is future-proofing); height pins the widget’s rendered height to a CSS length exactly like structure_diagram() (default: the 400px-floor bare-cell behavior).

Expanded substate ids are instance-qualified (…::swapSource::swap::evaluating) so they stay unique per expansion site, selectable in the browser (the resolver walks typing hops), and exactly what longeron.replay records: two usages of one definition never share a replay key.

Return type:

Diagram

longeron.diagrams.structure_diagram(element, *, show_attributes=True, show_relationships=True, composition='defs', membership='nested', annotations=False, actor_style='figure', parts='nested', levels=None, folded=None, toolbar=True, routing='orthogonal', direction='right', max_label_width=480.0, height=None)[source]

Containment structure with specialization/typing/connection edges.

composition="defs" (the default) draws definition-level membership edges – a filled diamond at the whole end for composite part/item members, a hollow diamond for referential (ref) members, role name on the line, multiplicity at the part end – per the SysML v2 Parts notation; composition="none" suppresses them. Flow / binding / dependency / satisfy / alias / portion notation is always drawn when both ends resolve to drawn nodes (see the module docstring).

membership="nested" (the default) draws each package’s owned members NESTED inside its box – the spec’s primary presentation and exactly the pre-0.8 output. membership="edges" draws the spec’s ALTERNATIVE presentation instead (printed p.26, errata E18): packages do not swallow their drawn members – every member becomes a SIBLING node and a solid owned-membership edge runs from the owning package, carrying a true circle-plus at the owning end. (Siblings keep ELK’s layered layout stable: an edge between a package and a node nested inside it is the ancestor<->descendant case the layout mishandles.) Membership edges are containment presentation, not relationship edges, so show_relationships=False keeps them.

Port usages owned by a drawn definition/usage box render as the spec’s boundary squares (10x10, straddling the border, name : Type label INSIDE the box next to the square – where the spec’s part figures write it – direction arrow inside the square when the port definition’s directed features agree on one); interface / connection / binding / flow ends then attach square-to-square, and connector ends naming UNDRAWN nested features draw the spec’s proxy dot on the shallowest drawn ancestor (printed p.67). Only nodes that own drawn ports opt into ELK port handling – everything else keeps the exact pre-port layout path.

annotations=True (default off, to keep existing diagrams uncluttered) additionally draws comment/documentation notes – the folded-corner box with a dashed anchor line (no endpoint glyph) to each annotated element (spec printed pp.20-21) – and «@Type» / «#keyword» metadata adornments on annotated nodes.

actor_style="figure" (the default) draws actor usages as the spec’s stick figure (BNF printed p.244) – head, body, arms, legs in the usage palette, name below the figure, no «actor» stereotype (the figure IS the stereotype); actor_style="box" keeps the «actor» keyword-box alternative (errata N17), which also shows compartments. Stakeholders always draw the «stakeholder» box – the spec reserves the figure for actors.

Textual members group into the spec’s LABELED compartments (8.2.3.6 printed p.199): every compartment opens with a full-width separator rule and its italic name – ‘attributes’ (printed p.46), ‘enums’ (p.48), ‘directed features’ (p.62; ‘parameters’ on action/calc boxes, p.91), the constraint compartments (p.127), ‘subject’, and so on – replacing the earlier unlabeled row blob. Every row is a first-class SELECTABLE projection of its model element: it carries the element’s qualified name as its id, clicking it in the browser feeds on_select() exactly like a node click, and kernel-side selection writes light it up.

parts picks the presentation of nested usages (both are legal spec notation; the option only chooses): "nested" (the default) draws them as nested boxes – the graphical compartment, required where children anchor edges (connections, flows, proxies) – while "rows" is the COLLAPSED presentation: parts, items, the occurrence family, actions, states, requirements, named satisfies and allocations, actors, stakeholders and views render as textual name : Type rows in their spec compartments (‘parts’ printed p.60, ‘items’ p.57, ‘actions’ p.89, ‘states’ p.117, …). Edges that would anchor on the collapsed children are not drawn – the textual presentation trades them for compactness.

levels names individual nodes (qualified names, or elements -> "partial" / "collapsed") whose rendition starts below the expanded default – the state behind the toolbar’s collapse button (which CYCLES the selected node: expanded -> partial -> collapsed -> expanded, each click one step less detail) and the level() kernel API (see CollapseTool). "partial" is the per-node version of parts="rows": the node’s rowable members become textual rows. "collapsed" is the smallest legal rendition: the name compartment alone – kind chip + name, no compartment stack, no drawn children (boundary port squares stay: they are border interface points, the classic black-box view); a collapsed PACKAGE likewise draws its folder box alone, whatever the membership mode. folded names per-node FOLDED compartments (qualified name -> compartment names): a folded compartment keeps its header – with the closed twist – and drops its rows while the node stays at its level (the header row’s click affordance in the browser; the fold() kernel API).

How collapse composes, level x presentation:

  • edges – connector-family edges (connections, bindings, interfaces, flows, allocates) that anchored on a shrunken node’s children re-anchor as the spec’s proxy dots on the node itself (printed p.67) at BOTH shrunken levels; connectors living entirely inside one shrunken node are part of the collapsed content and are not drawn; the specialization/typing family from undrawn children is not drawn (at partial, the rows’ : Type text carries it) – all exactly as under the diagram-wide parts="rows";

  • parts="rows" – every node is already textual, so "partial" changes nothing there and the toolbar cycle skips it (expanded -> collapsed -> expanded); "collapsed" and folded work unchanged;

  • folds – independent of the level: they apply to whatever compartments the node currently shows (attributes at expanded, parts rows at partial, none at collapsed) and are remembered through level changes.

toolbar=False keeps ipyelk’s stock text-button toolbar instead of the compact icon+search one (longeron.toolbar).

routing picks the ELK edge routing style – "orthogonal" (the default), "polyline" or "splines" – for headless renders and the initial widget; the toolbar’s routing button cycles it live. direction picks the layout flow – "right" (left-to-right, the default) or "down" (top-to-bottom); the toolbar’s orientation button toggles it live.

max_label_width caps how wide a compartment row may draw, in px (default 480): longer rows – calculation/expression attributes are the usual offenders – are end-ellipsized with the FULL text on the row’s hover tooltip, so one absurd expression no longer makes the whole node absurd. None lifts the cap (every row at full width).

height pins the widget’s rendered height to a CSS length (e.g. "480px") so inline compositions can match a neighbor exactly – tutorial 7 sits a diagram beside a 650px 3D viewer in an HBox. The default None keeps the bare-cell behavior: content-driven height with a 400px minimum floor. An explicit height always wins, even below that floor.

Return type:

Diagram

Toolbar

A compact toolbar with live search for the interactive ipyelk diagrams.

Every widget built by longeron.diagrams gets this toolbar by default (pass toolbar=False there to keep ipyelk’s stock text buttons). It reworks ipyelk’s hover-revealed toolbar in place:

  • the stock Fit / Center / Toggle Collapsed text buttons become icon-only buttons with tooltips (the underlying Tool instances are reused, so behavior is exactly ipyelk’s; on STRUCTURE views longeron.diagrams then swaps the stock collapse tool for its three-level CollapseTool in the same slot);

  • an EdgeRoutingTool button CYCLES the diagram’s ELK edge routing style – ORTHOGONAL (the default) -> POLYLINE -> SPLINES – and re-lays the live diagram out through the pipeline; the active style persists per widget on the tool’s routing trait;

  • a DirectionTool button toggles the layout flow – RIGHT (left-to-right, the default) <-> DOWN (top-to-bottom) – through the same refresh path; the active direction persists per widget on the tool’s direction trait (seeded by the direction= constructor kwarg), and the flip queues a one-shot re-fit so the new aspect ratio lands centered instead of keeping a viewport framed for the old one;

  • an (invisible) AutoFitTool fits-and-centers the diagram when its FIRST layout arrives from the browser, with a small padding and never zooming past 1:1 – later relayouts (collapse, routing) keep the user’s viewport. Its hidden _FitSentinel companion rides INSIDE the widget’s own DOM and reports the browser-side moments a kernel-side re-fit must answer – a fresh sprotty view materializing (the first-layout fit can be dropped while the view is still constructing: the cropped-diagram bug), the widget’s first reveal (background tab, display:none lifted, lazy output rendering), and container resizes (an HBox squeeze, a dock drag) – always respecting the user’s pan/zoom latch, so a viewport the user has touched is never re-framed behind their back;

  • a DiagramSearch tool is registered: typing in its text box live-highlights every diagram node whose title or qualified name contains the query (case-insensitive), shows a matches/total count, and offers a clear button that restores the diagram.

Search highlighting is deliberately not selection. Matches are marked by pushing sysml-search-hit / sysml-search-dim fragments onto the elements’ properties.cssClasses and force-syncing the viewer’s source (send_state), which re-renders without re-running layout. view.selection.ids is never written, so callbacks attached via longeron.diagrams.on_select() can never fire from a search.

longeron.toolbar.DIRECTIONS = ('RIGHT', 'DOWN')

the layout flow directions the orientation button toggles between (elk.direction): RIGHT is elkjs layered’s default and longeron’s

longeron.toolbar.FIT_PADDING = 24.0

viewport padding (px) of the one-shot initial fit: the diagram never touches the viewport limits

longeron.toolbar.ROUTING_STYLES = ('ORTHOGONAL', 'POLYLINE', 'SPLINES')

the ELK layered edge routing styles the routing button cycles through (spec figures mix straight and orthogonal connectors; SPLINES rounds the corners): ORTHOGONAL is elkjs layered’s default and longeron’s

longeron.toolbar.SEARCH_ACTIVE_CSS = 'sysml-search-active'

DOM class pinning the (hover-revealed) toolbar while a search is active

longeron.toolbar.SEARCH_DIM_CSS = 'sysml-search-dim'

cssClasses fragment dimming non-matches while a search is active

longeron.toolbar.SEARCH_HIT_COLOR = '#d81b60'

saturated raspberry, deliberately outside the muted diagram palette (blues/greens/ambers/purples in longeron.render) so hits read as search state, never as another element kind

longeron.toolbar.SEARCH_HIT_CSS = 'sysml-search-hit'

cssClasses fragment marking a search match (node box + its labels)

longeron.toolbar.TOOLBAR_STYLE: dict[str, dict[str, str]] = {' .jp-ElkToolbar.sysml-search-active': {'opacity': '1 !important', 'transform': 'translateY(0)', 'visibility': 'visible'}, ' .jp-ElkToolbar:focus-within': {'opacity': '1 !important', 'transform': 'translateY(0)', 'visibility': 'visible'}, ' .sysml-edge.sysml-search-dim': {'opacity': '0.25'}, ' .sysml-search-dim > rect': {'opacity': '0.35'}, ' .sysml-search-dim > text': {'opacity': '0.35'}, ' .sysml-search-hit > rect': {'filter': 'drop-shadow(0 0 4px rgba(216, 27, 96, 0.55))', 'stroke': '#d81b60', 'stroke-width': '2'}}

style rules upgrade_toolbar() merges into the diagram’s scoped stylesheet (ipyelk StyledWidget: keys with a leading space are namespaced to this widget only)

class longeron.toolbar.AutoFitTool(diagram, **kwargs)[source]

Bases: Tool

Keep the diagram fitted-and-centered until the user takes over.

Diagrams used to first paint at 1:1 anchored top-left, so anything larger than the viewport started half off-screen until the user clicked Fit. This tool watches the viewer’s post-layout source tree (view.source.value – set by the browser-side elkjs pipe, i.e. exactly when the first layout settles) and answers the first arrival with one FitToScreenAction request: padding px of margin, zoom capped at max_zoom (small diagrams center at natural size instead of blowing up), no animation (a snap, not a glide).

The layout watcher fires ONCE: collapse/routing relayouts keep the user’s viewport. request_refit() queues exactly one more fit for the NEXT layout arrival – the direction toggle uses it, because a viewport framed for a left-to-right layout reads wrong on the top-to-bottom flip.

The first-layout fit request is a widget message: if a frontend view does not exist yet when the first layout lands (a slow display, a lazily rendered output), the message is dropped. That is what the tool’s sentinel exists for – a hidden anywidget the builder mounts INSIDE the diagram widget’s own DOM whose browser half reports fresh views, first reveals, and untouched-viewport resizes (see _sentinel_class()); each report is answered with refit_now(), which also bumps the sentinel’s fit_stamp to clear the browser-side user-interaction latch. Without anywidget the sentinel is None and only the first-layout fit remains.

Headless renders never construct tools, so they are unaffected.

padding

viewport margin (px) around the fitted diagram

max_zoom

never zoom in past this to fit (1.0 = natural size)

pending

whether the next layout arrival triggers a fit

fit_count

how many fit requests this tool has sent

request_refit()[source]

Queue exactly one more fit, for the next layout arrival.

Return type:

None

refit_now()[source]

Fit immediately, without waiting for a layout arrival.

For fits that happened into a USELESS viewport (a hidden widget has a zero-sized one: a docked background tab, a display:none’d cached diagram) or that were dropped outright (no frontend view yet). Bumping the sentinel’s fit_stamp afterwards clears the browser-side user-interaction latch: the viewport is the fitted one again, so a later resize may re-frame it without fighting anyone.

Return type:

None

class longeron.toolbar.DiagramSearch(diagram, **kwargs)[source]

Bases: Tool

Live search-and-highlight over a diagram’s model-backed nodes.

Typing (or setting query) matches a case-insensitive substring against every node’s title and qualified name (node ids, so instance-qualified expanded-submachine states match too). All matches light up at once; everything else dims; a 3/41 counter reports matches over searchable nodes. Clearing the query restores the diagram exactly.

The highlight mechanism is css-only: sysml-search-hit / sysml-search-dim fragments on properties.cssClasses, pushed to the browser with send_state on the viewer’s source (a re-render, not a re-layout). The selection tool is never touched, so longeron.diagrams.on_select() callbacks cannot fire from a search; and if the browser replaces the view tree (e.g. after a collapse/relayout), the active search is re-applied automatically.

query

live search text; empty clears the highlight

match_count

how many searchable nodes match the query

total_count

how many searchable nodes the diagram has

property hit_ids: frozenset[str]

The ids of the nodes the current query highlights.

refresh_highlights()[source]

Recompute matches and apply/clear highlight classes everywhere.

Return type:

None

class longeron.toolbar.DirectionTool(diagram, **kwargs)[source]

Bases: Tool

Toggle the diagram’s layout flow and re-lay it out.

The button toggles elk.direction RIGHT (left-to-right, the default) <-> DOWN (top-to-bottom) and the choice persists per widget on the direction trait (initialized from the diagram root’s elk.direction, so the direction= constructor kwarg carries through). Setting the trait directly works too: either way the option lands on the ROOT only (apply_direction() – unlike edge routing, elkjs carries the direction into nested compounds under INCLUDE_CHILDREN), the pipeline inlet is marked dirty with the layout-options flow, and the diagram refreshes through the SAME pipeline the routing tool uses. The flip also queues a ONE-SHOT re-fit (AutoFitTool): the aspect ratio inverts, so keeping a viewport framed for the old flow reads worse than re-centering once.

direction

active elk.direction flow

apply()[source]

Push the active direction onto the diagram’s trees and re-lay out.

Return type:

None

longeron.toolbar.EdgeRouting

the edge-routing styles a diagram view accepts (routing= on every view constructor; the toolbar’s routing button cycles the same set). The lowercase spellings are the public vocabulary; ROUTING_STYLES holds the equivalent elk.edgeRouting option values, and apply_routing() normalizes case between the two.

alias of Literal[‘orthogonal’, ‘polyline’, ‘splines’]

class longeron.toolbar.EdgeRoutingTool(diagram, **kwargs)[source]

Bases: Tool

Cycle the diagram’s ELK edge routing style and re-lay it out.

SysML tools (and the spec’s own figures) mix straight and orthogonal connectors; ELK layered supports both plus splines. The button cycles ORTHOGONAL -> POLYLINE -> SPLINES -> … and the choice persists per widget on the routing trait (default ORTHOGONAL, initialized from the diagram root’s elk.edgeRouting so the routing= constructor kwarg carries through). Setting the trait directly works too: either way the style lands on the root and every compound node (apply_routing()), the pipeline inlet is marked dirty with the layout-options flow, and the diagram refreshes through the SAME pipeline the other tools use – a true re-layout, not a re-render. Endpoint glyphs survive non-orthogonal paths in both pipelines: the browser rotates symbols to the endpoint segment’s angle, the headless markers orient with auto-start-reverse.

routing

active elk.edgeRouting style

apply()[source]

Push the active style onto the diagram’s trees and re-lay out.

Return type:

None

longeron.toolbar.LayoutDirection

the layout flow directions a diagram view accepts (direction= on every view constructor; the toolbar’s orientation button toggles the same pair). Lowercase is the public vocabulary; DIRECTIONS holds the elk.direction option values, and apply_direction() normalizes case between the two.

alias of Literal[‘right’, ‘down’]

longeron.toolbar.apply_direction(root, direction)[source]

Set elk.direction on a diagram source tree; returns the normalized direction name.

ROOT-ONLY, unlike apply_routing(): elkjs DOES carry the layout direction into nested compounds under INCLUDE_CHILDREN (verified empirically – a root-only DOWN stacks nested children vertically, and restating the option per level changes nothing), while the SEPARATE_CHILDREN packing grids keep their own default flow (they stay wide either way, which is what the pack-aspect chains assume).

The per-node companions are _fit_compound_labels() (elkjs sizes EXPANDED compound nodes on the wrong axis under vertical flows, so the compartment-bearing containers get their width pinned or their horizontal defaults restored alongside every direction change) and _orient_glyphs() (direction-sensitive glyph geometry – fork/join bar dimensions and the fixed-side convergence anchors – is re-derived for the new flow axis).

Already-computed edge routes are dropped for the same reason apply_routing() drops them: a direction change makes them stale, and elkjs writes new routes INTO old section objects without clearing leftover keys, so re-laying out a laid-out tree would not be idempotent otherwise.

Return type:

str

longeron.toolbar.apply_routing(root, routing)[source]

Set elk.edgeRouting on a diagram source tree; returns the normalized style name.

The option goes on the root AND on every compound node: ELK does not inherit it through INCLUDE_CHILDREN hierarchy levels (elkjs routes a container’s edges with the CONTAINER’s option, so a root-only value leaves every nested edge orthogonal) – restated per level, exactly like the edge-node clearance in longeron.diagrams._finish().

Any already-computed edge routes are dropped: a routing change makes them stale, and elkjs writes new routes INTO the old section objects without clearing leftover keys (an orthogonal re-route of a polyline section keeps the polyline bendPoints), so re-laying out a laid-out tree would not be idempotent otherwise.

Return type:

str

longeron.toolbar.upgrade_toolbar(diagram)[source]

Swap the stock ipyelk toolbar contents for the compact longeron one.

Idempotent, and composed entirely from the outside: the existing Fit/Center/Toggle-Collapsed tools keep their behavior but lose the text labels (icon + tooltip instead), an EdgeRoutingTool (cycles orthogonal/polyline/splines edge routing), a DirectionTool (toggles left-to-right/top-to-bottom flow) and a DiagramSearch tool are registered, and TOOLBAR_STYLE is merged into the widget’s scoped stylesheet (search-hit/dim rules + keeping the toolbar pinned while it is being used).

Return type:

Any