Labels. This is an engineering record. "Track W" and its items (
W1–W3) name entries of the roadmap, where each is stated in full; a reader who only wants the design can ignore them.
Status: text, markdown, mermaid, dot and plantuml implemented — dot is Track W's
W1 and plantuml its W2, both wired into every surface W3 names. This page records how a
view's rendering is separated from the forms it is written in, why Graphviz DOT and PlantUML are
offered next to Mermaid, and what the DOT and PlantUML writers emit — the
DiagramLayout geometry included.
A view renders into a view.Rendering (internal/ir/view/view.go): the kind (tree,
interconnection, state, action, sequence, table), typed nodes with an identifier, a
kind, a name, the declared type of a typed usage, an optional detail holding the notes (initial,
already shown, own flow) and their children, edges with a label and an EdgeKind
(connection, transition, succession, flow), a table's columns and rows, the origin of every node
and row, and notices for what the kind could not represent. The tree, interconnection, state and
action kinds are produced from the model — the last two from the lowered StateGraph and
ActionGraph the runtime executes — and nothing in the rendering is text of any diagram
language.
A form is a writer over that tree (internal/ir/view/form.go):
| Form | Writer | Kinds | Role |
|---|---|---|---|
text |
text.go |
every kind | What a person reads at a terminal |
markdown |
markdown.go |
table |
The machine-readable form of a table |
mermaid |
mermaid.go |
tree, interconnection, state, action, sequence |
The default machine-readable form of the graph-shaped kinds |
dot |
dot.go |
tree, interconnection, state, action |
Graphviz DOT, the alternative to Mermaid |
plantuml |
plantuml.go |
tree, interconnection, state, action, sequence |
PlantUML in the Pilot visualizer's B&W style, for PlantUML toolchains |
Kind.MachineForm chooses the form a tool gets when none is asked for — markdown for a table,
mermaid for everything else — and Kind.SupportsForm decides whether a kind can be written in
a form at all. Asking for a form the kind is not written in is one typed WrongFormError, naming
the kind, the form asked and the form the kind uses, on every surface: the CLI stops with status 2
(-render-all skips the view and says so), the REPL prints the usage, the LSP refuses the request,
and a document's Diagram block is refused at planning time.
Every graphical form draws a node's label the way the graphical notation heads a compartment:
the element's name first, the kind after it. label.go composes the lines once, and each writer
only joins them:
- the name, with
: Typeafter it for a typed usage (pump : Pump); a definition has just its name; an anonymous element leads with its kind instead; - the kind in guillemets,
«part»,«state def»— left out when line 1 is already the kind; - the detail, when there is one.
Mermaid joins the lines with <br> in every grammar it writes — a flowchart node label, a
state "…" as n and a participant n as … — which the pinned mermaid-cli breaks at whether
htmlLabels is on (the text becomes HTML, <br> a line break) or off (the label is split into
<tspan> rows); no <br> survives as text in the drawing. The tree, interconnection and action
kinds draw the same flowchart labels. A flowchart reserves one line of height for a subgraph
title and draws the first child over the rest, so a rendering whose cluster title spans several
lines opens on a YAML frontmatter block, config: flowchart: subGraphTitleMargin: bottom: <n>,
claiming 24px per extra line as the title's bottom margin (writeFlowchartFrontmatter); the
block rides the text into every consumer, and a flowchart without such a cluster, a tree, a
state diagram and a sequence diagram carry none. Every subgraph opens on a direction
statement restating the flowchart's own (TD, LR for an interconnection, or the one asked
for), since Mermaid lays out a subgraph that states none without regard to the flowchart's;
a tree draws containment as edges, not subgraphs, so it states none. DOT writes an HTML-like
label, label=<<b>pump : Pump</b><br/><font point-size="10">«part»</font>>, the name in bold
and the keyword line under the 14pt Graphviz draws the rest in; &, <, > and " in a name become entities so no name
reads as markup. A cluster's label is the same string. The text form keeps the notation's
declaration order, part pump : Pump, with a detail parenthesised after it. The declared type is
a field of the node (Node.Type, type in the JSON), never parsed back out of the detail.
Mermaid was chosen first because it draws where models are read — Markdown, documentation sites, editors — with nothing installed. It stays the default. DOT is offered beside it for what Mermaid is not:
- Graphviz toolchains. Publishing pipelines that already run
dot,neatoorfdptake DOT as input and produce SVG, PDF or PNG with a layout Mermaid's browser renderer cannot match on a graph of hundreds of nodes. - Exact positions. DOT has a native vocabulary for a node's position (
pos), size and an edge's route, which Mermaid lacks. The writer fills it from the rendering's DiagramLayout geometry (below), so a view laid out in an editor is drawn by Graphviz where the editor put it; the Mermaid form can only carry the same numbers as%%comments.
Producing DOT needs no Graphviz installation. The writer is text over the rendering tree,
exactly as mermaid.go is, and neither the writer nor its tests run a Graphviz binary. The one
place that does is the PDF backend, and only to draw the figure it embeds: internal/doc/docpdf
runs the dot that OPENSYSML_DOT names (else the one on PATH) with -Tsvg, under the engine
the block's // layout: header names, so a positioned view is drawn where its Layouts put
it. Without a Graphviz the PDF keeps the DOT source under a notice, as it does without any
optional tool — see Surfaces.
// view: VehicleViews::vehicleView
// kind: tree
// layout: dot
digraph "VehicleViews::vehicleView" {
graph [fontname="Helvetica"];
node [shape=box, style=filled, fillcolor=white, color="#181818", fontname="Helvetica", fontsize=14, penwidth=0.5];
edge [color="#181818", fontname="Helvetica", fontsize=13, penwidth=1];
"n0" [label=<<b>Vehicles::Vehicle</b><br/><font point-size="10"><i>«part def»</i></font>>];
"n1" [style="rounded,filled", label=<<b>engine : Engine</b><br/><font point-size="10"><i>«part»</i></font>>];
"n0" -> "n1" [arrowhead=none];
}-
Header.
// view: <name>when a view was named,// kind: <kind>,// stated: <how the kind was decided>when the rendering records it, one// not represented: <notice>per notice — the same facts the Mermaid form writes as%%comments — and// layout: dot. -
Graph.
digraph "<view>"(digraphalone for a pseudo-view), agraphstatement with the font andrankdir=<dir>when a direction is asked for, thenodeandedgedefaults of the style below, andcompound=trueonly when an edge ends at a cluster. -
Nodes. A leaf is
"<id>" [label=<<b><head></b><br/><font point-size="10"><i>«<kind>»</i></font><br/><detail>>], the label lines above as an HTML-like string, the detail line omitted when empty; a usage addsstyle="rounded,filled"before its label. In an interconnection, state or action rendering a node with children issubgraph "cluster_<id>" { label=<…>; color=black; penwidth=<w>; … }, the containment Mermaid writes assubgraph; in a tree, containment is anarrowhead=noneedge, as the Mermaid tree draws it, so a tree has no clusters. Since DOT edges join nodes, not subgraphs, every cluster holds an invisible, sizeless anchor node named by the cluster's own ID; an edge whose end is a cluster names that anchor, so the rendering's endpoints survive verbatim, and is clipped at the cluster withlhead/ltail— except at an end that encloses the other, where the edge starts or ends inside it rather than at a border it never crosses. -
State kind. A state is a rounded box, a region a dashed cluster, the start pseudo-state a
point, an initial state acircle, a final state adoublecircle— an unnamed initial or final one the filled black UML dot, a named one a labelled ring; a transition's label is the trigger/guard/effect text the state writer composes, unchanged. -
Edges. The
EdgeKindstyles parallel the Mermaid arrows so the two forms read alike:EdgeKindMermaid DOT connection ---arrowhead=none, penwidth=3transition, succession -->solid, default arrowhead flow -.->style=dashed -
Quoting. Every identifier, edge label and geometry value passes through one helper that double-quotes it and escapes
",\and newlines; a node or cluster label is an HTML-like string whose text passes through one helper that writes&,<,>,"and'as entities. The writer never emits an unquoted identifier or unescaped label text. -
Order. Nodes and edges are written in the rendering's order; nothing is emitted from a map. Within an attribute list, what a node is (shape, style, colours, label) precedes where it is (
pos,width,height).
Three methods of the writer produce every attribute list — graphAttributes,
dotNodeAttributes (with dotClusterAttributes and dotAnchorAttributes for a node drawn as a
cluster) and dotEdgeAttributes — so what is said about a node or an edge changes without
touching how the graph is walked.
The DOT form is drawn in the Standard B&W style of the OMG SysML v2 Pilot Implementation's
PlantUML visualizer, after the sysmlbw PlantUML skin by Hisashi Miyashita (Mgnite Inc.) shipped
with it — github.com/himi/plantuml, branch psysml,
bundles/net.sourceforge.plantuml.lib/skin/sysmlbw.skin — and the edge rules of the Pilot's
org.omg.sysml.plantuml/src/org/omg/sysml/plantuml/SysML2PlantUMLStyle.java. The Pilot source is
EPL-2.0 (its file header). The skin file carries no licence header of its own; the bundle it ships
in, net.sourceforge.plantuml.lib, is under the Eclipse Public License v1.0 (its COPYING), the
licence the fork's README names for the whole repository. This project reproduces the skin's visual
parameters (colours, line widths, font choices) in Graphviz's and PlantUML's vocabularies, not its
text. The Pilot's default is skin sysmlbw, skinparam monochrome true and hide circle; the
translation to DOT is:
| Skin / Pilot rule | DOT |
|---|---|
FontName SansSerif, FontSize 14, FontColor black |
graph, node and edge default fontname="Helvetica", Graphviz's portable sans-serif; fontsize=14 on nodes; text stays black |
BackGroundColor #ffffff, monochrome true |
node default style=filled, fillcolor=white; no other colour without a palette |
LineColor #181818, element { LineThickness 0.5 } |
node default color="#181818", penwidth=0.5 |
RoundCorner 0 for definitions, UsageRoundCorner 20 for usages |
a definition (… def, or a KerML classifier keyword) keeps shape=box; a usage adds style="rounded,filled". Graphviz's corner radius is fixed, so the 20-unit radius is approximated |
stereotype { FontStyle italic } |
the «keyword» label line is <i>…</i> at its 10 pt size |
element { title { FontStyle bold } } |
the name line is <b>…</b> |
stateDiagram { element { title { FontStyle plain } } } |
not followed: a state's name stays bold, as in every other kind and in the text and Mermaid forms, so the four forms read alike |
group { BackGroundColor transparent; LineThickness 1.0 }, package { LineThickness 1.5; LineColor black }, stateDiagram { group { LineThickness 0.5 } } |
a cluster is unfilled with color=black; penwidth=1.5 when its kind is a package, penwidth=0.5 for a cluster standing for an element and for a region (which keeps style=dashed) |
arrow { FontSize 13; LineThickness 1.0 } |
edge default color="#181818", fontsize=13, penwidth=1 |
Pilot caseConnectionUsage, caseConnector: -[thickness=3]- |
EdgeConnection: arrowhead=none, penwidth=3 |
Pilot caseFlow, caseSuccession, caseTransitionUsage: --> |
the EdgeKind table above, unchanged |
| initial and final pseudo-states | the UML filled black dot: shape=circle (doublecircle for a final), fillcolor=black, label="", width=0.2 unless a Layout sizes it; a pseudo-state the rendering names keeps its labelled ring. The start point is unchanged |
Not translated, because Graphviz has no vocabulary for them: Shadowing 0 (no shadows to turn
off), hide circle (no class circles), wrapWidth 300 (DOT does not wrap label text), and the
20-unit corner radius. Out of scope: the skin's notes, sequence, gantt, mindmap and wbs
sections — the DOT form draws no notes and a sequence rendering has no DOT form — and the
Pilot's -[thickness=5]- binding connectors, which the interconnection rendering does not
distinguish from connections today (there is no EdgeBinding kind), so DOT cannot draw them
apart either; that is a known limitation, not an approximation.
A view.Palette fills the DOT and PlantUML forms' nodes by keyword family, the way the Pilot's
STDCOLOR mode does, so a part def and a part share a hue. The empty palette is the B&W
default above. Every named palette is colourblind-safe:
| Name | Source | Colours |
|---|---|---|
okabe-ito |
Okabe & Ito, Color Universal Design (2002, jfly.uni-koeln.de/color) | #E69F00 #56B4E9 #009E73 #F0E442 #0072B2 #D55E00 #CC79A7 #999999 |
tol-bright |
Paul Tol, Colour Schemes, SRON technical note 3.2 (2021), personal.sron.nl/~pault | #4477AA #EE6677 #228833 #CCBB44 #66CCEE #AA3377 #BBBBBB |
tol-muted |
same note | #332288 #88CCEE #44AA99 #117733 #999933 #DDCC77 #CC6677 #882255 #AA4499 #DDDDDD |
tol-light |
same note | #77AADD #99DDFF #44BB99 #BBCC33 #AAAA00 #EEDD88 #EE8866 #FFAABB #DDDDDD |
brewer-set2 |
ColorBrewer 2.0 Set2, Cynthia Brewer (colorbrewer2.org) | #66C2A5 #FC8D62 #8DA0CB #E78AC3 #A6D854 #FFD92F #E5C494 #B3B3B3 |
brewer-dark2 |
ColorBrewer 2.0 Dark2 | #1B9E77 #D95F02 #7570B3 #E7298A #66A61E #E6AB02 #A6761D #666666 |
viridis |
matplotlib's viridis (van der Walt & Smith; CC0), 16 evenly spaced stops | #440154 … #FDE725 |
cividis |
matplotlib's cividis (Nuñez, Anderton & Renslow 2018; CC0), 16 stops | #00224E … #FEE838 |
ColorBrewer notice: Set2 and Dark2 are colour specifications and designs developed by Cynthia Brewer (http://colorbrewer.org/), licensed under the Apache License, Version 2.0.
- Families, in fixed order: part, item, port, attribute, action, state, requirement,
constraint, connection, interface, use case, case, allocation, analysis, verification, enum,
occurrence, flow, then anything else. A kind is placed by the first of its words with a family
(
perform actionis an action,analysis casean analysis), thedefsuffix set aside. The family's place in this order is its index into a qualitative palette, so a diagram with only parts and ports uses the palette's first and third colours whatever else is absent; a palette shorter than the families wraps round. A sequential palette (viridis,cividis) is instead sampled evenly across the families present in the rendering, darkest first. - Definitions and usages. A definition takes the family colour as
fillcolor; a usage a tint of it, blended 60 % toward white. Both borders are the untinted family colour atpenwidth=1. The PlantUML form writes the same fill as#hexon the element declaration, with the border as;line:hex, so the two forms agree on every node's hex (TestPlantUMLPaletteParityWithDOT); a sequence participant takes the fill alone, PlantUML accepting no border colour on one. - Contrast. Text stays black. Every fill — definition or usage — is lightened toward white,
a hundredth at a time, until black text on it reaches the WCAG 2 level-AA ratio of 4.5:1; a
colour already legible is unchanged. One function (
paletteFill) defines the blend, and a test asserts the ratio for every colour of every palette at both tints. - What stays black and white. Pseudo-states, control nodes (fork, join, decision, …) and cluster borders keep the B&W rules under every palette; only plain nodes are filled.
- Other forms. Mermaid writes a
%% not represented: palette <name>; only the DOT and PlantUML forms fill nodes by keyword familycomment and is not themed; text and Markdown ignore a palette silently. An unknown palette name is a typed*view.UnknownPaletteError(wrappingview.ErrUnknownPalette) naming the palettes there are, on every surface.
The palette API is shaped so a later caller can ask for the colour of category i of n
(Palette.Color(i, n)) without knowing about keyword families; colouring by a data attribute or
query result is not built.
A rendering carries the DiagramLayout annotations of its view — a
Rendering.Canvas, a Node.Geometry, an Edge.Route — in the library's units: pixels,
y down, origin at the canvas's top-left corner. Graphviz reads points, y up, from the
bottom-left; the writer converts, so the DOT it writes is laid out by Graphviz without a
preprocessing step:
// view: PlantViews::placedView
// kind: interconnection
// stated: render asInterconnectionDiagram
// canvas: unit=px w=1200 h=800
// layout: neato
digraph "PlantViews::placedView" {
graph [fontname="Helvetica", inputscale=72, dpi=72];
node [shape=box, style=filled, fillcolor=white, color="#181818", fontname="Helvetica", fontsize=14, penwidth=0.5];
edge [color="#181818", fontname="Helvetica", fontsize=13, penwidth=1];
"canvas:0" [shape=point, style=invis, width=0, height=0, label="", pos="0,800!", pin=true];
"canvas:1" [shape=point, style=invis, width=0, height=0, label="", pos="1200,0!", pin=true];
subgraph "cluster_n0" {
label=<<b>Plant::Loop</b><br/><font point-size="10"><i>«part def»</i></font>>;
color=black;
penwidth=0.5;
"n0" [shape=point, style=invis, width=0, height=0, label=""];
"n1" [style="rounded,filled", label=<<b>pump : Pump</b><br/><font point-size="10"><i>«part»</i></font>>, pos="359,741.5!", pin=true, width=1.6388888888888888, height=0.5138888888888888, comment="collapsed"];
"n2" [style="rounded,filled", label=<<b>tank : Tank</b><br/><font point-size="10"><i>«part»</i></font>>, pos="560,730!", pin=true, width=1.6666666666666667, height=0.8333333333333334, fixedsize=true];
}
"n1" -> "n2" [label="supply", arrowhead=none, penwidth=3, pos="400,730 400,730 450,680 450,680 450,680 500,730 500,730"];
}- Scale. One pixel is one point:
inputscale=72tellsneatothatposis in points, anddpi=72keeps the rendered pixel at that size. Lengths Graphviz takes in inches — a node'swidth/height— are divided by 72. - Axis.
yis flipped: measured up from the canvas's bottom edge (height - y) when the canvas states a height, negated when it does not.xis unchanged. - Canvas. A
Canvasis echoed in the header as// canvas: unit=<u> w=<w> h=<h>(the parts it states). When it has an extent and a node is positioned, an invisible, sizeless point is pinned at each of its corners —"canvas:0"at the origin,"canvas:1"at(w, h)— so the drawing's bounding box is the canvas, not the hull of the nodes: Graphviz recomputes the rootbband ignores asizelarger than the drawing, but it keeps a pinned node where it is. The names cannot collide with a rendering'sn<i>node IDs. - Nodes. A
Layoutnames the box's top-left corner; Graphviz positions a node's centre, so the writer pinspos="x,y!"at the centre of the box andpin=truekeepsneatofrom moving it. A stated size iswidth/heightin inches withfixedsize=true. Without one the writer sizes the box to the label itself — 0.6 em a glyph (0.66 em in the bold head), 1.2 em a line, at 14 pt for every line but the 10 pt keyword line, Graphviz's margins, no smaller than its 54×36 pt default box, a circle round the label for a pseudo-state, a 3.6 pt point for a start — and writes thatwidth/heightwithoutfixedsize, so Graphviz may still grow the box for its own font but the corner is where the Layout put it under the writer's estimate.collapsedis kept ascomment="collapsed", an attribute Graphviz ignores and a consumer can read. - Clusters. A node drawn as a cluster writes its box as
bb="llx,lly,urx,ury"and pins its anchor node at the box's centre. The box is the stated one, or, with a corner alone, the one from that corner round its positioned members' boxes with Graphviz's 8 pt cluster margin; a cluster with neither has no box to state and pins its anchor at the corner. - Edges. A
Routebecomesposas the cubic B-spline Graphviz reads: each segment's ends are its own control points, so the spline is the polyline through the waypoints. A route of one waypoint draws no line; it is left out and noticed as// not represented:. - Engine. The
// layout:header names the command that honours what is written:neato -n2when every node is positioned and any edge is routed (the pinned nodes and the written routes are taken as given, the other edges are drawn),neato -nwhen every node is positioned and no edge is routed,neatowhen only some nodes are (pinned nodes stay, the rest are placed around them),dotwhen none is.neatoanddotredraw every edge, so when the header names either and a route was written, a// not represented:notice says so. A rendering with no geometry is written byte for byte as before.
The writer is still text over the tree: no Graphviz binary is run to produce, check or test the output.
The plantuml form is for toolchains that draw with PlantUML — the OMG Pilot's own visualizer
among them — and for the one graph-shaped kind DOT has no grammar for, the sequence. It is
produced by pure text emission over the rendering tree, as the other forms are: no Java and no
PlantUML jar is needed to write it, and neither the writer, its tests nor the CLI, REPL and LSP
surfaces run one. A jar, when present on a developer's machine, checks the goldens by hand
(java -jar plantuml.jar -checkonly) or draws them; it is not a dependency of the writer. The PDF
backend alone runs it, to draw the figure it embeds: internal/doc/docpdf pipes each block through
java -jar $OPENSYSML_PLANTUML_JAR -tsvg -pipe, the java from OPENSYSML_JAVA or PATH, and
keeps the source under a notice when either is absent — see Surfaces.
@startuml
' VehicleViews::vehicleView — tree rendering
<style>
…
</style>
skinparam wrapWidth 300
hide stereotype
hide circle
hide empty members
class "**Vehicles::Vehicle**\n<size:10>//«part def»//</size>" as n0 <<part def>>
class "**engine : Engine**\n<size:10>//«part»//</size>" as n1 <<part>> <<usage>>
n0 -- n1
@endumlEvery file has the same shape, in this order:
@startuml.- The header comment,
' <view> — <kind> rendering (<stated>)('opens a PlantUML line comment), then one' not represented: <notice>line per notice of the rendering and per loss the writer itself incurs: a reversed direction, and geometry kept as comments. - The style block and
skinparam wrapWidth 300, thenhide stereotype. - The direction statement, when one applies:
top to bottom directionforTB,left to right directionforLR. PlantUML draws no reversed direction, soBTandRLwrite the nearest forward one and a' not represented: direction RL; …notice records the loss. The empty direction leaves PlantUML's default. State and action diagrams take the same statements; a sequence ignores direction, as Mermaid does. - The geometry as comments —
' canvas: unit=px w=800 h=600,' layout: <id> x=.. y=.. w=.. h=.. collapsed,' route: <from>-><to> x,y x,y …— the very lines the Mermaid form writes as%%comments, produced by the samewriteGeometryCommentswith the comment prefix as a parameter. PlantUML has no absolute positioning, so aLayoutorRouteis carried, not honoured; a notice counts what was kept. For pinned positions use thedotform. - The diagram body, per kind (below).
@enduml.
Aliases and labels. Every node is declared as <grammar> "<label>" as <id> <<stereotypes>>.
The rendering's node IDs are n<i> (and empty for an empty rendering), already word characters,
so they are the PlantUML aliases unchanged and the mapping is the identity. The label is the
name-first lines of label.go, joined with \n inside one double-quoted string:
the name line in creole bold (**…**), the keyword line italic at 10 pt
(<size:10>//«part»//</size>), the detail line plain. One helper, plantumlText, writes every
label and edge label so PlantUML shows it as it is: ", \, <, > and the creole escape ~
become <U+XXXX> escapes, as does each character of a run creole would read as markup (**,
//, __, --, [[, ]]), and a newline becomes \n. The bare guillemets « » render as
themselves in the released jar and are written bare.
Stereotypes. Each node carries its keyword as a stereotype, <<part def>>, <<state>>,
<<port>>, so a style rule can select it, plus one shape stereotype the style block keys on:
<<usage>> on every usage (rounded corners) and <<package>> on a package (heavier border); a
definition and an orthogonal region carry no shape stereotype and keep the element rules. PlantUML
would print every stereotype as its own «…» line above the name, which would put the keyword
line twice on the node and the shape stereotype beside it, so the file says hide stereotype:
the label prints the guillemet line, PlantUML does not — one keyword line, name first, as in
the Mermaid and DOT forms. The stereotypes still drive the style and the pseudostate shapes. A
control node's stereotype stands alone (<<start>>, <<fork>>, …), since PlantUML draws the
pseudostate shape only when nothing else is attached.
Per kind:
tree— a class diagram:hide circleandhide empty membersas the Pilot's style does, oneclass "…" as n<i> <<kind>>per node and containment as an undirected edgeparent -- childfrom each node to each of its children, written right after the child. This is how the Mermaid and DOT trees draw containment — a tree of edges, no nested containers — so the three forms show the same picture and a tree has no blocks. The Pilot'scomp def/comp usageelement kinds exist only in the PlantUML fork and are not emitted; every element is a standardclass.interconnection— nestedrectangleblocks: a node with children isrectangle "…" as n<i> <<kind>> {…}, indented two spaces a level, a leaf a one-linerectangle. A port is a nested rectangle inside its owner, not aportin/portout: the released jar's port grammar belongs tocomponentelements, and one grammar for every node keeps the style rules uniform. A connection is the Pilot's heavy undirected connectora -[thickness=3]- b : label, a flow a dashed arrowa -[dashed]-> b : label.state— the state grammar withhide empty description:state "…" as n<i> <<state>>, a body or composite state asstate … {…}holding its substates, the body's start as the[*]marker inside its block ([*] --> n1, one per start edge, after the substates — the Mermaid writer'sstartsmap, reused), and every other edge a transitiona --> b : labelwith the trigger/guard/effect text the state writer composes. The rendering's control kinds map to PlantUML's pseudostate stereotypes:initial→<<start>>,final→<<end>>,fork/join→<<fork>>/<<join>>,decision,choice,mergeandjunction→<<choice>>(PlantUML has no round junction),shallow history/deep history→<<history>>/<<history*>>. Only the nodes the rendering holds are written; the start pseudostate is the[*]marker and no other node is invented. Regions carry<<region>>, which the style dashes.action— the state grammar, uniformly. PlantUML's activity grammar is procedural (start,:action;,fork,if … then … endif): it draws a program, not a graph, and cannot hold an arbitrary set of action nodes joined by successions and flows — a node with two incoming successions, a flow crossing a fork, a nested body with its own start — without inventing structure the rendering does not have. Rather than write activity syntax where the graph happens to be linear and fall back elsewhere, which would give two grammars for one kind, every action rendering is a state diagram: actions are states, control nodes the pseudostates above, a nested body a composite state with its[*]start, a succession a solid-->, a flow a dashed-[dashed]->labelled as the DOT writer labels it. Every node and edge of every action golden is drawn, nesting included.sequence—participant "…" as n<i> <<kind>>per root in root order, thena -> b : labelper edge in edge order,a -> bfor an edge without a label — the same participants and messagesmermaid.go'swriteSequenceDiagramwrites. An empty rendering writes one participant carryingEmptyReason(). Under a palette a participant is filled like any usage by keyword family (fill only; PlantUML takes no border colour on a participant), so the palette is represented, not noticed.
Hyperlinks are not written: no writer derives a stable URL from Origin today, and the PlantUML
form adds none on its own; [[url]] links stay open with the DOT URL= attribute.
PlantUML proper does not ship the sysmlbw skin — it lives in the fork alone — so every file
carries the B&W rules itself, in a <style> block (PlantUML's CSS-like style language) with the
one skinparam the block cannot express. The translation of the same two sources the
DOT style credits:
| Skin / Pilot rule | PlantUML |
|---|---|
FontName SansSerif, FontSize 14, FontColor black, HorizontalAlignment left, BackGroundColor #ffffff |
root { BackGroundColor white; FontName SansSerif; FontSize 14; FontColor black; LineColor #181818; HorizontalAlignment left } |
LineColor #181818, element { LineThickness 0.5 }, Shadowing 0.0 |
element { BackGroundColor white; LineColor #181818; LineThickness 0.5; RoundCorner 0; Shadowing 0.0 } — shadows are turned off, which DOT could not |
RoundCorner 0 for definitions, UsageRoundCorner 20 for usages |
RoundCorner 0 on every element; .usage { RoundCorner 20 } on the <<usage>> shape stereotype, the skin's radius exactly — the fork's UsageRoundCorner property does not exist in released PlantUML, so the stereotype rule stands in for it |
stereotype { FontStyle italic }, element { title { FontStyle bold } } |
carried by the label, since the stereotype is hidden: the keyword line //…// at 10 pt, the name line **…** |
stateDiagram { element { title { FontStyle plain } } } |
not followed, as in DOT: a state's name stays bold so the forms read alike |
group { LineThickness 1.0 }, package { LineThickness 1.5 }, stateDiagram { group { LineThickness 0.5 } } |
.package { LineThickness 1.5 } on the <<package>> shape stereotype; every other block keeps the element's 0.5; .region { LineStyle 4 } dashes an orthogonal region as DOT does |
arrow { FontSize 13; LineThickness 1.0 } |
arrow { LineColor #181818; LineThickness 1; FontSize 13 } |
note BackGroundColor #FEFFDD, 13 pt |
note { BackGroundColor #FEFFDD; FontSize 13 } — no note is drawn today, the rule is there for one |
Pilot skinparam wrapWidth 300 |
skinparam wrapWidth 300, the one rule written as a skinparam; DOT could not wrap |
Pilot hide circle |
hide circle on the class diagram (a tree), where the circle exists |
Pilot -[thickness=3]- connectors, --> flows and successions |
-[thickness=3]- for EdgeConnection; --> for a transition or succession; -[dashed]-> for a flow, as the Pilot's VAction dashes flows |
| initial and final pseudo-states | PlantUML's own <<start>>/<<end>> dots, filled black by start, end, activityBar { BackGroundColor black } (the element rule would otherwise leave them and the fork/join bars hollow) |
So the three rules DOT could not honour — the 20-unit usage radius, shadows off and the 300 px
wrap — PlantUML honours in full; what PlantUML cannot honour and DOT does are absolute
positions and routes, kept as comments. skinparam monochrome true is not written: the rules
above already draw black and white, and monochrome would grey a palette's fills. Bindings draw at
connector weight, as in DOT, the rendering having no EdgeBinding kind.
A palette fills a node as #hex;line:hex after its stereotypes — one mechanism, the element
declaration, which the released jar honours on class, rectangle, state and participant
alike — with the palette rules shared with DOT unchanged: same family, same tint, same
contrast lightening, same hex per node. Pseudostates, control nodes and containers stay B&W under
every palette, and text stays black.
dot and plantuml are accepted wherever a form is chosen:
| Surface | Where | Documentation |
|---|---|---|
| CLI | -render <view> -render-form dot|plantuml; -render-all <dir> -render-form dot writes .dot files and -render-form plantuml writes .puml files; -render-palette <name> fills either |
docs/reference/cli.md |
| REPL | %render <view> dot|plantuml [palette]; %help names them; the form and, after a form that takes one, the palette complete |
docs/reference/repl-commands.md |
| LSP | "form": "dot" or "plantuml" and "palette": "<name>" on opensysml/render; a palette also gives each node of the result its fill and border, so a client drawing its own SVG colours a node as these forms do (Rendering.Fills) |
docs/reference/lsp.md |
| VS Code | SysML: Export Diagram picks among the forms the server lists under its openSysmlRenderForms capability (the documented five for a server without it), sends the pick as form, and saves .dot or .puml (.mmd, .md, .txt for the others) |
docs/guide/08-editors.md |
| VS Code | The diagram panel's Style list and opensysml.diagram.style: pilot draws the panel's SVG under this section's B&W rules, a palette name fills its nodes from the fill and border the server returns |
editors/vscode/README.md |
| Documents | -render-document/-render-documents … -diagram-form dot|plantuml, %render-document <name> dot|plantuml, "diagramForm" on opensysml/renderDocument: every graph-shaped diagram block as a ```dot or ```plantuml fence in Markdown, <pre class="dot"> or <pre class="plantuml"> in HTML; in PDF, a figure drawn by Graphviz (OPENSYSML_DOT, else dot on PATH; -Tsvg under the engine the // layout: header names) or by the PlantUML jar (OPENSYSML_PLANTUML_JAR, run by OPENSYSML_JAVA or the java on PATH, -tsvg -pipe), and the source under a notice naming the variable to set when the tool is absent; a tool that fails is the typed tool-failed error with its stderr, as mmdc is. The form is chosen at render time, not stated in the model: a Diagram block says what is drawn, not the notation — though it may state a palette, as it states a direction, which the DOT or PlantUML figure is filled with and the HTML figure carries as data-palette |
docs/manual/authoring.md, docs/manual/outputs.md, docs/reference/environment.md |
The gRPC service (api/proto/sysml.proto, internal/frontend/grpc) has no view-render RPC and no
render-form field — RenderDocument alone, to Markdown — so the wire contract carries no form
and did not change. A view-render RPC added later would take the form as a string, as
-render-form does.
internal/ir/view/dot_test.go: a*.dot.goldenbeside every Mermaid golden for the tree, interconnection, state, state-entry, action and filtered fixtures, each walked by an in-test DOT syntax check — balanced braces, every edge endpoint declared as a node or a cluster, every identifier quoted — so a golden is proven well-formed without shelling out todot; the wrong-form errors forsequenceandtable; quoting of names holding"and\; nested clusters and tree containment; every direction and the empty one; everyEdgeKind; the state shapes and labels; the empty rendering and its notices. The geometry haslayout.dot.goldenbeside the Mermaid and text goldens of the same fixture, the flipped axis with and without a canvas height, the centring and label-fitted size of unsized nodes, sized and pseudo-state nodes, thebband pinned anchor of a stated, a member-fitted and a corner-only cluster, a route's spline and the one-waypoint notice, the zero-extent canvas, and the header's engine for none, some and all of the nodes positioned and all edges routed; the syntax check parses everyposandbbit meets, and reads an HTML-like label as one string whose tags balance and whose entities are known.internal/ir/view/dot_style_test.go,palette_test.go: the B&W defaults; a definition square and a usage rounded; the pseudo-state rules named and unnamed, placed and not; the package, element and region cluster widths; the connection'spenwidth=3; the family of every kind and the stability of the family order; the contrast ratio of every palette colour at both tints; the sequential sampling; the unknown-palette error text; the Mermaid notice and the silence of the text and Markdown forms; labels holding&,<,>,",'and newlines; and theinterconnection.okabe-ito,state.okabe-itoandtree.viridisgoldens.internal/ir/view/plantuml_test.go: a*.plantuml.goldenbeside every Mermaid golden — the tree, interconnection, state, state-entry, action, typed-action, typed-state, filtered, layout and everysequence-*fixture — andinterconnection.okabe-ito.plantuml.goldenbeside the DOT one, each walked by an in-test PlantUML syntax check:@startuml/@endumlbracketing, a closed<style>block, balanced braces, every quoted label closed, every alias an arrow names declared (or[*]); the wrong-form errors fortable,textualandgeometry; the unknown palette refused before any output; every direction, the reversed ones noticed; the escaping of",\,<,>,~, doubled creole runs and newlines; the geometry comments and their notice; the sequence's participants and messages one for one with Mermaid's; and the palette parity test asserting the same fill hex per node as the DOT form over every golden model and every palette. WhenOPENSYSML_PLANTUML_JARnames a PlantUML jar andjavais on thePATH, every golden is additionally passed through-checkonly; the check is silent without them and nothing ingo testdepends on the jar.internal/ir/view/label_test.go,render_test.go: the label lines of a typed usage, an untyped usage, a definition, an anonymous node and a node with notes; the text form's keyword-leading line; the same<br>-joined label in the flowchart, state and sequence Mermaid grammars; the escaping of<,>,"and#in a Mermaid label.cmd/sysml/render_test.go,internal/frontend/repl/view_render_test.go,internal/frontend/lsp/render_test.go: each form on each surface — DOT refused for a table or sequence, PlantUML for a table and written for a sequence;-render-allwriting.dotand.puml; the palette accepted on both, noted by Mermaid, and refused by name with the palettes there are.internal/ir/docplan,docir,docrender: theDiagramblock'spaletteaccepted, refused when unknown (invalid-palette) or stated on a kind with no DOT or PlantUML form (unsupported-palette), carried into the document IR and onto the DOT and HTML figures.internal/doc/docrender,docpdf,cmd/sysml,internal/frontend/repl,internal/frontend/lsp: the render-time diagram form defaulting to Mermaid, written as adotorplantumlfence and a<pre class="dot">or<pre class="plantuml">for every graph-shaped block with tables left as tables, refused for an unknown form and for a kind with no DOT form.internal/doc/docpdf/diagrams_test.go,cmd/sysml/render_document_pdf_test.go: with fake tools, a DOT block drawn by thedotthatOPENSYSML_DOTnames and a PlantUML block byjava -jar <jar> -tsvg -pipefed on stdin; the// layout:header choosingdot,neato,neato -nandneato -n2; the block kept as source under a notice namingOPENSYSML_DOT,OPENSYSML_PLANTUML_JARorOPENSYSML_JAVAwhen the tool is absent; a failing tool or one that writes no SVG the typedtool-failederror carrying its stderr; Mermaid, DOT and PlantUML blocks of one document drawn in source order, Mermaid still required.internal/doc/docpdf/integration_test.godraws through the pinned Graphviz and PlantUML thatscripts/download-doc-pdf-toolchain.shprovisions — an ordinary graph, aneato -nlayout whose nodes stay where the model put them, a malformed PlantUML refused withSyntax Error— and CI'spdf-toolchainjob runs it withOPENSYSML_REQUIRE_PDF_TOOLCHAIN=1, so a missing tool there fails instead of skipping.editors/vscode/src/export.test.ts,internal/frontend/lsp/render_test.go: the export picker offering the server's forms, the pick sent asform, the artifact saved under.dot/.puml/.mmd/.md/.txtwith the matching filter, nothing sent or written when the pick or the save dialog is dismissed; the server advertisingopenSysmlRenderFormsand answering each form it lists.
- A
Routeis written as the polyline through its waypoints; the writer does not smooth it into a curve, and Graphviz draws it as given. - The PDF backend draws a DOT or PlantUML diagram only when the tool is installed: Graphviz and
the PlantUML jar are optional, so without them the source stays readable under a notice
naming the variable to set, where a missing
mmdcis an error. The figure is embedded as SVG; Graphviz's own-Tpdfoutput is not embedded by WeasyPrint. - A
sequencerendering has no DOT form. DOT has no sequence-diagram vocabulary; the MermaidsequenceDiagramand PlantUML sequence forms are its machine-readable ones. - The PlantUML form cannot pin a position or a route: DiagramLayout geometry is written as
comments and a notice counts it;
dotis the form that honours it. - PlantUML draws no reversed direction:
BTandRLread asTBandLR, and a notice says so. - An action rendering is a PlantUML state diagram, not an activity diagram, for the reason the
PlantUML section gives; a
junctionormergeis drawn as PlantUML's<<choice>>diamond, PlantUML having no round junction. - A port is a nested rectangle inside its owner in the PlantUML interconnection, not a boundary
portin/portout. - PlantUML prints no stereotype:
hide stereotypeis written so the label's keyword line is the one guillemet line; the stereotypes drive only the style and the pseudostate shapes. - No
[[url]]hyperlinks are written by the PlantUML form, no writer having a stable URL for a node'sOrigin. - Producing PlantUML runs no jar. The goldens are checked by the in-test syntax walk; a jar on
the machine is used by hand, or by the optional
-checkonlycheck thatOPENSYSML_PLANTUML_JARturns on. - Node shapes are not yet specialised for action control nodes (fork, join, decision): those take the default box with their kind in the label.
- Graphviz has no corner radius, shadow or text wrapping, so the skin's
UsageRoundCorner 20,Shadowing 0andwrapWidth 300are approximated or dropped as the style section records. - Binding connectors are not drawn at the Pilot's thickness 5: the interconnection rendering has no edge kind for them.
- A palette fills nodes by keyword family only; colouring by a data attribute or query result, and Mermaid theming, are not built.
- Producing DOT still runs no Graphviz binary. The goldens are checked by the in-test syntax walk; a Graphviz installation is used only by hand to look at them.