Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ words:
- normalised
- optimise
- optimised
- opto
- Opto
- quadtree
- quantisation
- Quantisation
Expand Down
18 changes: 18 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ primitives (bar, diamond, pentagon, note). `LayoutActivation`/`LayoutBand` alrea
**Visual gate:** sequence shows activation bars + a fragment; action flow shows a fork/join and
a decision/merge with correct shapes.

### Interconnection View: genuine cross-boundary connector routing

`InterconnectionViewLayoutStrategy` now resolves a connection endpoint's full dotted reference
(e.g. `board.cpu`) for its port **label**, so a cross-boundary reference shows the true nested
target's name instead of discarding it. The connector line itself, however, still terminates at
the containing part's own box boundary rather than routing all the way into the nested container
to the inner part — genuine cross-boundary routing would require restructuring
`LayOutInterior`'s per-level independent `LayeredPlacement.Place` calls into one connected nested
`LayoutGraph`/`LayoutGraphNode.Children` for the affected subtree, using the companion
`DemaConsulting.Rendering` package's boundary/delegation-port (`HierarchyHandling.Recursive`)
support end-to-end, instead of the strategy's current two-independent-layouts-stitched-together
recursion.

**Scope:** `InterconnectionViewLayoutStrategy`'s `LayOutInterior`/`CollectParts` recursion;
possibly a new `LayeredPlacement` entry point that builds a genuinely nested `LayoutGraph`.
**Visual gate:** `connect psu to board.cpu;` renders a connector line that visually terminates on
the inner `cpu` box, not the `board` container's boundary.

### View `filter [<expr>];` expression evaluation

`GeneralViewLayoutStrategy` now scopes a rendered diagram to a view's `expose <...>;` subject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,15 @@ were not depth-truncated — is decided later, in `BuildGraph`.

###### `BuildGraph(groups, modelEdges, theme, depthLimit)`

Builds the single input `LayoutGraph`. Each package becomes a folder container node
Builds the single input `LayoutGraph`, setting `CoreOptions.MergeParallelEdges` to `false` on the
root graph so multiple distinct model relationship edges that happen to share the same source and
target (for example two attributes of the same type, or a redefinition edge that coincides with
another edge between the same two definitions) are never collapsed by the bundled layered
algorithm's default parallel-edge merging — every distinct model relationship this strategy adds
remains its own visible, independently-routed edge, unlike `LayeredPlacement`'s helper (used by the
flat view strategies), which defaults to the algorithm's own merge-by-default behavior unless a
caller opts out (see `LayeredPlacement`'s design documentation). Each package becomes a folder
container node
(`Shape = Folder`, `Keyword = "package"`, `Label` the simple package name, `TitleHeight` set from
`BoxMetrics.TitleAreaHeight` so the hierarchical engine reserves the exact title band the renderer
will draw) holding its definitions as leaf nodes under `folder.Children`; the folder's own
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ container box for the host definition.
##### Data Model

`InterconnectionViewLayoutStrategy` has no instance state; all input arrives through the
`BuildLayout` parameters. Layout constants (`MinPartWidth`, `CharWidthFactor`, `MinPortSlot`,
`ConnectorClearance`) are declared as `private const double` fields. Three private records carry
intermediate data: `PartItem` (a nested part usage with its computed box size, typing, and — when
the part is a container — its pre-laid-out `InnerContent`), `ConnPair` (a resolved binary
connection between two nested-part indices), and `InteriorLayout` (the full container size and
content produced by laying out one definition's interior).
`BuildLayout` parameters. Layout constants (`MinPartWidth`, `CharWidthFactor`) are declared as
`private const double` fields. Three private records carry intermediate data: `PartItem` (a nested
part usage with its computed box size, typing, and — when the part is a container — its
pre-laid-out `InnerContent`), `ConnPair` (a resolved binary connection between two nested-part
indices, together with the optional port-name label for each end), and `InteriorLayout` (the full
container size and content produced by laying out one definition's interior).

##### Key Methods

Expand All @@ -25,9 +25,10 @@ content produced by laying out one definition's interior).
Entry point. Resolves the view's `expose` scope via `ExposeScopeResolver.ResolveExposedScope`,
selects the root part definition via `FindRoot(workspace, scope)`, builds the container-definition
index via `BuildDefinitionIndex`, lays out the root's interior via `LayOutInterior` (threading
`scope` through every recursive call), and assembles the root container box plus the interior
content into the `LayoutTree`. Returns a minimal 200×100 empty `LayoutTree` when no root or no
parts are found.
`scope` through every recursive call), and assembles the root container box with the interior
content nested as that box's own `Children` (mirroring the nesting `MakePartBox` already uses for a
container part, so the root box is never a bare sibling of its own content) into the `LayoutTree`.
Returns a minimal 200×100 empty `LayoutTree` when no root or no parts are found.

###### Recursive nested layout (`LayOutInterior`, `CollectParts`, `BuildDefinitionIndex`)

Expand All @@ -51,26 +52,70 @@ node by its parent, which is laid out with the **same** flat placement.
`offsetX = LabelPadding × 2`, `offsetY = TitleAreaHeight(hasLabel, hasKeyword) + LabelPadding × 2`,
`containerWidth = TotalWidth + offsetX × 2`, and
`containerHeight = TotalHeight + offsetY + LabelPadding × 2`. A container box therefore bounds its
laid-out children plus its title and insets, and the parent treats that size as atomic. Because a
node is only ever grown via `Math.Max(height, degreeMinHeight)`, the reserved interior never
shrinks below what the children need; any extra port-slot height is empty space at the bottom of
the container.
laid-out children plus its title and insets, and the parent treats that size as atomic. Box
height itself is no longer scaled by hand from a per-port minimum-slot heuristic: each part's
ports are modeled as named `LayoutGraphPort`s on `LayeredPlacement.PlaceWithPorts`'s input graph,
so the layered algorithm itself grows a box, when needed, to keep every incident connection's
port visually distinct. Every part is also passed to `PlaceWithPorts` with `HasLabel: true,
HasKeyword: true` (every `PartItem` always carries a non-empty name and a `"part"` keyword,
mirroring the `hasLabel: true, hasKeyword: true` convention already used for `TitleAreaHeight`
above), which activates the engine's automatic title-vs-side-port reservation so no port is ever
placed across the box's own title band.
- **Positioning.** `MakePartBox` builds a leaf box with empty `Children` (unchanged), and a
container box whose `Children` are its `InnerContent` translated from the child's local origin
`(0, 0)` to the box's absolute top-left by `TranslateNodes`, which recursively shifts box
positions (and their nested children), port centres, and connector waypoints. The interior was
laid out reserving its own title area, so the inner part boxes land below the container's
"name : Type" title, inside its border. Box `Depth` increases by one per level (the renderer
indexes `DepthFillColors` by modulo, so any depth is safe).
indexes `DepthFillColors` by modulo, so any depth is safe). The root container box mirrors this
same pattern one level up: `BuildLayout` nests the root's own interior content as the root box's
`Children` rather than as flat top-level siblings of the root box — the root sits at the same
origin `(0, 0)` its interior content is already positioned relative to, so no translation is
needed there.
- **No-op invariant.** When no part is a container, every `PartItem.InnerContent` is `null`,
`MakePartBox` emits exactly the non-recursive leaf box with empty `Children`, and the placement
call, offsets, ports, and lines are identical to the single-level layout — single-level output is
byte-identical.
- **Cross-boundary limitation.** Connection endpoints resolve to **parent-level** part indices via
the head segment of the dotted reference, so a reference such as `connect psu to board.cpu`
terminates on the `board` container boundary rather than routing to the inner `cpu` box. This is
the known `SEPARATE_CHILDREN` limitation (no true cross-boundary routing); gallery models avoid
relying on cross-boundary endpoints.
- **Cross-boundary resolution.** `ResolveEndpoint` resolves the **full** dotted endpoint reference,
not just its head segment: the head segment still identifies the parent-level part index, and any
remaining dotted segment(s) (e.g. `cpu` in `board.cpu`, or `encoder` in `StepperMotorX.encoder`) is
captured as that endpoint's port-name label and flows through `ConnPair.LabelA`/`LabelB` into the
emitted `LayoutPort.ExternalLabel`. This means a reference into a nested part (`connect psu to
board.cpu`) now shows the true target name (`cpu`) on the port, rather than the pre-fix behavior of
silently discarding it. The connector itself is still **routed only to the container's own
boundary** — it does not continue into the container's interior to physically terminate on the
inner `cpu` box. Achieving that (a genuine boundary/delegation-port anchor shared between the
outer connector and an inner one, via the companion library's `HierarchyHandling.Recursive`
support) would require restructuring `LayOutInterior`'s per-level independent
`LayeredPlacement.PlaceWithPorts` calls into one connected nested `LayoutGraph`/
`LayoutGraphNode.Children` for the affected subtree, which is a materially larger architectural
change than this feature makes; it remains a known, documented limitation. `ResolveEndpoint`
captures **everything** after the first dotted segment as the label, however many levels deep
(e.g. `board.sub.cpu` yields the label `sub.cpu`, not just `sub`); only the _routing_ — never the
label text — stops at the container's boundary regardless of path depth.

##### Port Labeling

Each connection endpoint that resolves to a nested part is requested as a named
`LayeredPlacement.EdgePortRef` on that endpoint's `PortEdge`, carrying the real SysML port-name
segment (from `ConnPair.LabelA`/`LabelB`; see _Cross-boundary resolution_ above and
`ResolveEndpoint` below) as the port's `ExternalLabel`. `LayeredPlacement.PlaceWithPorts` creates
the corresponding `LayoutGraphPort` and returns the engine-placed `LayoutPort` with that label
already attached — the strategy only translates the returned port's `CentreX`/`CentreY` by the
container offset before adding it to the interior's content. A bare endpoint reference with no
dotted port segment (e.g. `connect psu to board`) yields a `null` label (a port is still created
and placed; only its label is absent), preserving the pre-fix rendering (no label shown) for that
connector end.

##### Parallel Connection Preservation

`LayOutInterior` calls `LayeredPlacement.PlaceWithPorts` (which unconditionally disables
parallel-edge merging — see `docs/design/sysml2-tools-core/layout/internal/layered-placement.md`),
so multiple distinct SysML connections between the same two parts (e.g. separate
`power`/`encoder`/`sensor` connections wired between the same controller and motor, as seen in a
real 3-axis-gantry wiring model) are never collapsed onto one shared routed polyline: each
connection keeps its own independently-routed connector with distinct waypoints (a separate
parallel lane), and its own pair of labeled ports.

###### `FindRoot(workspace, scope)`

Expand All @@ -89,10 +134,12 @@ break ties among equally specific candidates; this ordering does not apply when
`CollectParts` gathers the root's nested `part` usages, sizing each box from its `name : Type`
label, additionally excluding — when a scope is resolved — any part feature whose qualified name
fails `ExposeScopeResolver.IsInSubjectScope`. `ResolveConnections` maps each binary connection's
dotted endpoint references to nested-part indices by matching the first segment against the
(possibly narrowed) part names, keeping only distinct, resolvable pairs; a connection whose
endpoint was excluded by scoping simply fails to resolve and is dropped by this existing
endpoint-lookup logic — no separate edge-side scoping is needed.
dotted endpoint references to nested-part indices and port-name labels via `ResolveEndpoint`
(matching the first dotted segment against the (possibly narrowed) part names and capturing any
remaining segment as the port label — see _Cross-boundary resolution_ above), keeping only
distinct, resolvable pairs; a connection whose endpoint was excluded by scoping simply fails to
resolve and is dropped by this existing endpoint-lookup logic — no separate edge-side scoping is
needed.

##### Expose Scoping

Expand All @@ -116,13 +163,16 @@ every candidate and `CollectParts` keeps every part, unchanged from the pre-scop

###### Placement and routing

Placement and routing are delegated to `LayeredPlacement.Place` with a left-to-right flow direction.
Nested-container recursion is driven at the strategy level (see _Recursive nested layout_ above);
each level calls the same flat placement helper. The strategy passes the collected part boxes and
resolved connection pairs as plain geometric input. `LayeredPlacement` delegates to the off-the-shelf
`DemaConsulting.Rendering.Layout` layered algorithm and returns placed rectangles and connector
waypoints, with disconnected components packed without overlap. The total canvas extent is derived
from the placed box and waypoint geometry.
Placement and routing are delegated to `LayeredPlacement.PlaceWithPorts` with a left-to-right flow
direction; parallel-edge merging is unconditionally disabled by that method (see _Parallel
Connection Preservation_ above). Nested-container recursion is driven at the strategy level (see
_Recursive nested layout_ above); each level calls the same flat placement helper. The strategy
passes the collected part boxes and resolved connection pairs — each carrying an `EdgePortRef` for
every endpoint — as plain geometric input. `LayeredPlacement` delegates to the off-the-shelf
`DemaConsulting.Rendering.Layout` layered algorithm and returns placed rectangles, connector
waypoints, and correlated placed ports, with disconnected components packed without overlap and
port spacing/box growth resolved by the engine itself. The total canvas extent is derived from the
placed box and waypoint geometry.

The strategy then shifts the placed content to sit inside the container box and extends the
container so every connector waypoint is enclosed, without ever moving a box.
Expand Down
Loading
Loading