From 93cf9f87ef7753367821b5a6d0059fe76c5e62f4 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 6 Jul 2026 23:54:12 -0400 Subject: [PATCH 1/3] Support selecting rendering style via render ; Wire render asTreeDiagram; and render asInterconnectionDiagram; to their corresponding layout strategies in DiagramTypeRouter, taking precedence over the existing name/supertype heuristic. asElementTable and asTextualNotation remain deliberately unhandled (no diagnostic, no behavior change) since no corresponding layout strategy exists yet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 15 ++- ROADMAP.md | 23 ---- docs/design/sysml2-tools-core/rendering.md | 30 +++-- .../sysml2-tools-core/rendering/internal.md | 8 +- .../rendering/internal/diagram-type-router.md | 19 ++- .../internal/diagram-type-router.yaml | 32 +++++ docs/user_guide/introduction.md | 31 +++-- .../rendering/internal/diagram-type-router.md | 12 ++ .../Rendering/Internal/DiagramTypeRouter.cs | 23 +++- .../Rendering/DiagramTypeRouterTests.cs | 109 ++++++++++++++++++ 10 files changed, 243 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 686f836d..5cc8b6a3 100644 --- a/README.md +++ b/README.md @@ -225,12 +225,15 @@ section above). | Multiple views, `--view ` | Render only the named view | | `--view ` names a view that does not exist | Error: lists available view names and exits non-zero | -For the General View diagram strategy (the diagram produced when no more specialized view kind -applies), a view's body `expose <...>;` statements now scope the rendered diagram to the union of -the exposed names' containment subtrees, instead of always rendering the full workspace; a view -with no `expose` statement continues to render the full workspace. `render ;` names a -rendering style (not yet honored) and has no effect on scope. This scoping is not yet extended to -the other layout strategies — see the *Introduction* user guide for details. +For every layout strategy (not just the General View), a view's body `expose <...>;` statements +now scope the rendered diagram to the union of the exposed names' containment subtrees, instead +of always rendering the full workspace; a view with no `expose` statement continues to render +the full workspace. `render asTreeDiagram;` and `render asInterconnectionDiagram;` now select +the Browser View and Interconnection View layout strategies respectively, taking precedence over +the name/supertype heuristic; other `render` targets (`asElementTable`, `asTextualNotation`, an +unrecognized name, or none) leave strategy selection unchanged. `render` never affects content +scope — `expose` remains the sole scoping mechanism regardless of which strategy is selected — +see the *Introduction* user guide for details. ## NuGet Packages diff --git a/ROADMAP.md b/ROADMAP.md index b985512c..d57dbe02 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -95,29 +95,6 @@ new expression-evaluation component; `GeneralViewLayoutStrategy` filter applicat **Visual gate:** a view with a `filter [];` statement renders only the elements satisfying the predicate, with no "not yet evaluated" warning. -### Support selecting rendering style via `render ;` - -A view's `render ;` member names a rendering style/format usage per the SysML v2 -grammar (e.g. `asTreeDiagram`, `asElementTable`, `asTextualNotation`, `asTextualNotationTable` — -`rendering` usages, a distinct usage/definition kind) — it is captured verbatim on -`SysmlViewNode.RenderTargetName` but currently has no effect on rendering; every view renders -through the single `DiagramTypeRouter` → `ILayoutStrategy` selection regardless of its declared -`render` member. This is a distinct future capability from content scoping, which is `expose`'s -exclusive role. - -- Design a mapping from recognized rendering-style names (`asTreeDiagram`, `asElementTable`, - and so on, once corresponding layout/rendering strategies exist) to an `ILayoutStrategy`/ - renderer selection, honored when a view declares a `render` member naming one. -- Leave `RenderTargetName` un-honored (as today) for rendering-style names with no corresponding - strategy, with no diagnostic — an unrecognized rendering-style name is not an error, since - `render` selects presentation, not content. - -**Scope:** `DiagramTypeRouter`/`RenderCommand` (rendering-style selection); new layout/rendering -strategies for tree-diagram/element-table/textual-notation styles, if not already covered by an -existing strategy. -**Visual gate:** a view declaring `render asTreeDiagram;` (once a tree-diagram strategy exists) -renders using that style instead of the default `GeneralView` layout. - --- ## Release & packaging diff --git a/docs/design/sysml2-tools-core/rendering.md b/docs/design/sysml2-tools-core/rendering.md index 3787a054..aeeacba8 100644 --- a/docs/design/sysml2-tools-core/rendering.md +++ b/docs/design/sysml2-tools-core/rendering.md @@ -56,11 +56,14 @@ flowchart TD - *Contract*: `string ViewName`, `SysmlWorkspace Workspace`, `SysmlViewNode? ViewNode = null`. `ViewNode` is the view's resolved AST node, giving a layout strategy access to the view's declared `render`/`expose`/`filter` body statements. Of these, only `ExposedNames` (and its - resolved `Expose` edges) drives content scoping; `RenderTargetName` names a rendering - style/format per the SysML v2 grammar and is captured as inert metadata only (it never - affects scope or resolution); `FilterExpressionText` is captured as raw text and not yet - evaluated. `ViewNode` is `null` for the `--auto` synthesized view, which carries no AST node - of its own. + resolved `Expose` edges) drives content scoping — `RenderTargetName` never affects which + elements are included in the diagram. `RenderTargetName` does, however, drive **strategy + selection**: `DiagramTypeRouter` recognizes `asTreeDiagram` and `asInterconnectionDiagram` and + routes to the browser and interconnection strategies respectively ahead of the name/supertype + heuristic; any other value (including `null`, `asElementTable`, or `asTextualNotation`) remains + inert, with no corresponding strategy selection or diagnostic. `FilterExpressionText` is + captured as raw text and not yet evaluated. `ViewNode` is `null` for the `--auto` synthesized + view, which carries no AST node of its own. **Theme**: Visual configuration record. @@ -111,12 +114,17 @@ flowchart TD - *Type*: Internal static class. - *Role*: Router. - *Contract*: `static ILayoutStrategy GetStrategy(object viewNode, SysmlWorkspace workspace, out string? unsupportedMessage)`. - Inspects the view node's name and declared supertype names (case-insensitively) for a recognized - view-kind marker, in priority order — Interconnection, StateTransition/State, ActionFlow/Action, - Grid/Matrix/Tabular, Browser/Tree, then Sequence — and returns the matching concrete - `ILayoutStrategy`, falling back to `GeneralViewLayoutStrategy` for unrecognized views or non-view - nodes. The `unsupportedMessage` out-parameter is reserved for future unsupported view kinds and is - currently always null because every view resolves to a strategy. + First checks the view's declared `render` target for an exact, case-sensitive match against + `asTreeDiagram` (routes to `BrowserViewLayoutStrategy`) or `asInterconnectionDiagram` (routes to + `InterconnectionViewLayoutStrategy`), taking precedence over the heuristic below; any other + render target value (including none, `asElementTable`, or `asTextualNotation`) falls through + unchanged. Absent a recognized render target, inspects the view node's name and declared + supertype names (case-insensitively) for a recognized view-kind marker, in priority order — + Interconnection, StateTransition/State, ActionFlow/Action, Grid/Matrix/Tabular, Browser/Tree, + then Sequence — and returns the matching concrete `ILayoutStrategy`, falling back to + `GeneralViewLayoutStrategy` for unrecognized views or non-view nodes. The `unsupportedMessage` + out-parameter is reserved for future unsupported view kinds and is currently always null + because every view resolves to a strategy. **StdlibFilter**: Standard-library element filter. diff --git a/docs/design/sysml2-tools-core/rendering/internal.md b/docs/design/sysml2-tools-core/rendering/internal.md index b64918b6..086cf7bd 100644 --- a/docs/design/sysml2-tools-core/rendering/internal.md +++ b/docs/design/sysml2-tools-core/rendering/internal.md @@ -17,8 +17,12 @@ whether an element belongs to the standard library. #### Design -`DiagramTypeRouter` inspects a view's name and declared supertype names for a recognized view -kind (interconnection, state transition, action flow, grid/matrix, browser/tree, sequence) and +`DiagramTypeRouter` first checks a view's declared `render` target for an exact match against +`asTreeDiagram` (browser/tree strategy) or `asInterconnectionDiagram` (interconnection strategy), +taking precedence over the name/supertype heuristic; other render targets (including +`asElementTable`, `asTextualNotation`, or none) have no effect. Absent a recognized render +target, it inspects a view's name and declared supertype names for a recognized view kind +(interconnection, state transition, action flow, grid/matrix, browser/tree, sequence) and returns the matching strategy, defaulting to the general view strategy. To keep diagrams focused on the user's model, the view strategies omit standard-library elements by testing each qualified name with the `StdlibFilter` helper. The `DiagramTypeRouter` unit is described in its own chapter. diff --git a/docs/design/sysml2-tools-core/rendering/internal/diagram-type-router.md b/docs/design/sysml2-tools-core/rendering/internal/diagram-type-router.md index d34f1774..5441a03f 100644 --- a/docs/design/sysml2-tools-core/rendering/internal/diagram-type-router.md +++ b/docs/design/sysml2-tools-core/rendering/internal/diagram-type-router.md @@ -16,9 +16,22 @@ diagnostic message when no strategy can be determined. ###### `GetStrategy(viewNode, workspace, out unsupportedMessage)` -Returns the strategy for the view. When the node is a view, the router tests the view's name -and its declared supertype names (case-insensitively) for a recognized view-kind marker, in a -fixed priority order: Interconnection, then StateTransition/State, then ActionFlow/Action, then +Returns the strategy for the view. Dispatch first checks the view's declared `render` target +(`SysmlViewNode.RenderTargetName`) for an exact, case-sensitive (`StringComparison.Ordinal`) +match against a recognized rendering-kind name: `asTreeDiagram` selects the browser (tree) +strategy and `asInterconnectionDiagram` selects the interconnection strategy, taking precedence +over the name/supertype heuristic below regardless of the view's own name or declared +supertypes. `asElementTable`, `asTextualNotation`, any other unrecognized rendering-kind name, +and a `null` (absent) render target are deliberately left unmapped: they have no effect and fall +through unchanged, with no diagnostic, to the name/supertype heuristic. `asElementTable` is left +unmapped because its `TabularRendering` semantics (a per-row/per-column table composition) are +fundamentally different from `GridViewLayoutStrategy`'s matrix layout, not merely a naming +variant of it; `asTextualNotation` is left unmapped because it is a non-graphical rendering +style with no corresponding `ILayoutStrategy` implementation. + +When no render target matches, the router falls back to testing the view's name and its +declared supertype names (case-insensitively) for a recognized view-kind marker, in a fixed +priority order: Interconnection, then StateTransition/State, then ActionFlow/Action, then Grid/Matrix/Tabular, then Browser/Tree, then Sequence. The first marker that matches selects the corresponding strategy. When no marker matches — or the node is not a view — the router returns the general view strategy. The fixed order resolves views that carry more than one marker diff --git a/docs/reqstream/sysml2-tools-core/rendering/internal/diagram-type-router.yaml b/docs/reqstream/sysml2-tools-core/rendering/internal/diagram-type-router.yaml index 03bf22e6..f769dcd6 100644 --- a/docs/reqstream/sysml2-tools-core/rendering/internal/diagram-type-router.yaml +++ b/docs/reqstream/sysml2-tools-core/rendering/internal/diagram-type-router.yaml @@ -36,3 +36,35 @@ sections: broadly applicable layout and is the natural fallback for an unspecified view kind. tests: - GetStrategy_PlainView_ReturnsGeneralViewStrategy + + - id: SysML2Tools-Core-Rendering-Internal-DiagramTypeRouter-RenderTargetOverride + title: >- + When a view declares a render target of asTreeDiagram or asInterconnectionDiagram, + DiagramTypeRouter shall select the corresponding browser or interconnection layout + strategy, taking precedence over the name/supertype heuristic. + justification: | + A view's declared render target is an explicit, author-specified rendering-style + selection per the SysML v2 grammar. Honoring it ahead of the name/supertype heuristic + lets an author pick a diagram type directly, without relying on naming conventions, + and resolves any conflict between the two in the author's favor. + tests: + - GetStrategy_RenderAsTreeDiagram_ReturnsBrowserStrategy + - GetStrategy_RenderAsInterconnectionDiagram_ReturnsInterconnectionStrategy + - GetStrategy_RenderTargetPrecedenceOverridesNameHeuristic + + - id: SysML2Tools-Core-Rendering-Internal-DiagramTypeRouter-RenderTargetUnrecognizedFallthrough + title: >- + When a view declares a render target other than asTreeDiagram or + asInterconnectionDiagram — including asElementTable, asTextualNotation, an unrecognized + name, or none at all — DiagramTypeRouter shall fall through unchanged to the + name/supertype heuristic, with no diagnostic. + justification: | + Rendering-style names with no corresponding layout strategy (asElementTable is a + fundamentally tabular composition distinct from the Grid View; asTextualNotation is + non-graphical) are not errors — render selects presentation, not content — so dispatch + must continue to resolve via the existing heuristic without regressing or emitting a + spurious diagnostic. + tests: + - GetStrategy_RenderAsElementTable_FallsThroughUnchanged + - GetStrategy_RenderAsTextualNotation_FallsThroughUnchanged + - GetStrategy_UnrecognizedRenderTarget_FallsThroughUnchanged diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 8fb31e56..2488e090 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -126,9 +126,13 @@ entire workspace: unresolved name, so the mistake is visible instead of silently rendering everything with no signal. - `render ;` — per the SysML v2 grammar, this names a rendering style/format (e.g. - `asTreeDiagram`, `asElementTable`) rather than content. It is captured but currently has **no - effect** on the rendered scope — see `ROADMAP.md` for the planned future capability to honor - it as a rendering-style selector. + `asTreeDiagram`, `asElementTable`). `render asTreeDiagram;` and + `render asInterconnectionDiagram;` now select the Browser View and Interconnection View layout + strategies respectively, taking precedence over the name/supertype heuristic `DiagramTypeRouter` + otherwise applies. Every other rendering-style name (`asElementTable`, `asTextualNotation`, or + an unrecognized name) — and a view declaring no `render` member at all — has **no effect** on + which strategy renders the view; see `ROADMAP.md` for further rendering-style selectors that + may be added in future. - `filter [];` — the bracketed filter expression is parsed and captured, but **not yet evaluated**: the resolved (`expose`) scope is rendered unfiltered, and a diagnostic reports that the filter expression was parsed but not yet evaluated. Full filter-expression @@ -162,7 +166,7 @@ of confusion, so it is worth stating plainly: | Statement | What it actually does | | --- | --- | | `expose ;` | The **only** mechanism scoping which model content appears in the diagram (see above). | -| `render ;` | Selects a rendering *style/format* (e.g. `asTreeDiagram`). Metadata only, no scoping. | +| `render ;` | Selects a rendering style — see "View Body Statements" above. Never scopes content. | | `filter [];` | Captured as raw text only; not yet evaluated (see ROADMAP.md's filter-evaluation entry). | ### Example A: exposing a definition to scope down to a subsystem @@ -185,10 +189,13 @@ package Vehicle { } ``` -`EngineOnlyView` renders **only** the `Engine` definition's containment subtree (`Engine` and -its `cylinder` part) — `Vehicle`, `myVehicle`, and `wheel` are excluded entirely. Removing the -`expose Engine;` statement (leaving only `render asTreeDiagram;`, or an empty view body) renders -the **full workspace** instead: `render` never narrows the scope, only `expose` does. +`EngineOnlyView` declares `render asTreeDiagram;`, so it renders via `BrowserViewLayoutStrategy` +as an indented tree of rows rather than the General View's nested boxes. It renders **only** the +`Engine` definition's containment subtree (`Engine` and its `cylinder` part) — `Vehicle`, +`myVehicle`, and `wheel` are excluded entirely. Removing the `expose Engine;` statement (leaving +only `render asTreeDiagram;`, or an empty view body) renders the **full workspace** instead: +`render` never narrows the scope, only `expose` does — `BrowserViewLayoutStrategy` honors +`expose` scoping identically to every other layout strategy. > **Note:** `expose` targets are qualified names (`::`-separated), not dotted member-access > chains. `expose myVehicle.engine;` is a **syntax error**, not merely an unresolved reference — @@ -217,9 +224,11 @@ package Vehicle { Here `expose myVehicle;` names a **usage** (`myVehicle : Vehicle`), not a `def`. The tool resolves `myVehicle`'s own `Typing` edge to find the definition it is typed by (`Vehicle`), and -scopes the diagram to the union of `myVehicle`'s and `Vehicle`'s containment subtrees. The -rendered diagram therefore includes `Vehicle` (with its `engine` and `wheel` parts) and, because -`engine` is typed by `Engine`, the `Engine` definition (with its `cylinder` part) as well. +scopes the diagram to the union of `myVehicle`'s and `Vehicle`'s containment subtrees. This view +also declares `render asTreeDiagram;`, so — like Example A — it renders via +`BrowserViewLayoutStrategy` as an indented tree of rows. The rendered tree therefore includes +`Vehicle` (with its `engine` and `wheel` parts) and, because `engine` is typed by `Engine`, the +`Engine` definition (with its `cylinder` part) as well. Contrast this with `expose Vehicle;` (exposing the **definition** directly, as in Example A): that scopes straight to `Vehicle`'s own containment subtree without needing to resolve any diff --git a/docs/verification/sysml2-tools-core/rendering/internal/diagram-type-router.md b/docs/verification/sysml2-tools-core/rendering/internal/diagram-type-router.md index 397f4129..cc42c0d8 100644 --- a/docs/verification/sysml2-tools-core/rendering/internal/diagram-type-router.md +++ b/docs/verification/sysml2-tools-core/rendering/internal/diagram-type-router.md @@ -17,6 +17,12 @@ configuration are required beyond a standard .NET SDK installation. - Each recognized view kind, whether identified by name or by a specialized supertype, routes to its corresponding strategy. - A view matching no recognized kind routes to the general view strategy. +- A view declaring `render asTreeDiagram;` or `render asInterconnectionDiagram;` routes to the + browser or interconnection strategy respectively, taking precedence over a conflicting + name/supertype heuristic match. +- A view declaring any other render target — `asElementTable`, `asTextualNotation`, an + unrecognized name, or none — falls through unchanged to the name/supertype heuristic, with no + diagnostic. ##### Test Scenarios @@ -30,3 +36,9 @@ configuration are required beyond a standard .NET SDK installation. | `GetStrategy_BrowserNamedView_ReturnsBrowserStrategy` | Browser/tree view | | `GetStrategy_SequenceNamedView_ReturnsSequenceStrategy` | Sequence view | | `GetStrategy_PlainView_ReturnsGeneralViewStrategy` | Unrecognized view falls back to general | +| `GetStrategy_RenderAsTreeDiagram_ReturnsBrowserStrategy` | `render asTreeDiagram;` selects browser strategy | +| `GetStrategy_RenderAsInterconnectionDiagram_ReturnsInterconnectionStrategy` | render picks interconnection | +| `GetStrategy_RenderTargetPrecedenceOverridesNameHeuristic` | Render target wins over a conflicting name heuristic | +| `GetStrategy_RenderAsElementTable_FallsThroughUnchanged` | `render asElementTable;` has no effect | +| `GetStrategy_RenderAsTextualNotation_FallsThroughUnchanged` | `render asTextualNotation;` has no effect | +| `GetStrategy_UnrecognizedRenderTarget_FallsThroughUnchanged` | Unrecognized render target has no effect | diff --git a/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DiagramTypeRouter.cs b/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DiagramTypeRouter.cs index aa04d8b5..4f458e55 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DiagramTypeRouter.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DiagramTypeRouter.cs @@ -13,9 +13,17 @@ namespace DemaConsulting.SysML2Tools.Rendering.Internal; /// based on the view type. /// /// -/// Dispatch inspects the view's declared supertype names (and its own name) for a recognized view -/// kind. A view that specializes a name containing Interconnection routes to the -/// interconnection strategy; everything else falls back to the general view strategy. +/// Dispatch first checks the view's declared render target () for an exact, case-sensitive () match against a recognized rendering-kind name: +/// asTreeDiagram routes to the browser (tree) strategy and asInterconnectionDiagram +/// routes to the interconnection strategy, regardless of the view's name or supertypes. Any other +/// value — including , asElementTable, asTextualNotation, or +/// an unrecognized name — has no effect and falls through unchanged, with no diagnostic, to the +/// existing name/supertype heuristic: the view's declared supertype names (and its own name) are +/// inspected for a recognized view kind. A view that specializes a name containing +/// Interconnection routes to the interconnection strategy; everything else falls back to +/// the general view strategy. /// internal static class DiagramTypeRouter { @@ -42,6 +50,15 @@ public static ILayoutStrategy GetStrategy( if (viewNode is SysmlViewNode view) { + switch (view.RenderTargetName) + { + case "asTreeDiagram": + return new BrowserViewLayoutStrategy(); + + case "asInterconnectionDiagram": + return new InterconnectionViewLayoutStrategy(); + } + if (Matches(view, "Interconnection")) { return new InterconnectionViewLayoutStrategy(); diff --git a/test/DemaConsulting.SysML2Tools.Tests/Rendering/DiagramTypeRouterTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Rendering/DiagramTypeRouterTests.cs index fa3634d9..ebce64f6 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Rendering/DiagramTypeRouterTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Rendering/DiagramTypeRouterTests.cs @@ -116,4 +116,113 @@ public void GetStrategy_SequenceNamedView_ReturnsSequenceStrategy() Assert.IsType(strategy); } + + /// A view declaring render asTreeDiagram; routes to the browser strategy. + [Fact] + public void GetStrategy_RenderAsTreeDiagram_ReturnsBrowserStrategy() + { + var view = new SysmlViewNode + { + Name = "MyView", + QualifiedName = "M::MyView", + RenderTargetName = "asTreeDiagram" + }; + var workspace = new SysmlWorkspace(); + + var strategy = DiagramTypeRouter.GetStrategy(view, workspace, out var unsupported); + + Assert.Null(unsupported); + Assert.IsType(strategy); + } + + /// A view declaring render asInterconnectionDiagram; routes to the interconnection strategy. + [Fact] + public void GetStrategy_RenderAsInterconnectionDiagram_ReturnsInterconnectionStrategy() + { + var view = new SysmlViewNode + { + Name = "MyView", + QualifiedName = "M::MyView", + RenderTargetName = "asInterconnectionDiagram" + }; + var workspace = new SysmlWorkspace(); + + var strategy = DiagramTypeRouter.GetStrategy(view, workspace, out var unsupported); + + Assert.Null(unsupported); + Assert.IsType(strategy); + } + + /// + /// A declared render target takes precedence over a conflicting name/supertype heuristic match. + /// + [Fact] + public void GetStrategy_RenderTargetPrecedenceOverridesNameHeuristic() + { + var view = new SysmlViewNode + { + Name = "TrafficStateTransitionView", + QualifiedName = "M::TrafficStateTransitionView", + RenderTargetName = "asTreeDiagram" + }; + var workspace = new SysmlWorkspace(); + + var strategy = DiagramTypeRouter.GetStrategy(view, workspace, out _); + + Assert.IsType(strategy); + } + + /// A view declaring render asElementTable; falls through unchanged to the existing heuristic. + [Fact] + public void GetStrategy_RenderAsElementTable_FallsThroughUnchanged() + { + var view = new SysmlViewNode + { + Name = "GeneralView", + QualifiedName = "M::GeneralView", + RenderTargetName = "asElementTable" + }; + var workspace = new SysmlWorkspace(); + + var strategy = DiagramTypeRouter.GetStrategy(view, workspace, out var unsupported); + + Assert.Null(unsupported); + Assert.IsType(strategy); + } + + /// A view declaring render asTextualNotation; falls through unchanged to the existing heuristic. + [Fact] + public void GetStrategy_RenderAsTextualNotation_FallsThroughUnchanged() + { + var view = new SysmlViewNode + { + Name = "GeneralView", + QualifiedName = "M::GeneralView", + RenderTargetName = "asTextualNotation" + }; + var workspace = new SysmlWorkspace(); + + var strategy = DiagramTypeRouter.GetStrategy(view, workspace, out var unsupported); + + Assert.Null(unsupported); + Assert.IsType(strategy); + } + + /// An unrecognized render target falls through unchanged to the existing heuristic. + [Fact] + public void GetStrategy_UnrecognizedRenderTarget_FallsThroughUnchanged() + { + var view = new SysmlViewNode + { + Name = "GeneralView", + QualifiedName = "M::GeneralView", + RenderTargetName = "asSomethingUnrecognized" + }; + var workspace = new SysmlWorkspace(); + + var strategy = DiagramTypeRouter.GetStrategy(view, workspace, out var unsupported); + + Assert.Null(unsupported); + Assert.IsType(strategy); + } } From 261c6f3f07e024e35083e0824f6f1a60babb7464 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Tue, 7 Jul 2026 00:13:55 -0400 Subject: [PATCH 2/3] Fix review findings: complete render-target docs and add exact-match test coverage - docs/design/introduction.md: mention render-target precedence in the DiagramTypeRouter quick-reference bullet - docs/verification/.../rendering/internal.md: add subsystem-level scenarios for render-target override and fallthrough - DiagramTypeRouter.cs remarks: enumerate all name-heuristic markers, not just Interconnection - DiagramTypeRouterTests.cs: add negative tests proving RenderTargetName matching is exact/case-sensitive (wrong case and trailing whitespace do not match and fall through unchanged) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/introduction.md | 5 ++- .../sysml2-tools-core/rendering/internal.md | 9 ++++ .../Rendering/Internal/DiagramTypeRouter.cs | 6 ++- .../Rendering/DiagramTypeRouterTests.cs | 44 +++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/docs/design/introduction.md b/docs/design/introduction.md index 012044e2..23ead676 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -265,7 +265,10 @@ renderer packages have no concept of workspaces or view iteration. stdlib-resolved qualified name of a view's viewpoint type (e.g., `SystemsModelingLibrary::Views::GeneralView`), not the raw token. User aliases and local imports therefore do not break dispatch. The router walks the supertype chain -to handle custom viewpoints that specialize stdlib viewpoints. +to handle custom viewpoints that specialize stdlib viewpoints. Dispatch first checks +the view's declared `render` target for an exact match against `asTreeDiagram` or +`asInterconnectionDiagram`, which takes precedence over the qualified-name/supertype +heuristic entirely. **Diagnostic model mirrors ReviewMark.** `SysmlDiagnostic` mirrors ReviewMark's `LintIssue` in structure and philosophy: file/line/col location, severity enum, diff --git a/docs/verification/sysml2-tools-core/rendering/internal.md b/docs/verification/sysml2-tools-core/rendering/internal.md index 9afbc02b..4d42fb92 100644 --- a/docs/verification/sysml2-tools-core/rendering/internal.md +++ b/docs/verification/sysml2-tools-core/rendering/internal.md @@ -18,6 +18,12 @@ or configuration are required beyond a standard .NET SDK installation. - All `DiagramTypeRouterTests` pass with zero failures across all three target frameworks. - Each recognized view kind routes to its corresponding strategy. - A view matching no recognized kind routes to the general view strategy. +- A view declaring `render asTreeDiagram;` or `render asInterconnectionDiagram;` routes to the + browser or interconnection strategy regardless of the view's name, taking precedence over the + name/supertype heuristic. +- A view declaring `render asElementTable;`, `render asTextualNotation;`, or any other + unrecognized (or absent) render target falls through unchanged to the existing + name-heuristic/general-view behavior, with no diagnostic. - Standard-library elements do not appear in the produced layout. #### Test Scenarios @@ -27,4 +33,7 @@ or configuration are required beyond a standard .NET SDK installation. | Interconnection-named or specializing view | Routes to the interconnection strategy | | State transition / action flow / matrix / browser / sequence views | Route to their strategies | | Plain view | Routes to the general view strategy | +| `render asTreeDiagram;` view | Routes to the browser (tree) strategy regardless of name | +| `render asInterconnectionDiagram;` view | Routes to the interconnection strategy regardless of name | +| `asElementTable;` / `asTextualNotation;` / other render target | Falls through unchanged, no diagnostic | | Standard-library-only workspace | Produces a minimal canvas (stdlib excluded) | diff --git a/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DiagramTypeRouter.cs b/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DiagramTypeRouter.cs index 4f458e55..76b5565f 100644 --- a/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DiagramTypeRouter.cs +++ b/src/DemaConsulting.SysML2Tools.Core/Rendering/Internal/DiagramTypeRouter.cs @@ -22,8 +22,10 @@ namespace DemaConsulting.SysML2Tools.Rendering.Internal; /// an unrecognized name — has no effect and falls through unchanged, with no diagnostic, to the /// existing name/supertype heuristic: the view's declared supertype names (and its own name) are /// inspected for a recognized view kind. A view that specializes a name containing -/// Interconnection routes to the interconnection strategy; everything else falls back to -/// the general view strategy. +/// Interconnection routes to the interconnection strategy; StateTransition/State, +/// ActionFlow/Action, Grid/Matrix/Tabular, +/// Browser/Tree, and Sequence route to their corresponding strategies; +/// everything else falls back to the general view strategy. /// internal static class DiagramTypeRouter { diff --git a/test/DemaConsulting.SysML2Tools.Tests/Rendering/DiagramTypeRouterTests.cs b/test/DemaConsulting.SysML2Tools.Tests/Rendering/DiagramTypeRouterTests.cs index ebce64f6..8f0fd99d 100644 --- a/test/DemaConsulting.SysML2Tools.Tests/Rendering/DiagramTypeRouterTests.cs +++ b/test/DemaConsulting.SysML2Tools.Tests/Rendering/DiagramTypeRouterTests.cs @@ -225,4 +225,48 @@ public void GetStrategy_UnrecognizedRenderTarget_FallsThroughUnchanged() Assert.Null(unsupported); Assert.IsType(strategy); } + + /// + /// A render target that is a near-miss of asTreeDiagram (wrong case) does not match: the + /// comparison is exact and case-sensitive, so the view falls through unchanged to the existing + /// heuristic instead of routing to the browser strategy. + /// + [Fact] + public void GetStrategy_RenderTargetWrongCase_DoesNotMatchAndFallsThroughUnchanged() + { + var view = new SysmlViewNode + { + Name = "GeneralView", + QualifiedName = "M::GeneralView", + RenderTargetName = "ASTreeDiagram" + }; + var workspace = new SysmlWorkspace(); + + var strategy = DiagramTypeRouter.GetStrategy(view, workspace, out var unsupported); + + Assert.Null(unsupported); + Assert.IsType(strategy); + } + + /// + /// A render target with trailing whitespace after asTreeDiagram does not match: the + /// comparison is exact, so the view falls through unchanged to the existing heuristic instead + /// of routing to the browser strategy. + /// + [Fact] + public void GetStrategy_RenderTargetTrailingWhitespace_DoesNotMatchAndFallsThroughUnchanged() + { + var view = new SysmlViewNode + { + Name = "GeneralView", + QualifiedName = "M::GeneralView", + RenderTargetName = "asTreeDiagram " + }; + var workspace = new SysmlWorkspace(); + + var strategy = DiagramTypeRouter.GetStrategy(view, workspace, out var unsupported); + + Assert.Null(unsupported); + Assert.IsType(strategy); + } } From ca67140c7bd1945c07e8e2d3a756b3bdcf9970f7 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Tue, 7 Jul 2026 07:08:14 -0400 Subject: [PATCH 3/3] Use explicit render statements in gallery views instead of name inference Add render asInterconnectionDiagram;/asTreeDiagram; to the gallery models for the two view kinds SysML2Tools currently wires (Interconnection, Browser), demonstrating the render-kind-selection feature explicitly rather than relying solely on the pre-existing name-substring heuristic. Regenerated outputs confirmed byte-identical to the name-inferred renders. Also updates the gallery README to describe both dispatch paths and fixes a stale RenderTargetName XML doc comment that still described the capability as future work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/gallery/README.md | 22 ++++++++++++++----- .../models/02-computer-interconnection.sysml | 5 ++++- docs/gallery/models/07-avionics-browser.sysml | 4 +++- .../models/08-nested-interconnection.sysml | 4 +++- .../Semantic/Model/SysmlNode.cs | 9 ++++---- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/docs/gallery/README.md b/docs/gallery/README.md index c721c73d..daeba663 100644 --- a/docs/gallery/README.md +++ b/docs/gallery/README.md @@ -5,9 +5,13 @@ generated from an interesting example system. Every model is rendered to both **PNG** (raster, in [`png/`](png/)) and **SVG** (vector, in [`svg/`](svg/)). All diagrams are produced by the `sysml2tools render` command directly from the -SysML v2 textual models in [`models/`](models/) — no manual layout. The view kind -is selected automatically from each view's name (see the -[rendering roadmap](../../ROADMAP.md) for the dispatch rules). +SysML v2 textual models in [`models/`](models/) — no manual layout. Where a +model declares an explicit `render asTreeDiagram;` or `render +asInterconnectionDiagram;` statement (the two rendering kinds SysML2Tools +currently implements), that takes precedence; otherwise the view kind is +inferred from each view's name (see the +[DiagramTypeRouter design](../design/sysml2-tools-core/rendering/internal/diagram-type-router.md) +for the full dispatch rules). To regenerate the gallery, run for each model: @@ -51,7 +55,9 @@ SVG: [`svg/BatterySubsystemView.svg`](svg/BatterySubsystemView.svg) Shows the internal structure of a part: nested part usages placed by the interconnection layout engine (a façade over the layered pipeline), ports on box boundaries, and connectors routed between them. -The motherboard sits at the hub of the component connections. +The motherboard sits at the hub of the component connections. The view declares an +explicit `render asInterconnectionDiagram;` statement, so the interconnection +strategy is selected directly rather than inferred from the view's name. Model: [`models/02-computer-interconnection.sysml`](models/02-computer-interconnection.sysml) · SVG: [`svg/WorkstationInterconnectionView.svg`](svg/WorkstationInterconnectionView.svg) @@ -153,7 +159,9 @@ SVG: [`svg/CarLineageGridView.svg`](svg/CarLineageGridView.svg) ## 7. Browser View — Avionics System Shows the membership hierarchy of nested packages and definitions as an indented -tree with parent-to-child connectors. +tree with parent-to-child connectors. The view declares an explicit `render +asTreeDiagram;` statement, so the browser strategy is selected directly rather +than inferred from the view's name. Model: [`models/07-avionics-browser.sysml`](models/07-avionics-browser.sysml) · SVG: [`svg/AvionicsBrowserView.svg`](svg/AvionicsBrowserView.svg) @@ -168,7 +176,9 @@ Shows a two-level nested Interconnection View. The `Computer` part contains a `b typed by `Motherboard`, which has its own internal `cpu`, `chipset`, and `ram` parts and connections. The motherboard's interior is laid out recursively (bottom-up) and nested inside the `board` container box, while the outer power and storage connections route -between the top-level parts. +between the top-level parts. The view declares an explicit `render +asInterconnectionDiagram;` statement, so the interconnection strategy is +selected directly rather than inferred from the view's name. Model: [`models/08-nested-interconnection.sysml`](models/08-nested-interconnection.sysml) · SVG: [`svg/ComputerSystemInterconnectionView.svg`](svg/ComputerSystemInterconnectionView.svg) diff --git a/docs/gallery/models/02-computer-interconnection.sysml b/docs/gallery/models/02-computer-interconnection.sysml index 9bf0af39..c729a431 100644 --- a/docs/gallery/models/02-computer-interconnection.sysml +++ b/docs/gallery/models/02-computer-interconnection.sysml @@ -28,12 +28,15 @@ package DesktopComputer { connection c8 connect cpu to memory; } - view def WorkstationInterconnectionView {} + view def WorkstationInterconnectionView { + render asInterconnectionDiagram; + } // View-scoped rendering: exposing just cpu and memory narrows the diagram to those // two parts and the connection between them (c8), dropping every connection with an // endpoint outside the exposed scope (board, graphics, storage, psu, network). view CoreLinkInterconnectionView { + render asInterconnectionDiagram; expose Workstation::cpu; expose Workstation::memory; } diff --git a/docs/gallery/models/07-avionics-browser.sysml b/docs/gallery/models/07-avionics-browser.sysml index 02c0e4c4..81110465 100644 --- a/docs/gallery/models/07-avionics-browser.sysml +++ b/docs/gallery/models/07-avionics-browser.sysml @@ -33,5 +33,7 @@ package AvionicsSystem { part def NavigationDisplay; } - view def AvionicsBrowserView {} + view def AvionicsBrowserView { + render asTreeDiagram; + } } diff --git a/docs/gallery/models/08-nested-interconnection.sysml b/docs/gallery/models/08-nested-interconnection.sysml index 4c67d518..9adf8263 100644 --- a/docs/gallery/models/08-nested-interconnection.sysml +++ b/docs/gallery/models/08-nested-interconnection.sysml @@ -28,5 +28,7 @@ package ComputerSystem { connection c3 connect board to storage; } - view def ComputerSystemInterconnectionView {} + view def ComputerSystemInterconnectionView { + render asInterconnectionDiagram; + } } diff --git a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs index 5bfcadf8..496ff7a2 100644 --- a/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs +++ b/src/DemaConsulting.SysML2Tools.Language/Semantic/Model/SysmlNode.cs @@ -181,10 +181,11 @@ public sealed class SysmlViewNode : SysmlNode /// when the view declares no rendering member. Per the SysML v2 grammar, this names a /// rendering style/format usage (e.g. asTreeDiagram, asElementTable) — never /// a content-scoping subject. Captured verbatim only: never - /// inspects or resolves this value (no edge is produced, no diagnostic is emitted), and it - /// has no effect on GeneralViewLayoutStrategy's rendered scope. Reserved for a - /// possible future capability that selects among rendering-style strategies — see the - /// project ROADMAP. + /// inspects or resolves this value (no edge is produced, no diagnostic is emitted). This + /// value is used by DiagramTypeRouter to select a rendering strategy — an exact + /// match against asTreeDiagram or asInterconnectionDiagram takes precedence + /// over the name/supertype heuristic; any other value (including asElementTable and + /// asTextualNotation, which have no corresponding strategy) falls through unchanged. /// public string? RenderTargetName { get; init; }