Skip to content

feat(view): fit a stated box's label to it, draw symbol kinds as their notation, and leave migration bookkeeping undrawn - #560

Merged
HuiJun merged 19 commits into
developfrom
feature/positioned-label-fitting
Sep 24, 2026
Merged

HuiJun merged 19 commits into
developfrom
feature/positioned-label-fitting

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

What and why

A SysML v1 migration with -layout reproduces Cameo's box geometry exactly (pos, width/height, fixedsize=true), but the DOT writer put its own label — name : Type at 14 pt bold, a «kind» line, the detail lines, unwrapped — into boxes Cameo had sized for the name alone at about 11 px. Over the TMT model that overflowed 2,012 of 2,927 stated boxes in width and 1,631 in height, control nodes and ports (24×12 diamonds, 120×6 bars, 15×15 squares) carried labels they could never hold, nested members repeated their owner's qualified path, and tree views over packages of migrated views filled with metadata/x/y/width/height/asTreeDiagram nodes. Four changes, one PR since they all reshape the same labels; the stated geometry is untouched throughout.

1. The label is fitted to a stated box, never the box to the label (dotFittedLabel, dotFitHead, dotWrap in internal/ir/view/dot.go). When Geometry.HasSize, the head is word-wrapped at the box width and drawn at the largest whole point size from 14 down to 8 at which the wrapped lines stack inside the height, preferring a size at which every word stays whole and breaking a word only when no size keeps it whole; the keyword line (at 10/14 of the head size) and each detail line follow only while height remains; a head that does not fit even at 8 pt is cut to the lines that fit and ellipsized. The estimator is the one the unsized boxes are already fitted with (dotGlyphEm, dotBoldGlyphEm, dotLineEm) — nothing Cameo-specific. Nodes without a stated size are unchanged. A stated box that holds other stated boxes — a part drawn round its members, a definition over its compartment rows — sets its title in the strip above the topmost of them and fits it to that strip (headroom, labelloc=t), so the title reads as the frame's header instead of being covered by the members, which keep their stated geometry; a box the Layout only places is sized to its own label and does not move a title.

2. Symbol kinds in a stated box are drawn as their notation, with no inner text (dotSymbolAttributes). decision/merge/choice → shape=diamond; fork/join → the filled bar; initial/junction → filled circle; final/terminate → filled doublecircle; ports → the small square. The name goes beside the symbol as xlabel, and is left out when it is one the model did not give.

How the renderer knows a name was made up. The migrator keeps that fact in memory only, so it now writes it into the model: a new bundled library MigrationMetadata (internal/workspace/libs/stdlib/OpenSysML Libraries/MigrationMetadata.sysml) declares metadata def SynthesizedName, and the migrator emits, once per body that contains any, metadata MigrationMetadata::SynthesizedName about decide2, fork, 'start to call'; ($::MigrationMetadata::… when the model shadows the package; targets quoted by the ordinary name-writing rule). semantics.Model.NameSynthesized reads it by resolved type, not by name text, and combines direct and about annotations — with a declaration-keyed fallback so a symbol re-indexed after an edit still finds its annotations (semantics/annotations.go). The view IR carries it as Node.NameSynthesized (also the language's own start/done, which no body named) and the LSP's render payload exposes it. Edges: edgeLabel no longer promotes a synthesized edge name to a label, while a trigger, guard, effect, flow payload or binding text is kept. Nothing pattern-matches names like decide2 or fork.

3. A member drawn under its owner is headed by its name below that owner (labeller in label.go). 'K-Mirror Offset'::'interpolation Error' : 'Interpolation Error' inside the 'K-Mirror Offset' box reads 'interpolation Error' : 'Interpolation Error', at every nesting depth and for an exposed element whose owner is drawn in the same rendering, typed owners and qualified type references included. A node carries the qualified names its typings resolve to (Node.Typings, from Model.DeclaredTypes), so a member drawn inside a usage whose written type differs from the type's canonical name ('summit Installation' : 'Summit Installation' holding computer : Control) is still headed below it. Rendering-time only, in the graphical forms' heads; text, JSON, Mermaid text and the LSP keep the qualified name.

4. A view's layout annotations and render members are not diagram content (tree.go, the member walk every rendering kind shares). DiagramLayout::Canvas/Layout/Route annotations — wherever owned, recognised by resolved type (semantics/layout.go) — and the render members a view holds are left out of the walk. Every other metadata usage, and a rendering usage outside a view, is drawn as before (bookkeeping_test.go pins both).

Also fixed on the way, because the marker exposed it: lowering a state … parallel body refused a metadata member as unsupported content, so the migrated TCS state machine (whose parallel state now carries the marker) rendered empty. parallelOwnedMember in internal/ir/lower/state_graph.go now treats a metadata usage as the state's own member, like an attribute or a port, so only the substates are regions.

Pinned expectations that changed, and why

  • internal/ir/view/dot_test.go — TestDOTWritesTheGeometry: pump : Pump in a stated 100×50 box is now pump<br/>: Pump (eleven bold 14 pt glyphs overrun 100 pt, so the head wraps; the box is unchanged). TestDOTLabelShape: a name-less typed node reads : Pump with its keyword instead of the bare kind word; new rows for a synthesized name (: doTracking + keyword) and a synthesized untyped name (kind word). The pinned pseudo-state done in a stated 10×10 box is now the double ring with xlabel="done" rather than a label it could not hold. xlabel joined the attributes whose values must be quoted.
  • internal/ir/view/plantuml_test.go — an action's start/done are the language's names, not the body's, so they head as their kind (**initial**, **final**) in PlantUML as they now do in DOT.
  • tests/migrate/layout_test.go — TestGoldenControlNodeLayout: the fork in a stated 120×6 box is the bare bar, the decision a diamond with xlabel="check" (v1-given), the initial/final dots and rings, and the migrator-named 'start to fork' edge carries no label; the test also asserts those made-up names are absent. TestMigratedRoutesRenderPinned: 'start to gain', 'engine to wheel', 'engine to wheel 2' and 'mass = limit' were migrator-made names — the edges now read as their kind (connection, binding) or nothing, while finish, drive, result to value and accept Resume (source-given or real edge text) are unchanged.
  • internal/translate/migrate/diagrams_internal_test.go, writer_test.go, tests/migrate/testdata/xmi/*.golden.sysml — each body that names something the source left unnamed gains one metadata MigrationMetadata::SynthesizedName about …; line. That is the only change to emitted text.
  • README.md, docs/internals/testing.md, docs/reference/grammar/README.md, examples/self-model/*.sysml — the bundled library count moved from 104 to 105 files (make docs-counts).
  • docs/project/pilot-differential-baseline.json — examples digest re-recorded (go run -C tools ./cmd/pilot-diff -update); only the digest and date moved.

Specification basis

SysML v2 §7.24 Views and Viewpoints, §10.2: the rendering is tool-defined; render members and the annotations that place a view are about the picture, not the exposed set. Metadata usages on a parallel state are ordinary owned members (SysML v2 §7.20 State-Based Behavior: a parallel state's regions are its nested states). The compliance rows for the DOT form in docs/project/spec-compliance.md are extended; no row moves status.

How it was verified

Unit tests: internal/ir/view/dot_fit_test.go (fit at 14 pt, shrink to 8 pt, keyword/detail only while height remains, a compartment row's one line, a word broken only when no size keeps it whole, the ellipsis at the floor, each symbol kind's shape and xlabel, an unsized node unchanged), label_test.go (owner-relative heads at every depth, typed owners, qualified types, the text form untouched), bookkeeping_test.go (layout annotations and render members left out; user metadata still drawn; NameSynthesized read semantically from the marker), internal/ir/lower/state_graph_nested_test.go (a metadata usage in a parallel body is not a region), internal/translate/migrate/writer_test.go (one marker per body, attached to the right body when bodies nest, quoting), internal/syntax/source/name_test.go, tests/migrate/layout_test.go.

Gates, all green: gofmt -l . (empty), go vet ./..., go build ./..., make test and make lint, go test ./... with OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_PILOT_LIBRARY_XMI=1 OPENSYSML_REQUIRE_PSSM_SUITE=1 after the four download scripts (training corpus, pilot corpora, pilot library XMI, PSSM suite, RDF round-trip included), python3 scripts/changelog.py check, scripts/check-doc-ids.py, scripts/check-doc-links.py, scripts/check-doc-figures.py, go run -C tools ./cmd/doc-counts -check, go test -C tools ./referee/diff.

TMT before/after

Same source and MTIP export, sysml TMT-2024x.mdzip -convert sysml -layout TMT_mtip.xml, then -render-all … -render-form dot (2,641 DOT views on both branches, identical render diagnostics, the same 1,063 views empty on both) and every DOT rendered with the engine its // layout: header names.

develop this branch
Graphviz size too small for label warnings 6,088 67
fixed-size nodes 7,230 7,230
whose label fits the stated box (same glyph estimate) 926 (13 %) 6,932 (96 %), of which 1,875 are symbols with no inner text
heads ellipsized at the 8 pt floor — 298
stated boxes headed above the stated boxes they hold — 427, in 105 views
other Graphviz warnings/errors (timeouts, crashes on huge views) 35 9

The 67 remaining warnings are the estimator's: 0.66 em per bold glyph is low for all-capital names in Helvetica bold (Command as a 13 pt port def head measures 73 px in a 64 px box; DETECTOR_EXPOSURE(CONTINUOUS)), and 15 constraint parameters drawn as 15×15 pins whose owner is not in the view (D…). Widening the estimate would move the unsized boxes too, so it is left alone. The 298 ellipsized heads are mostly 15×15 pins and compartment rows Cameo also clipped (analysisDriver : 'Analysis… in a 155×14 row).

The migration report and the results sidecar are byte-identical to develop's. The emitted .sysml differs only by the metadata MigrationMetadata::SynthesizedName about …; lines (1,757 in TMT, now that anonymous parameters, generated includes, stakeholders and instance slots are marked too), one per body that names something the source did not.

Shear plate SendAck activity — before / after:

SendAck before
SendAck after

'K-Mirror Offset Error Budget - Details' tree — before / after (the two blobs of layout metadata are gone, members read below their owner, a definition's title sits above its compartment rows; the members the Cameo diagram did not draw have no Layout, so neato still places them where it likes):

K-Mirror tree before
K-Mirror tree after

TCS state machine — after:

TCS after

'APS Physical' IBD — after (each part's title in the strip above its members, ports as squares with their names beside them, part labels fitted):

APS Physical IBD after

Checklist

  • make test and make lint pass locally
  • Tests added or updated for the change
  • Documentation extended where it already covers the surface (see CONTRIBUTING.md)
  • Changelog entry added as changes/unreleased/<slug>.<section>.md, not as an edit to CHANGELOG.md
  • baselines regenerated and make docs-counts run if a gate count moved (compliance rows need nothing: the census is counted at docs build)
  • No internal work-item labels (waves, slices, F4, K5) in the body, docs, or changelog

devin-ai-integration Bot and others added 12 commits September 24, 2026 01:45
A view's DiagramLayout annotations and its render members describe the
picture, not the model; the shared member walk now skips them so every
graphical form omits them, while ordinary metadata usages stay shown.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
… owner

Co-Authored-By: jason.han <hanhuijun@gmail.com>
…heir notation

A node a Layout sizes keeps its box: the head word-wraps at the width and
shrinks from 14pt to 8pt until it fits the height, the keyword and detail
lines follow only while height remains, and a head too long at 8pt is cut
and ellipsized. Decision, merge and choice nodes in stated boxes are drawn
as diamonds, forks and joins as filled bars, initial nodes as the filled
dot, final nodes and terminate actions as the double ring, and ports as
their square, with no text inside; a name is set beside the symbol unless
the node marks it as synthesized. A terminate action usage is kinded as
one, so the notation's final node is recognised.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
… and the bookkeeping left undrawn

Co-Authored-By: jason.han <hanhuijun@gmail.com>
…he dot or ring

Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…nclosing box before the boxes in it

Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ments unnamed

A bundled MigrationMetadata::SynthesizedName metadata def, written once per
body as an about list over the names the migration made up; the semantic
model reads it (Model.NameSynthesized), the view IR carries it on nodes, and
the graphical writers draw such an element as its source did: a bare control
symbol, an edge without a label, a typed node as ': Type'. An action's
language-given start and done are treated the same way. Re-indexed twins of
one declaration share the about annotations the declaration gathered.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
…baseline

Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ing one

Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

…me a member below its resolved owner

A stated box that encloses other stated boxes fits its title to the strip
above the topmost of them and sets it there (labelloc=t), so a container's
title is not covered by its members, which keep their stated geometry. A
box the Layout only places is sized to its label and does not move the title.

A node now carries the qualified names its typings resolve to (Node.Typings,
from Model.DeclaredTypes), so a member drawn inside a usage whose written
type differs from the type's canonical name is still headed below it.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
@devin-ai-integration
devin-ai-integration Bot marked this pull request as ready for review September 24, 2026 05:35
devin-ai-integration[bot]

This comment was marked as resolved.

…et them in a strip below it on request

A DOT drawing that positions some nodes left the others to neato, which set
them over the placed boxes. They are now omitted with the edges at them,
under a notice, so a migrated diagram shows what its source showed. The new
-render-unplaced strip (Options.Unplaced) packs them in rows below the canvas
or the positioned extent instead, on -render, -render-all, -render-document
and -render-documents alike. Every drawn node is pinned, so the layout header
never names plain neato.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration Bot and others added 3 commits September 24, 2026 06:48
…-label-fitting

The bundled library snapshot is regenerated with both AnalysisRecords and MigrationMetadata, the migration goldens the merge brought in gain their SynthesizedName markers, the self-model counts the twelfth library extension, and the pilot differential baseline re-records the examples digest; every verdict in it is unchanged.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…abel

Graphviz pads a node's label by its default margin of 0.11 by 0.055 in, so
a head fitted to the full stated width could still be drawn past the box's
border. A stated box that carries a label now sets margin=0, so the fit and
the drawing agree; a head filling the width keeps its size, one a point
over it shrinks.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…rk every synthesized declaration name

A port with no given name but a type drew nothing beside its square; the type is
now set as its xlabel like a given name. Anonymous parameters, port payloads,
generated subjects, stakeholders, includes and instance slots are declared under
a synthesized name that the SynthesizedName marker did not cover; they are now
marked, and an empty name is never marked.

Co-Authored-By: jason.han <hanhuijun@gmail.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment thread internal/ir/view/label.go
@HuiJun
HuiJun merged commit a5072ad into develop Sep 24, 2026
15 checks passed
@HuiJun
HuiJun deleted the feature/positioned-label-fitting branch September 24, 2026 13:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant