View persistence¶
Saving diagrams as SysML v2 views, and restoring them. Design rationale and the adopted two-tier scheme: Saving diagrams as SysML v2 views.
Persist diagrams as SysML v2 views (and restore them).
Design: docs/design/view-persistence.md
– the ratified two-tier scheme. The standard tier writes a
ViewUsage into the model itself: typed by the matching
StandardViewDefinitions view definition, exposing the shown elements
through expose relationships, and naming the spec rendering with a
render reference. That tier travels through .sysml text, the
Systems Modeling API, and every conformant tool. The sidecar tier is
a small versioned JSON file (.longeron/views.json next to the model
sources) carrying only what the standard cannot express: layout
direction, edge routing, collapse state, and the longeron diagram kind –
keyed by the view usage’s qualified name, so a model without the
sidecar still restores correctly with default presentation.
import longeron
from longeron import views
model = longeron.load("rig.sysml")
widget = longeron.diagrams.structure_diagram(model.find("Rig"))
views.save_view(model, widget, name="axle structure",
sidecar=views.sidecar_path(model))
# ... later, or in another tool entirely ...
views.restore_view(model, "Rig::axle structure")
Geometry is deliberately persisted nowhere: ELK re-derives layout from the persisted inputs (exposed elements, direction, routing), so a saved view never rots into stale pixel coordinates.
Restore resolves the expose closure through the resolver
(expose_closure() is the exact machinery, exposed for reuse):
membership exposes yield the named element (plus its subtree when
recursive), namespace exposes yield the target’s members, and filter
conditions restrict the closure. Metaclass filters (@SysML::PartUsage,
the dominant spec idiom) evaluate against longeron’s kind vocabulary;
arbitrary model-level filter expressions are preserved and exported but
not applied to the closure (the design doc’s scope fence). A dangling
expose warns and is skipped – never an exception – and the same
condition surfaces in longeron.validate() as the dangling-expose
diagnostic.
Saving is append-only and idempotent: a new view usage is appended to the scope’s owning package (keeping index-path element ids stable), and saving under an existing view name replaces that view usage’s recipe (exposes, filters, render) and its sidecar entry in place.
- longeron.views.VIEW_KINDS: tuple[Literal['structure', 'state', 'action', 'requirements'], ...] = ('structure', 'state', 'action', 'requirements')¶
the runtime table
ViewKindprojects to (the model.py house pattern: the Literal is the authority, so the two cannot drift)
- class longeron.views.ViewInfo(element, qualified_name, kind, exposes=<factory>)[source]¶
Bases:
objectOne view usage and its persistence-relevant metadata.
- longeron.views.ViewKind¶
the longeron diagram kinds a view can persist (mirrors
explorer.DIAGRAM_KINDS; asserted equal by the test suite)alias of
Literal[‘structure’, ‘state’, ‘action’, ‘requirements’]
- longeron.views.capture_presentation(widget)[source]¶
The live presentation of a diagram widget, in sidecar vocabulary.
Reads the CURRENT layout direction and edge routing off the widget’s source tree (the toolbar tools re-apply their traits there, so live toggles are captured) and the collapse state: the structure view’s per-node levels and per-compartment folds (
longeron.diagrams.CollapseTool) plus any nodes whose children are all hidden (the state/action widgets’ stock collapse, the legacycollapsedkey). Only deviations from the defaults are returned – absent sidecar keys mean defaults, so the file only grows when a user actually deviates from them.
- longeron.views.expose_closure(model, view, *, resolver=None)[source]¶
The elements a view usage exposes (its
exposedElementset).Each
exposeresolves through the resolver: a membership expose yields the named element (plus its whole subtree when recursive,X::**), a namespace expose yields the target’s members (X::*; all nested members when recursive). Filter conditions – the expose’s own bracket filters, the view’sfiltermembers, and conditions inherited from an in-model view definition – restrict the closure; metaclass filters evaluate, anything else is preserved but not applied (module docstring). A dangling expose warns and is skipped, never raises. Order is expose order, then tree order; duplicates are dropped.
- longeron.views.list_views(model)[source]¶
Every view usage in
model, with kind and expose metadata.The exposed closure of a view is computed on demand by
expose_closure()(it needs a resolver walk); this listing stays cheap and never warns.
- longeron.views.load_sidecar(path)[source]¶
Read a sidecar file; returns the per-view entries keyed by view qualified name (
{}when the file does not exist).Forward-compatible by contract: any
version >= 1is accepted and unknown per-view keys are preserved (they ride throughsave_sidecar()untouched). A file that is not a longeron views sidecar raisesSysMLError– silently treating it as empty would overwrite foreign data on the next save.
- longeron.views.restore_view(model, view, *, sidecar=None)[source]¶
Rebuild the diagram a view usage describes; returns the widget.
Three steps, per the design doc: the view’s typing picks the diagram builder (untyped views fall back to the
renderreference, then the sidecarkind, then structure – unknown view definitions warn and fall back), the expose closure yields the diagram scope (expose_closure(); dangling exposes warn and are skipped, a fully dangling view restores to an empty diagram), and the sidecar entry re-applies presentation:direction,routing, builderoptions, and the collapse state (structure/requirements widgets rebuild per-nodelevelsand per-compartmentfoldedthroughstructure_diagram(levels=..., folded=...); a legacy flatcollapsedlist maps to the smallest rendition, level"collapsed"; state/action widgets keep the stock hidden-children collapse). No sidecar entry means spec content with default presentation – the degraded mode IS the standard mode.sidecarmay be a path, an already-loaded entries mapping, orNoneto auto-discover the workspace sidecar next to the model’s sources (silently absent for in-memory models). Needs the diagram toolchain (the vendored ipyelk), likelongeron.diagrams.- Return type:
- longeron.views.save_sidecar(path, views, *, model=None)[source]¶
Write the sidecar file (schema
longeron/views, version 1).Entries are written under sorted qualified-name keys with stable two-space indentation – small, diffable, merge-friendly. When
modelis given, entries whose qualified name no longer resolves in it are PRUNED (the design doc’s orphan rule: a view deleted by another tool cannot wedge the sidecar). Returns the file path.- Return type:
- longeron.views.save_view(model, exposed, *, name=None, kind=None, options=None, sidecar=None)[source]¶
Write a diagram into
modelas a SysML v2 view usage.exposednames what the view shows: a longeron diagram widget (its root element, diagram kind, and non-default builder options are read off the widget – including live toolbar direction/routing and collapse state), a model element, a qualified name, or a list of elements/names. Each exposed element becomes one recursive membership expose (expose X::**– the element and its subtree).The view usage is typed by the matching
StandardViewDefinitionsview definition and carries arenderreference toViews::asInterconnectionDiagram(the design doc’s mapping table);kinddefaults to the widget’s diagram kind, else it is inferred from the first exposed element (state/actionelements pick their machine views, everything elsestructure).namedefaults to"<element> <kind>".Append-only, idempotent semantics (ratified): a NEW view is appended to the scope’s owning package – appending keeps existing index-path element ids stable – while saving under an existing view name REPLACES that view usage’s recipe (typing, exposes, filters, render) in place.
optionsseeds the sidecar entry: the presentation keys (direction,routing,collapsed) plus any diagram-builder kwargs (membership,submachine_depth,lanes, …). Whensidecaris a path, the entry is written there under the view’s qualified name (seesave_sidecar()); withsidecar=Nonethe model edit alone is performed and the caller owns any sidecar write.Returns the view usage element (freshly appended or replaced).
- Return type:
- longeron.views.sidecar_path(source)[source]¶
The workspace sidecar location for a model or a source path.
One JSON file per workspace,
.longeron/views.json, next to the.sysmlsources: for a file that is<dir>/.longeron/views.jsonbeside it, for a directory it lives inside the directory. A model resolves through itssource_name; models not loaded from disk (loadstext, merged multi-path models) returnNone.
- longeron.views.view_kind(view)[source]¶
The longeron diagram kind stated by a view usage’s typing.
Matches the last segment of each declared type against the
StandardViewDefinitionsmapping table (InterconnectionView->structure,StateTransitionView->state, …);Nonewhen the view is untyped or typed by an unknown view definition.