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,~Tconjugation 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; theactor_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=Trueadds comment/doc notes with dashed anchor lines and «@Type» metadata adornments.state_diagram()– hierarchical states, entry markers, transitions labeledtrigger [guard] / effect; state usages typed by a state def expand into the definition’s submachine (submachine_depthbounds 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:
ToolPer-node collapse through the THREE levels of a structure box, plus per-compartment folds.
The structure view replaces ipyelk’s stock
ToggleCollapsedToolwith 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 : Typerows 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-wideparts="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) andfolded(qualified name -> folded compartment names) are the state seams: the toolbar button and header clicks toggle them, thelevel()/fold()kernel API edits them, view persistence captures them (longeron.views.capture_presentation()) and re-seeds them throughstructure_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
newflow).- Return type:
- 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:
- 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 suppressedalias 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 : Typerows 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, orTrueto derive lanes fromperformtargets (perform part1.action1lands in lanepart1). 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=Falsekeeps ipyelk’s stock toolbar;routingpicks the edge routing style (orthogonal / polyline / splines);directionthe layout flow ("right", the flow-reading default, or"down").max_label_widthcaps compartment-row display width exactly likestructure_diagram()(behavior boxes carry no rows today, so the cap is future-proofing);heightpins the widget’s rendered height to a CSS length exactly likestructure_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.sectionis 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:
- longeron.diagrams.level(widget, element, to=None)[source]¶
Get or set one node’s collapse level on a structure diagram.
to=Nonereturns 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 (seeCollapseTool). 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
callbackwith 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_edgesseam instead.- Return type:
- 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 (StateMachinedescends throughmembers_of). Expansion is recursive and cycle-safe: a definition reached again through its own submachine draws as a collapsed leaf.submachine_depthbounds how many typing hops to expand:None(the default) is unlimited,0draws typed states as plain leaves (the pre-0.8 behavior). Plain nested states are always shown.toolbar=Falsekeeps ipyelk’s stock toolbar;routingpicks the edge routing style (orthogonal / polyline / splines);directionthe layout flow ("right"or"down");max_label_widthcaps compartment-row display width exactly likestructure_diagram()(state boxes carry no rows today, so the cap is future-proofing);heightpins the widget’s rendered height to a CSS length exactly likestructure_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 whatlongeron.replayrecords: 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, soshow_relationships=Falsekeeps them.Port usages owned by a drawn definition/usage box render as the spec’s boundary squares (10x10, straddling the border,
name : Typelabel 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.partspicks 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 textualname : Typerows 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.levelsnames 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 thelevel()kernel API (seeCollapseTool)."partial"is the per-node version ofparts="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 themembershipmode.foldednames 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; thefold()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’
: Typetext carries it) – all exactly as under the diagram-wideparts="rows";parts="rows"– every node is already textual, so"partial"changes nothing there and the toolbar cycle skips it (expanded -> collapsed -> expanded);"collapsed"andfoldedwork 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=Falsekeeps ipyelk’s stock text-button toolbar instead of the compact icon+search one (longeron.toolbar).routingpicks 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.directionpicks the layout flow –"right"(left-to-right, the default) or"down"(top-to-bottom); the toolbar’s orientation button toggles it live.max_label_widthcaps 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.Nonelifts the cap (every row at full width).heightpins 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 defaultNonekeeps 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 Collapsedtext buttons become icon-only buttons with tooltips (the underlyingToolinstances are reused, so behavior is exactly ipyelk’s; on STRUCTURE viewslongeron.diagramsthen swaps the stock collapse tool for its three-levelCollapseToolin the same slot);an
EdgeRoutingToolbutton 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’sroutingtrait;a
DirectionToolbutton 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’sdirectiontrait (seeded by thedirection=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)
AutoFitToolfits-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_FitSentinelcompanion 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:nonelifted, 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
DiagramSearchtool is registered: typing in its text box live-highlights every diagram node whose title or qualified name contains the query (case-insensitive), shows amatches/totalcount, 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:
ToolKeep 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 oneFitToScreenActionrequest:paddingpx of margin, zoom capped atmax_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
sentinelexists for – a hidden anywidget the builder mounts INSIDE the diagram widget’s own DOM whose browser half reportsfreshviews, first reveals, and untouched-viewport resizes (see_sentinel_class()); each report is answered withrefit_now(), which also bumps the sentinel’sfit_stampto clear the browser-side user-interaction latch. Without anywidget the sentinel isNoneand 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
- 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_stampafterwards 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:
- class longeron.toolbar.DiagramSearch(diagram, **kwargs)[source]¶
Bases:
ToolLive 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; a3/41counter reports matches over searchable nodes. Clearing the query restores the diagram exactly.The highlight mechanism is css-only:
sysml-search-hit/sysml-search-dimfragments onproperties.cssClasses, pushed to the browser withsend_stateon the viewer’s source (a re-render, not a re-layout). The selection tool is never touched, solongeron.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
- class longeron.toolbar.DirectionTool(diagram, **kwargs)[source]¶
Bases:
ToolToggle the diagram’s layout flow and re-lay it out.
The button toggles
elk.directionRIGHT (left-to-right, the default) <-> DOWN (top-to-bottom) and the choice persists per widget on thedirectiontrait (initialized from the diagram root’selk.direction, so thedirection=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 underINCLUDE_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
- 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_STYLESholds the equivalentelk.edgeRoutingoption values, andapply_routing()normalizes case between the two.alias of
Literal[‘orthogonal’, ‘polyline’, ‘splines’]
- class longeron.toolbar.EdgeRoutingTool(diagram, **kwargs)[source]¶
Bases:
ToolCycle 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
routingtrait (default ORTHOGONAL, initialized from the diagram root’selk.edgeRoutingso therouting=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 withauto-start-reverse.- routing¶
active elk.edgeRouting style
- 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;DIRECTIONSholds theelk.directionoption values, andapply_direction()normalizes case between the two.alias of
Literal[‘right’, ‘down’]
- longeron.toolbar.apply_direction(root, direction)[source]¶
Set
elk.directionon a diagram source tree; returns the normalized direction name.ROOT-ONLY, unlike
apply_routing(): elkjs DOES carry the layout direction into nested compounds underINCLUDE_CHILDREN(verified empirically – a root-onlyDOWNstacks nested children vertically, and restating the option per level changes nothing), while theSEPARATE_CHILDRENpacking 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:
- longeron.toolbar.apply_routing(root, routing)[source]¶
Set
elk.edgeRoutingon 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_CHILDRENhierarchy 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 inlongeron.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:
- 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), aDirectionTool(toggles left-to-right/top-to-bottom flow) and aDiagramSearchtool are registered, andTOOLBAR_STYLEis merged into the widget’s scoped stylesheet (search-hit/dim rules + keeping the toolbar pinned while it is being used).- Return type: