diff --git a/changes/unreleased/cameo-suppressed-compartments.added.md b/changes/unreleased/cameo-suppressed-compartments.added.md new file mode 100644 index 0000000000..a0f784fe99 --- /dev/null +++ b/changes/unreleased/cameo-suppressed-compartments.added.md @@ -0,0 +1 @@ +- `sysml -migrate sysml` keeps the compartments a modeler collapsed on a Cameo symbol: the symbol's `SUPPRESS_*` compartment properties, its own and the ones inherited from the diagram's symbol styles, are written as `showAttributes`, `showParts`, `showReferences`, `showPorts`, `showConstraints`, `showItems` and `showActions = false` on the symbol's `DiagramLayout::Layout`, and the migration report counts them (`# compartments suppressed: N on M symbols`). The positioned-tree renderer honours the switches per symbol: a hidden compartment is neither listed in the box nor counted among the rows clipped by its stated size, while a rendering's `showCompartments = false` still hides every compartment. diff --git a/docs/guide/11-migrating-from-sysml-v1.md b/docs/guide/11-migrating-from-sysml-v1.md index faf17ae228..0ef23ffb25 100644 --- a/docs/guide/11-migrating-from-sysml-v1.md +++ b/docs/guide/11-migrating-from-sysml-v1.md @@ -379,6 +379,15 @@ symbol stream and writes it beside the view it produces as [DiagramLayout](../project/diagram-layout-annotations.md) metadata — `Layout` boxes, `Route` waypoints, a `Canvas` the size of the diagram frame, a `Style` for a symbol drawn in its own colours or font, and a `Note` for every comment and text box — so nothing has to be laid out again. +The compartments a modeler collapsed on a symbol (Cameo's `SUPPRESS_*` symbol properties, stated +on the symbol or inherited from the diagram's symbol styles) become the `Layout`'s `show…` +switches, by the kind their members take in v2 — parts `showParts`, references `showReferences`, +properties, values and class attributes `showAttributes`, constraints `showConstraints`, flow +properties `showItems`, proxy, full and class ports `showPorts`, operations `showActions` — so a +box the modeler sized for the compartments left visible is drawn with those alone, with no +clipped-row notice for the rest; the report counts them (`# compartments suppressed: N on M +symbols`). A suppressed compartment with no v2 kind (structure, behaviors, signal receptions) is +listed among the unsupported presentation properties. No export from another tool is needed; an [MTIP](https://github.com/Open-MBEE/mtip-cameo) export given with `-layout` is still honoured and takes precedence for every element its record places or routes, the stream supplying the elements the record leaves out diff --git a/docs/project/diagram-layout-annotations.md b/docs/project/diagram-layout-annotations.md index 66e9f8a178..78e46e35b3 100644 --- a/docs/project/diagram-layout-annotations.md +++ b/docs/project/diagram-layout-annotations.md @@ -58,6 +58,13 @@ library package DiagramLayout { attribute width : ScalarValues::Real[0..1]; attribute height : ScalarValues::Real[0..1]; attribute collapsed : ScalarValues::Boolean[0..1]; + attribute showAttributes : ScalarValues::Boolean[0..1]; + attribute showParts : ScalarValues::Boolean[0..1]; + attribute showReferences : ScalarValues::Boolean[0..1]; + attribute showPorts : ScalarValues::Boolean[0..1]; + attribute showConstraints : ScalarValues::Boolean[0..1]; + attribute showItems : ScalarValues::Boolean[0..1]; + attribute showActions : ScalarValues::Boolean[0..1]; } metadata def Route { @@ -248,7 +255,21 @@ For a (view, element) pair the **effective layout** is, in order: 3. none: the element is laid out by the consumer. A `Route` resolves the same way for an edge's declaring element. A `Canvas` belongs to a -view alone, stated in its body. Where a view's body states two `about` annotations of one +view alone, stated in its body. + +A `Layout`'s `show…` switches are per symbol, as its box is: `showParts = false` hides the +parts compartment of the annotated element's box in that view (and, inline, in every view +that states nothing), while the same element drawn in another view, or by another `Layout`, +keeps its compartments. Unset, a switch leaves the compartment to the rendering, whose +`showCompartments = false` hides every compartment whatever the switches say. The seven +switches follow the compartments a modelling tool lets a modeler collapse symbol by symbol: +`showAttributes` the attributes, `showParts` the composite part rows, `showReferences` the +`ref` rows of every compartment, `showPorts`, `showConstraints`, `showItems` and `showActions` +the compartments of those kinds. A hidden compartment's members are neither listed in the +box nor counted among the rows its stated size clips; the folding notice counts them apart +(`9 node(s) without a position: 7 drawn as compartment rows of the node above them, 2 in +compartments its Layout hides left undrawn`). The SysML v1 migrator writes them from a +Cameo symbol's `SUPPRESS_*` properties (see [migrating from SysML v1](../guide/11-migrating-from-sysml-v1.md#publishing-a-migrated-document-with-cameo-style-diagrams)). Where a view's body states two `about` annotations of one kind for one element, the first in declaration order applies and the second is a warning. With no view — a `#tree`, `#interconnection:X`, `#case` or `#mixed` pseudo-view, and the @@ -265,7 +286,7 @@ annotation stated anywhere else is element-level. ### The rendering tree `view.Node` carries `Geometry *Geometry` (`X`, `Y`, `Width`, `Height`, `HasSize`, -`Collapsed`), `view.Edge` carries `Route []Point`, and `view.Rendering` carries +`Collapsed`, `Hidden`), `view.Edge` carries `Route []Point`, and `view.Rendering` carries `Canvas *Canvas` (`Unit`, `Width`, `Height`, `HasSize`). They are values: the tree stays free of AST nodes and symbols, and `Rendering.Clone` copies them. Every graph-shaped rendering populates them — the containment tree, the interconnection diagram, the state machine diff --git a/docs/project/pilot-differential-baseline.json b/docs/project/pilot-differential-baseline.json index a9da421ccf..be25fef0db 100644 --- a/docs/project/pilot-differential-baseline.json +++ b/docs/project/pilot-differential-baseline.json @@ -76,7 +76,7 @@ "dir": "internal/workspace/libs/stdlib/OpenSysML Libraries", "origin": "ours", "files": 16, - "digest": "sha256:478be4a232db162cf4de51048786b7f78013bb8495de861c2647ef0e73523925" + "digest": "sha256:c399daea865e29d2c056db2746eb3aad94ab26e591cf7669ffcb060583dfee0b" } ], "recorded": "2026-10-16" diff --git a/docs/project/sysml-v1-transformation-census-baseline.json b/docs/project/sysml-v1-transformation-census-baseline.json index ac43dcdd4a..0369f76e93 100644 --- a/docs/project/sysml-v1-transformation-census-baseline.json +++ b/docs/project/sysml-v1-transformation-census-baseline.json @@ -22,7 +22,7 @@ }, { "name": "fixtures", - "files": 123 + "files": 124 } ], "counts": { @@ -140,7 +140,7 @@ }, "uml:Class": { "pssm": 311, - "fixtures": 567 + "fixtures": 570 }, "uml:ClearAssociationAction": { "pssm": 0, @@ -248,7 +248,7 @@ }, "uml:Property": { "pssm": 25, - "fixtures": 566 + "fixtures": 571 }, "uml:Property[qualifier]": { "pssm": 0, diff --git a/docs/project/sysml-v1-transformation-census.md b/docs/project/sysml-v1-transformation-census.md index 66c922b729..2551bf8f3a 100644 --- a/docs/project/sysml-v1-transformation-census.md +++ b/docs/project/sysml-v1-transformation-census.md @@ -1094,7 +1094,7 @@ SysML profile namespace. The counts are elements affected, not effort. | Rank | OMG mappings | Status | Scope | PSSM suite | Fixtures | Total | Reason | |---|---|---|---|---|---|---|---| -| 1 | 1: `PropertyUntyped_Mapping` | ⚠️ approximate | `uml:Property` | 25 | 566 | 591 | an untyped property is written as the usage its tool's property-kind marker names, as a `ref` typed by its default individual, else as a `part` when composite and an `attribute` otherwise, rather than always as a reference usage; the scope counts every uml:Property, so it is an upper bound | +| 1 | 1: `PropertyUntyped_Mapping` | ⚠️ approximate | `uml:Property` | 25 | 571 | 596 | an untyped property is written as the usage its tool's property-kind marker names, as a `ref` typed by its default individual, else as a `part` when composite and an `attribute` otherwise, rather than always as a reference usage; the scope counts every uml:Property, so it is an upper bound | | 2 | 8: `EndToSubsettedFeatureChaining_Mapping`, `EndToSubsettedFeature_Mapping`, `NonOwnedEndFeatureTyping_Mapping`, `NonOwnedEndMembership_Mapping`, `NonOwnedEndSubsettingMembership_Mapping`, `NonOwnedEndSubsetting_Mapping`, `NonOwnedEndToSubsettedFeatureMembership_Mapping`, `NonOwnedEnd_Mapping` | ⚠️ approximate | `uml:Association` | 4 | 37 | 41 | a classifier-owned end is written as that classifier's property and as an end of the connection def crossing it through the opposite end, `end wheels : Wheel crosses car.wheels;`, the mapping's feature chain; the cross is left out, and the association reported approximated with the reason, where it would be unsound: the opposite end is untyped, the end's type is not written or is written as a usage (an actor), the association is not binary, or the property is not written as a feature of the opposite end's type | | 3 | 1: `TimeTriggerReferenceUsage2_Mapping` | ⚠️ approximate | `uml:TimeEvent` | 0 | 16 | 16 | a relative time event is written `accept after ` and an absolute one `accept at ` on the simulation clock; a time that is neither a number with a time unit nor an expression is unmapped (fixtures: 2 of 10 time events) | | 4 | 3: `EqualOperatorExpressionFeatureValue_Mapping`, `EqualOperatorExpressionFeature_Mapping`, `EqualOperatorExpressionOperandParameterMembership_Mapping` | ❌ not implemented | `uml:TestIdentityAction` | 14 | 0 | 14 | these are used only by TIAOperatorExpression_Mapping, as the private first/second operands of a TestIdentityAction's `==`, not by Expression trees; they are written with TestIdentityAction, which is not migrated (PSSM: 14) | diff --git a/docs/project/view-rendering-forms.md b/docs/project/view-rendering-forms.md index f4b77875d7..51e18fb3b6 100644 --- a/docs/project/view-rendering-forms.md +++ b/docs/project/view-rendering-forms.md @@ -901,7 +901,11 @@ digraph "PlantViews::placedView" { does, so no row is ever empty). No edge is written to a member drawn as a row, and the notice counts them (`4 node(s) without a position, drawn as compartment rows of the node above them`); a row shows the member alone, so the members nested under a row are left undrawn - and the notice counts them apart (`, and 1 node(s) under them left undrawn`). A member with a + and the notice counts them apart (`, and 1 node(s) under them left undrawn`). A member of a + compartment the owner's `Layout` hides with a `show…` switch (`showParts = false`; see + [DiagramLayout annotations](diagram-layout-annotations.md)) is not drawn as a row and the + notice says so (`9 node(s) without a position: 7 drawn as compartment rows of the node above + them, 2 in compartments its Layout hides left undrawn`); its edges are dropped as a row's are. A member with a `Layout` of its own stays a node (a compartment row by its box when inside the owner's, an edge when outside), as does an unplaced member with a positioned node under it, so that node keeps its owner. An unpositioned tree is unchanged: every member diff --git a/docs/reference/sysml-v1-migration.md b/docs/reference/sysml-v1-migration.md index 343d9fe4b7..c4f3a1d943 100644 --- a/docs/reference/sysml-v1-migration.md +++ b/docs/reference/sysml-v1-migration.md @@ -584,6 +584,30 @@ as several members — a control flow written as a succession into a join the mi and from that join on — is pinned to the members whose both ends a diagram symbol stands at, so the route is drawn where Cameo drew it and not into a node no symbol stands for. +The compartments a modeler collapsed on a symbol are kept with its box. A Cameo symbol's +`SUPPRESS_*` compartment properties — `BooleanProperty` elements in its `properties`, a `value` +of `true` meaning suppressed and no `value` the tool's default, shown — are read from the symbol +itself and then from the chain of symbol styles the stream's `symbolStyles` hold (the property +manager its `symbolStyleID` names and each `parentPropertyManager` above it), the symbol's own +setting winning over an inherited one. Each suppressed compartment is written as a `show…` +switch of the symbol's `Layout`, by the kind its members take in v2: parts (`SUPPRESS_PARTS_COMPARTMENT`, +`SUPPRESS_IBD_PARTS_COMPARTMENT`) `showParts = false`; references (`…REFERENCES_COMPARTMENT`) +`showReferences`; properties, values and class attributes (`…PROPERTIES_COMPARTMENT`, +`…VALUES_COMPARTMENT`, `SUPPRESS_CLASS_ATTRIBUTES`, the IBD values, UML-property and initial-value +compartments) `showAttributes`; constraints (`…CONSTRAINTS_COMPARTMENT`) `showConstraints`; flow +properties (`…FLOWPROPERTIES_COMPARTMENT`) `showItems`; proxy, full and class ports +(`SUPPRESS_PROXYPORTS_COMPARTMENT`, `SUPPRESS_FULLPORTS_COMPARTMENT`, `SUPPRESS_CLASS_PORTS`) +`showPorts`; operations (`SUPPRESS_CLASS_OPERATIONS`, `SUPPRESS_IBD_OPERATIONS_COMPARTMENT`) +`showActions`; a v2 compartment lists what several Cameo compartments list between them, so its +switch is written only when every one of them the symbol states is suppressed (a block's class +attributes compartment is suppressed by Cameo's own style while its values compartment is shown: +`showAttributes` stays unset). The report's layout summary counts them (`# compartments suppressed: N on M +symbols`; `compartmentsSuppressed` and `symbolsSuppressing` in the sidecar). A suppressed +compartment no v2 compartment answers to (`SUPPRESS_STRUCTURE`, `SUPPRESS_BEHAVIORS`, +`SUPPRESS_SIGNAL_RECEPTIONS`) is counted among the unsupported presentation properties and +dropped. The renderer then lists only the compartments left on and counts only their rows against +the box's stated size (see [DiagramLayout annotations](../project/diagram-layout-annotations.md)). + A note the diagram shows is written as `metadata DiagramLayout::Note about { text; x; y; width; height; }` — a `Note` symbol whatever it names, a comment's symbol, or a free text box saying something of its own — with the comment's body when it stands for a comment and the diff --git a/internal/ir/view/compartments.go b/internal/ir/view/compartments.go index e440a5dbf5..3a7da19c58 100644 --- a/internal/ir/view/compartments.go +++ b/internal/ir/view/compartments.go @@ -4,6 +4,7 @@ import ( "slices" "strings" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/source" ) @@ -139,6 +140,31 @@ func (l labeller) compartments(node *Node) []Compartment { return node.Compartments } +// referenceRow reports whether sym lists as a reference row, the `ref part` +// a tool's references compartment holds: a structural usage that refers to +// its value rather than owning it. An attribute, referential by definition, +// is an attribute row. +func referenceRow(sym *symbols.Symbol) bool { + switch sym.Kind { + case symbols.SymbolAttributeUsage, symbols.SymbolEnumerationUsage: + return false + } + return structuralUsage(sym) && !compositeUsage(sym) +} + +// hiddenRow reports whether node's Layout hides the compartment row would +// list in: a hidden "references" hides every referential row, a hidden kind +// the compartment of that name. +func hiddenRow(node, row *Node) bool { + if node.Geometry == nil || !node.Geometry.Hidden.Any() { + return false + } + if row.Referential && !row.StandIn { + return node.Geometry.Hidden.Hides("references") + } + return node.Geometry.Hidden.Hides(compartmentName(row.Kind)) +} + // rowCount is how many rows a node's compartments hold. func (l labeller) rowCount(node *Node) int { n := 0 diff --git a/internal/ir/view/geometry.go b/internal/ir/view/geometry.go index 5b23240e48..9e35f29f4d 100644 --- a/internal/ir/view/geometry.go +++ b/internal/ir/view/geometry.go @@ -20,11 +20,19 @@ type Point struct { // Geometry is where a node is drawn, from the DiagramLayout::Layout positioning // it in the rendered view; HasSize says whether Width and Height were given. +// Hidden are the compartments the Layout's show… switches turn off for this +// symbol, which its box neither lists nor counts as clipped. type Geometry struct { X, Y float64 Width, Height float64 HasSize bool Collapsed bool + Hidden semantics.HiddenCompartments +} + +// geometryFromLayout is the Geometry a resolved Layout gives. +func geometryFromLayout(l *semantics.Layout) *Geometry { + return &Geometry{X: l.X, Y: l.Y, Width: l.Width, Height: l.Height, HasSize: l.HasSize, Collapsed: l.Collapsed, Hidden: l.Hidden} } // centre is the middle of the geometry, its corner when it has no size. @@ -201,8 +209,7 @@ func (r *Renderer) geometryOf(view, elem *symbols.Symbol, out *Rendering) *Geome if site.Layout == nil { return nil } - l := site.Layout - return &Geometry{X: l.X, Y: l.Y, Width: l.Width, Height: l.Height, HasSize: l.HasSize, Collapsed: l.Collapsed} + return geometryFromLayout(site.Layout) } // memberGeometryOf is the Geometry positioning, in view, the member owner has @@ -223,8 +230,7 @@ func (r *Renderer) memberGeometryOf(view, owner *symbols.Symbol, name string, ou if site.Layout == nil { return nil } - l := site.Layout - return &Geometry{X: l.X, Y: l.Y, Width: l.Width, Height: l.Height, HasSize: l.HasSize, Collapsed: l.Collapsed} + return geometryFromLayout(site.Layout) } // memberDress gives node the Style of, and adds the Notes anchored to, the diff --git a/internal/ir/view/placement.go b/internal/ir/view/placement.go index 51de8fb887..800032729b 100644 --- a/internal/ir/view/placement.go +++ b/internal/ir/view/placement.go @@ -150,13 +150,16 @@ func foldCompartments(r *Rendering) *Rendering { out := *r out.Roots = cloneNodes(r.Roots) rows := map[string]bool{} - drawn, nested := p.foldNodes(out.Roots, rows) - if drawn == 0 { + folded := p.foldNodes(out.Roots, rows) + if folded.drawn+folded.hidden == 0 { return r } - notice := fmt.Sprintf("%d node(s) without a position, drawn as compartment rows of the node above them", drawn) - if nested > 0 { - notice += fmt.Sprintf(", and %d node(s) under them left undrawn", nested) + notice := fmt.Sprintf("%d node(s) without a position, drawn as compartment rows of the node above them", folded.drawn) + if folded.hidden > 0 { + notice = fmt.Sprintf("%d node(s) without a position: %d drawn as compartment rows of the node above them, %d in compartments its Layout hides left undrawn", folded.drawn+folded.hidden, folded.drawn, folded.hidden) + } + if folded.nested > 0 { + notice += fmt.Sprintf(", and %d node(s) under them left undrawn", folded.nested) } out.Edges = make([]Edge, 0, len(r.Edges)) for _, edge := range r.Edges { @@ -182,27 +185,40 @@ func foldCompartments(r *Rendering) *Rendering { return &out } +// folding counts what folding drew as compartment rows, what it left undrawn +// because the node's Layout hides the row's compartment, and the nodes under +// the rows, which a row does not show. +type folding struct { + drawn, hidden, nested int +} + // foldNodes folds the unplaced members of each placed node under nodes into -// its compartments, in place, recording the IDs of the rows and of every -// node under them in rows; it counts the rows drawn and the nodes under -// them, which a row does not show. -func (p *placement) foldNodes(nodes []*Node, rows map[string]bool) (drawn, nested int) { +// its compartments, in place, less the rows of the compartments the node's +// Layout hides, recording the IDs of the folded members and of every node +// under them in rows. +func (p *placement) foldNodes(nodes []*Node, rows map[string]bool) folding { + var f folding for _, node := range nodes { if p.placed[node.ID] { - var folded []*Node + var folded, listed []*Node node.Children, folded = p.rowsOf(node) - node.Compartments = compartmentsOf(folded) for _, row := range folded { before := len(rows) recordIDs(row, rows) - drawn++ - nested += len(rows) - before - 1 + f.nested += len(rows) - before - 1 + if hiddenRow(node, row) { + f.hidden++ + continue + } + f.drawn++ + listed = append(listed, row) } + node.Compartments = compartmentsOf(listed) } - d, n := p.foldNodes(node.Children, rows) - drawn, nested = drawn+d, nested+n + sub := p.foldNodes(node.Children, rows) + f.drawn, f.hidden, f.nested = f.drawn+sub.drawn, f.hidden+sub.hidden, f.nested+sub.nested } - return drawn, nested + return f } // recordIDs records the ID of node and of every node under it in ids. diff --git a/internal/ir/view/suppressed_compartments_test.go b/internal/ir/view/suppressed_compartments_test.go new file mode 100644 index 0000000000..b8d4a5e4a8 --- /dev/null +++ b/internal/ir/view/suppressed_compartments_test.go @@ -0,0 +1,75 @@ +package view + +import ( + "path/filepath" + "strings" + "testing" +) + +// A Layout's show… switches hide compartments of one symbol: hidden ones are +// neither listed nor counted as clipped, the rest keep the fit and clipping +// rules, and a rendering that shows no compartments still shows none. +func TestGoldenSuppressedCompartments(t *testing.T) { + for _, view := range []string{"selective", "collapsed", "cramped", "roomy", "bare"} { + rendering := render(t, "suppressed-compartments.sysml", "CameraViews::"+view) + for _, form := range []Form{FormDot, FormMermaid, FormPlantUML, FormD2, FormText} { + out, err := rendering.Write(form) + if err != nil { + t.Fatalf("write %s %s: %v", view, form, err) + } + checkGolden(t, filepath.Join("testdata", "suppressed-compartments-"+view+"."+string(form)+".golden"), out) + } + } +} + +func TestSuppressedCompartmentsAreNotListedNorClipped(t *testing.T) { + dot := func(view string) string { + out, err := render(t, "suppressed-compartments.sysml", "CameraViews::"+view).Write(FormDot) + if err != nil { + t.Fatalf("write %s: %v", view, err) + } + return string(out) + } + selective := dot("selective") + for _, row := range []string{"resolution", "exposure", "mount", "data", "focus"} { + if !strings.Contains(selective, row) { + t.Errorf("selective lacks the row %s of a compartment left on", row) + } + } + for _, row := range []string{"lens", "sensor", "capture", "frame", ">items<", ">actions<"} { + if strings.Contains(selective, row) { + t.Errorf("selective lists %s, which its Layout hides", row) + } + } + if strings.Contains(selective, "compartment row(s) with no room") { + t.Errorf("selective reports clipped rows:\n%s", selective) + } + collapsed := dot("collapsed") + for _, text := range []string{"resolution", "mount", "data", "focus", "capture", "frame", "compartment row(s) with no room"} { + if strings.Contains(collapsed, text) { + t.Errorf("collapsed writes %q, though every compartment is hidden", text) + } + } + cramped := dot("cramped") + if !strings.Contains(cramped, "7 compartment row(s) with no room in the stated size of their node's box, left unwritten") { + t.Errorf("cramped does not report the 7 rows of the compartments left on as clipped:\n%s", cramped) + } + if strings.Contains(cramped, "lens") || strings.Contains(cramped, "sensor") { + t.Errorf("cramped lists a hidden part:\n%s", cramped) + } + roomy := dot("roomy") + for _, row := range []string{"resolution", "exposure", "mount", "data", "focus", "capture", "frame"} { + if !strings.Contains(roomy, row) { + t.Errorf("roomy lacks the row %s of a compartment left on", row) + } + } + if strings.Contains(roomy, "lens") || strings.Contains(roomy, "sensor") || strings.Contains(roomy, "compartment row(s) with no room") { + t.Errorf("roomy lists a hidden part or clips:\n%s", roomy) + } + bare := dot("bare") + for _, text := range []string{"resolution", "mount", "data", "lens", "compartment row(s)"} { + if strings.Contains(bare, text) { + t.Errorf("bare writes %q under a rendering without compartments", text) + } + } +} diff --git a/internal/ir/view/testdata/suppressed-compartments-bare.d2.golden b/internal/ir/view/testdata/suppressed-compartments-bare.d2.golden new file mode 100644 index 0000000000..27fd5e2490 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-bare.d2.golden @@ -0,0 +1,8 @@ +# CameraViews::bare — tree rendering (render bareTree) +# not represented: 9 node(s) without a position, left undrawn +# not represented: 1 positioned node(s) and 0 route(s) kept as comments; D2 pins no position, the dot form does +classes: { + definition: { style: { fill: white; stroke: "#181818"; stroke-width: 1; font-color: black; font-size: 14 } } +} +# layout: n0 x=10 y=10 w=240 h=260 +n0: "«part def»\nImaging" { class: definition } diff --git a/internal/ir/view/testdata/suppressed-compartments-bare.dot.golden b/internal/ir/view/testdata/suppressed-compartments-bare.dot.golden new file mode 100644 index 0000000000..4133031ad6 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-bare.dot.golden @@ -0,0 +1,11 @@ +// view: CameraViews::bare +// kind: tree +// stated: render bareTree +// not represented: 9 node(s) without a position, left undrawn +// layout: neato -n +digraph "CameraViews::bare" { + graph [fontname="Helvetica", layout=neato, 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]; + "n0" [label=<«part def»
Imaging>, margin=0, pos="130,-140!", pin=true, width=3.3333333333333335, height=3.611111111111111, fixedsize=true]; +} diff --git a/internal/ir/view/testdata/suppressed-compartments-bare.mermaid.golden b/internal/ir/view/testdata/suppressed-compartments-bare.mermaid.golden new file mode 100644 index 0000000000..43940f796f --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-bare.mermaid.golden @@ -0,0 +1,29 @@ +--- +config: + fontFamily: "Helvetica, Arial, sans-serif" + theme: base + themeCSS: ".edgeLabel rect { opacity: 1 !important; } .cluster-label .nodeLabel { text-align: center; }" + themeVariables: + fontFamily: "Helvetica, Arial, sans-serif" + fontSize: "14px" + primaryColor: "#FFFFFF" + secondaryColor: "#FFFFFF" + tertiaryColor: "#FFFFFF" + background: "#FFFFFF" + primaryBorderColor: "#181818" + primaryTextColor: "#000000" + lineColor: "#181818" + textColor: "#000000" + noteBkgColor: "#FEFFDD" + noteBorderColor: "#181818" + noteTextColor: "#000000" + clusterBkg: "#FFFFFF" + clusterBorder: "#181818" + edgeLabelBackground: "#FFFFFF" +--- +%% CameraViews::bare — tree rendering (render bareTree) +%% not represented: 9 node(s) without a position, left undrawn +%% layout: n0 x=10 y=10 w=240 h=260 +flowchart TD + n0["`*«part def»* +**Imaging**`"] diff --git a/internal/ir/view/testdata/suppressed-compartments-bare.plantuml.golden b/internal/ir/view/testdata/suppressed-compartments-bare.plantuml.golden new file mode 100644 index 0000000000..ef87673d28 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-bare.plantuml.golden @@ -0,0 +1,49 @@ +@startuml +' CameraViews::bare — tree rendering (render bareTree) +' not represented: 9 node(s) without a position, left undrawn +' not represented: 1 positioned node(s) and 0 route(s) kept as comments; PlantUML pins no position, the dot form does + +skinparam wrapWidth 300 +hide stereotype +' layout: n0 x=10 y=10 w=240 h=260 +hide circle +hide empty members +class "//«part def»//\n**Imaging**" as n0 <> +@enduml diff --git a/internal/ir/view/testdata/suppressed-compartments-bare.text.golden b/internal/ir/view/testdata/suppressed-compartments-bare.text.golden new file mode 100644 index 0000000000..1bbf4b44b4 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-bare.text.golden @@ -0,0 +1,12 @@ +CameraViews::bare - tree rendering (render bareTree) + +part def Imaging at (10, 10) size 240×260 + attribute resolution + attribute exposure + part lens + part sensor + part mount + port data + constraint focus + action capture + item 'frame' diff --git a/internal/ir/view/testdata/suppressed-compartments-collapsed.d2.golden b/internal/ir/view/testdata/suppressed-compartments-collapsed.d2.golden new file mode 100644 index 0000000000..351e6e5590 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-collapsed.d2.golden @@ -0,0 +1,8 @@ +# CameraViews::collapsed — tree rendering (render asTreeDiagram) +# not represented: 9 node(s) without a position: 0 drawn as compartment rows of the node above them, 9 in compartments its Layout hides left undrawn +# not represented: 1 positioned node(s) and 0 route(s) kept as comments; D2 pins no position, the dot form does +classes: { + definition: { style: { fill: white; stroke: "#181818"; stroke-width: 1; font-color: black; font-size: 14 } } +} +# layout: n0 x=10 y=10 w=200 h=33 +n0: "«part def»\nImaging" { class: definition } diff --git a/internal/ir/view/testdata/suppressed-compartments-collapsed.dot.golden b/internal/ir/view/testdata/suppressed-compartments-collapsed.dot.golden new file mode 100644 index 0000000000..0b1431eb7e --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-collapsed.dot.golden @@ -0,0 +1,11 @@ +// view: CameraViews::collapsed +// kind: tree +// stated: render asTreeDiagram +// not represented: 9 node(s) without a position: 0 drawn as compartment rows of the node above them, 9 in compartments its Layout hides left undrawn +// layout: neato -n +digraph "CameraViews::collapsed" { + graph [fontname="Helvetica", layout=neato, 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]; + "n0" [label=<«part def»
Imaging>, margin=0, pos="110,-26.5!", pin=true, width=2.7777777777777777, height=0.4583333333333333, fixedsize=true]; +} diff --git a/internal/ir/view/testdata/suppressed-compartments-collapsed.mermaid.golden b/internal/ir/view/testdata/suppressed-compartments-collapsed.mermaid.golden new file mode 100644 index 0000000000..8cb2a1cf31 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-collapsed.mermaid.golden @@ -0,0 +1,29 @@ +--- +config: + fontFamily: "Helvetica, Arial, sans-serif" + theme: base + themeCSS: ".edgeLabel rect { opacity: 1 !important; } .cluster-label .nodeLabel { text-align: center; }" + themeVariables: + fontFamily: "Helvetica, Arial, sans-serif" + fontSize: "14px" + primaryColor: "#FFFFFF" + secondaryColor: "#FFFFFF" + tertiaryColor: "#FFFFFF" + background: "#FFFFFF" + primaryBorderColor: "#181818" + primaryTextColor: "#000000" + lineColor: "#181818" + textColor: "#000000" + noteBkgColor: "#FEFFDD" + noteBorderColor: "#181818" + noteTextColor: "#000000" + clusterBkg: "#FFFFFF" + clusterBorder: "#181818" + edgeLabelBackground: "#FFFFFF" +--- +%% CameraViews::collapsed — tree rendering (render asTreeDiagram) +%% not represented: 9 node(s) without a position: 0 drawn as compartment rows of the node above them, 9 in compartments its Layout hides left undrawn +%% layout: n0 x=10 y=10 w=200 h=33 +flowchart TD + n0["`*«part def»* +**Imaging**`"] diff --git a/internal/ir/view/testdata/suppressed-compartments-collapsed.plantuml.golden b/internal/ir/view/testdata/suppressed-compartments-collapsed.plantuml.golden new file mode 100644 index 0000000000..31bf1d5c28 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-collapsed.plantuml.golden @@ -0,0 +1,49 @@ +@startuml +' CameraViews::collapsed — tree rendering (render asTreeDiagram) +' not represented: 9 node(s) without a position: 0 drawn as compartment rows of the node above them, 9 in compartments its Layout hides left undrawn +' not represented: 1 positioned node(s) and 0 route(s) kept as comments; PlantUML pins no position, the dot form does + +skinparam wrapWidth 300 +hide stereotype +' layout: n0 x=10 y=10 w=200 h=33 +hide circle +hide empty members +class "//«part def»//\n**Imaging**" as n0 <> +@enduml diff --git a/internal/ir/view/testdata/suppressed-compartments-collapsed.text.golden b/internal/ir/view/testdata/suppressed-compartments-collapsed.text.golden new file mode 100644 index 0000000000..b0bb249589 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-collapsed.text.golden @@ -0,0 +1,12 @@ +CameraViews::collapsed - tree rendering (render asTreeDiagram) + +part def Imaging at (10, 10) size 200×33 + attribute resolution + attribute exposure + part lens + part sensor + part mount + port data + constraint focus + action capture + item 'frame' diff --git a/internal/ir/view/testdata/suppressed-compartments-cramped.d2.golden b/internal/ir/view/testdata/suppressed-compartments-cramped.d2.golden new file mode 100644 index 0000000000..f20bb57bff --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-cramped.d2.golden @@ -0,0 +1,8 @@ +# CameraViews::cramped — tree rendering (render asTreeDiagram) +# not represented: 9 node(s) without a position: 7 drawn as compartment rows of the node above them, 2 in compartments its Layout hides left undrawn +# not represented: 1 positioned node(s) and 0 route(s) kept as comments; D2 pins no position, the dot form does +classes: { + definition: { style: { fill: white; stroke: "#181818"; stroke-width: 1; font-color: black; font-size: 14 } } +} +# layout: n0 x=10 y=10 w=200 h=33 +n0: "«part def»\nImaging\nattributes\nresolution = 4096\nexposure\nitems\n'frame'\nparts\nmount\nports\ndata\nactions\ncapture\nconstraints\nfocus" { class: definition } diff --git a/internal/ir/view/testdata/suppressed-compartments-cramped.dot.golden b/internal/ir/view/testdata/suppressed-compartments-cramped.dot.golden new file mode 100644 index 0000000000..209f930f50 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-cramped.dot.golden @@ -0,0 +1,12 @@ +// view: CameraViews::cramped +// kind: tree +// stated: render asTreeDiagram +// not represented: 9 node(s) without a position: 7 drawn as compartment rows of the node above them, 2 in compartments its Layout hides left undrawn +// not represented: 7 compartment row(s) with no room in the stated size of their node's box, left unwritten +// layout: neato -n +digraph "CameraViews::cramped" { + graph [fontname="Helvetica", layout=neato, 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]; + "n0" [label=<«part def»
Imaging>, margin=0, pos="110,-26.5!", pin=true, width=2.7777777777777777, height=0.4583333333333333, fixedsize=true]; +} diff --git a/internal/ir/view/testdata/suppressed-compartments-cramped.mermaid.golden b/internal/ir/view/testdata/suppressed-compartments-cramped.mermaid.golden new file mode 100644 index 0000000000..517549fa15 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-cramped.mermaid.golden @@ -0,0 +1,42 @@ +--- +config: + fontFamily: "Helvetica, Arial, sans-serif" + theme: base + themeCSS: ".edgeLabel rect { opacity: 1 !important; } .cluster-label .nodeLabel { text-align: center; }" + themeVariables: + fontFamily: "Helvetica, Arial, sans-serif" + fontSize: "14px" + primaryColor: "#FFFFFF" + secondaryColor: "#FFFFFF" + tertiaryColor: "#FFFFFF" + background: "#FFFFFF" + primaryBorderColor: "#181818" + primaryTextColor: "#000000" + lineColor: "#181818" + textColor: "#000000" + noteBkgColor: "#FEFFDD" + noteBorderColor: "#181818" + noteTextColor: "#000000" + clusterBkg: "#FFFFFF" + clusterBorder: "#181818" + edgeLabelBackground: "#FFFFFF" +--- +%% CameraViews::cramped — tree rendering (render asTreeDiagram) +%% not represented: 9 node(s) without a position: 7 drawn as compartment rows of the node above them, 2 in compartments its Layout hides left undrawn +%% layout: n0 x=10 y=10 w=200 h=33 +flowchart TD + n0["`*«part def»* +**Imaging** +attributes +resolution = 4096 +exposure +items +'frame' +parts +mount +ports +data +actions +capture +constraints +focus`"] diff --git a/internal/ir/view/testdata/suppressed-compartments-cramped.plantuml.golden b/internal/ir/view/testdata/suppressed-compartments-cramped.plantuml.golden new file mode 100644 index 0000000000..6d45ae62b7 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-cramped.plantuml.golden @@ -0,0 +1,49 @@ +@startuml +' CameraViews::cramped — tree rendering (render asTreeDiagram) +' not represented: 9 node(s) without a position: 7 drawn as compartment rows of the node above them, 2 in compartments its Layout hides left undrawn +' not represented: 1 positioned node(s) and 0 route(s) kept as comments; PlantUML pins no position, the dot form does + +skinparam wrapWidth 300 +hide stereotype +' layout: n0 x=10 y=10 w=200 h=33 +hide circle +hide empty members +class "//«part def»//\n**Imaging**\nattributes\nresolution = 4096\nexposure\nitems\n'frame'\nparts\nmount\nports\ndata\nactions\ncapture\nconstraints\nfocus" as n0 <> +@enduml diff --git a/internal/ir/view/testdata/suppressed-compartments-cramped.text.golden b/internal/ir/view/testdata/suppressed-compartments-cramped.text.golden new file mode 100644 index 0000000000..2fe07aa603 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-cramped.text.golden @@ -0,0 +1,12 @@ +CameraViews::cramped - tree rendering (render asTreeDiagram) + +part def Imaging at (10, 10) size 200×33 + attribute resolution + attribute exposure + part lens + part sensor + part mount + port data + constraint focus + action capture + item 'frame' diff --git a/internal/ir/view/testdata/suppressed-compartments-roomy.d2.golden b/internal/ir/view/testdata/suppressed-compartments-roomy.d2.golden new file mode 100644 index 0000000000..79186e5c56 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-roomy.d2.golden @@ -0,0 +1,8 @@ +# CameraViews::roomy — tree rendering (render asTreeDiagram) +# not represented: 9 node(s) without a position: 7 drawn as compartment rows of the node above them, 2 in compartments its Layout hides left undrawn +# not represented: 1 positioned node(s) and 0 route(s) kept as comments; D2 pins no position, the dot form does +classes: { + definition: { style: { fill: white; stroke: "#181818"; stroke-width: 1; font-color: black; font-size: 14 } } +} +# layout: n0 x=10 y=10 w=300 h=420 +n0: "«part def»\nImaging\nattributes\nresolution = 4096\nexposure\nitems\n'frame'\nparts\nmount\nports\ndata\nactions\ncapture\nconstraints\nfocus" { class: definition } diff --git a/internal/ir/view/testdata/suppressed-compartments-roomy.dot.golden b/internal/ir/view/testdata/suppressed-compartments-roomy.dot.golden new file mode 100644 index 0000000000..7d2d96d448 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-roomy.dot.golden @@ -0,0 +1,11 @@ +// view: CameraViews::roomy +// kind: tree +// stated: render asTreeDiagram +// not represented: 9 node(s) without a position: 7 drawn as compartment rows of the node above them, 2 in compartments its Layout hides left undrawn +// layout: neato -n +digraph "CameraViews::roomy" { + graph [fontname="Helvetica", layout=neato, 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]; + "n0" [label=<





«part def»
Imaging
attributes
resolution = 4096
exposure
items
'frame'
parts
mount
ports
data
actions
capture
constraints
focus
>, margin=0, pos="160,-220!", pin=true, width=4.166666666666667, height=5.833333333333333, fixedsize=true]; +} diff --git a/internal/ir/view/testdata/suppressed-compartments-roomy.mermaid.golden b/internal/ir/view/testdata/suppressed-compartments-roomy.mermaid.golden new file mode 100644 index 0000000000..1431e4ea9d --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-roomy.mermaid.golden @@ -0,0 +1,42 @@ +--- +config: + fontFamily: "Helvetica, Arial, sans-serif" + theme: base + themeCSS: ".edgeLabel rect { opacity: 1 !important; } .cluster-label .nodeLabel { text-align: center; }" + themeVariables: + fontFamily: "Helvetica, Arial, sans-serif" + fontSize: "14px" + primaryColor: "#FFFFFF" + secondaryColor: "#FFFFFF" + tertiaryColor: "#FFFFFF" + background: "#FFFFFF" + primaryBorderColor: "#181818" + primaryTextColor: "#000000" + lineColor: "#181818" + textColor: "#000000" + noteBkgColor: "#FEFFDD" + noteBorderColor: "#181818" + noteTextColor: "#000000" + clusterBkg: "#FFFFFF" + clusterBorder: "#181818" + edgeLabelBackground: "#FFFFFF" +--- +%% CameraViews::roomy — tree rendering (render asTreeDiagram) +%% not represented: 9 node(s) without a position: 7 drawn as compartment rows of the node above them, 2 in compartments its Layout hides left undrawn +%% layout: n0 x=10 y=10 w=300 h=420 +flowchart TD + n0["`*«part def»* +**Imaging** +attributes +resolution = 4096 +exposure +items +'frame' +parts +mount +ports +data +actions +capture +constraints +focus`"] diff --git a/internal/ir/view/testdata/suppressed-compartments-roomy.plantuml.golden b/internal/ir/view/testdata/suppressed-compartments-roomy.plantuml.golden new file mode 100644 index 0000000000..4f7457e144 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-roomy.plantuml.golden @@ -0,0 +1,49 @@ +@startuml +' CameraViews::roomy — tree rendering (render asTreeDiagram) +' not represented: 9 node(s) without a position: 7 drawn as compartment rows of the node above them, 2 in compartments its Layout hides left undrawn +' not represented: 1 positioned node(s) and 0 route(s) kept as comments; PlantUML pins no position, the dot form does + +skinparam wrapWidth 300 +hide stereotype +' layout: n0 x=10 y=10 w=300 h=420 +hide circle +hide empty members +class "//«part def»//\n**Imaging**\nattributes\nresolution = 4096\nexposure\nitems\n'frame'\nparts\nmount\nports\ndata\nactions\ncapture\nconstraints\nfocus" as n0 <> +@enduml diff --git a/internal/ir/view/testdata/suppressed-compartments-roomy.text.golden b/internal/ir/view/testdata/suppressed-compartments-roomy.text.golden new file mode 100644 index 0000000000..d1539986ac --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-roomy.text.golden @@ -0,0 +1,12 @@ +CameraViews::roomy - tree rendering (render asTreeDiagram) + +part def Imaging at (10, 10) size 300×420 + attribute resolution + attribute exposure + part lens + part sensor + part mount + port data + constraint focus + action capture + item 'frame' diff --git a/internal/ir/view/testdata/suppressed-compartments-selective.d2.golden b/internal/ir/view/testdata/suppressed-compartments-selective.d2.golden new file mode 100644 index 0000000000..83188855e3 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-selective.d2.golden @@ -0,0 +1,8 @@ +# CameraViews::selective — tree rendering (render asTreeDiagram) +# not represented: 9 node(s) without a position: 5 drawn as compartment rows of the node above them, 4 in compartments its Layout hides left undrawn +# not represented: 1 positioned node(s) and 0 route(s) kept as comments; D2 pins no position, the dot form does +classes: { + definition: { style: { fill: white; stroke: "#181818"; stroke-width: 1; font-color: black; font-size: 14 } } +} +# layout: n0 x=10 y=10 w=240 h=220 +n0: "«part def»\nImaging\nattributes\nresolution = 4096\nexposure\nparts\nmount\nports\ndata\nconstraints\nfocus" { class: definition } diff --git a/internal/ir/view/testdata/suppressed-compartments-selective.dot.golden b/internal/ir/view/testdata/suppressed-compartments-selective.dot.golden new file mode 100644 index 0000000000..5ea3e88ab2 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-selective.dot.golden @@ -0,0 +1,11 @@ +// view: CameraViews::selective +// kind: tree +// stated: render asTreeDiagram +// not represented: 9 node(s) without a position: 5 drawn as compartment rows of the node above them, 4 in compartments its Layout hides left undrawn +// layout: neato -n +digraph "CameraViews::selective" { + graph [fontname="Helvetica", layout=neato, 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]; + "n0" [label=<



«part def»
Imaging
attributes
resolution = 4096
exposure
parts
mount
ports
data
constraints
focus
>, margin=0, pos="130,-120!", pin=true, width=3.3333333333333335, height=3.0555555555555554, fixedsize=true]; +} diff --git a/internal/ir/view/testdata/suppressed-compartments-selective.mermaid.golden b/internal/ir/view/testdata/suppressed-compartments-selective.mermaid.golden new file mode 100644 index 0000000000..3d4e794b63 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-selective.mermaid.golden @@ -0,0 +1,38 @@ +--- +config: + fontFamily: "Helvetica, Arial, sans-serif" + theme: base + themeCSS: ".edgeLabel rect { opacity: 1 !important; } .cluster-label .nodeLabel { text-align: center; }" + themeVariables: + fontFamily: "Helvetica, Arial, sans-serif" + fontSize: "14px" + primaryColor: "#FFFFFF" + secondaryColor: "#FFFFFF" + tertiaryColor: "#FFFFFF" + background: "#FFFFFF" + primaryBorderColor: "#181818" + primaryTextColor: "#000000" + lineColor: "#181818" + textColor: "#000000" + noteBkgColor: "#FEFFDD" + noteBorderColor: "#181818" + noteTextColor: "#000000" + clusterBkg: "#FFFFFF" + clusterBorder: "#181818" + edgeLabelBackground: "#FFFFFF" +--- +%% CameraViews::selective — tree rendering (render asTreeDiagram) +%% not represented: 9 node(s) without a position: 5 drawn as compartment rows of the node above them, 4 in compartments its Layout hides left undrawn +%% layout: n0 x=10 y=10 w=240 h=220 +flowchart TD + n0["`*«part def»* +**Imaging** +attributes +resolution = 4096 +exposure +parts +mount +ports +data +constraints +focus`"] diff --git a/internal/ir/view/testdata/suppressed-compartments-selective.plantuml.golden b/internal/ir/view/testdata/suppressed-compartments-selective.plantuml.golden new file mode 100644 index 0000000000..5e9f0ea5d4 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-selective.plantuml.golden @@ -0,0 +1,49 @@ +@startuml +' CameraViews::selective — tree rendering (render asTreeDiagram) +' not represented: 9 node(s) without a position: 5 drawn as compartment rows of the node above them, 4 in compartments its Layout hides left undrawn +' not represented: 1 positioned node(s) and 0 route(s) kept as comments; PlantUML pins no position, the dot form does + +skinparam wrapWidth 300 +hide stereotype +' layout: n0 x=10 y=10 w=240 h=220 +hide circle +hide empty members +class "//«part def»//\n**Imaging**\nattributes\nresolution = 4096\nexposure\nparts\nmount\nports\ndata\nconstraints\nfocus" as n0 <> +@enduml diff --git a/internal/ir/view/testdata/suppressed-compartments-selective.text.golden b/internal/ir/view/testdata/suppressed-compartments-selective.text.golden new file mode 100644 index 0000000000..787e40d542 --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments-selective.text.golden @@ -0,0 +1,12 @@ +CameraViews::selective - tree rendering (render asTreeDiagram) + +part def Imaging at (10, 10) size 240×220 + attribute resolution + attribute exposure + part lens + part sensor + part mount + port data + constraint focus + action capture + item 'frame' diff --git a/internal/ir/view/testdata/suppressed-compartments.sysml b/internal/ir/view/testdata/suppressed-compartments.sysml new file mode 100644 index 0000000000..5db2cb6f2d --- /dev/null +++ b/internal/ir/view/testdata/suppressed-compartments.sysml @@ -0,0 +1,88 @@ +// A block definition diagram migrated from a tool that let the modeler +// collapse compartments symbol by symbol: each Layout keeps the box the tool +// sized for the compartments left visible and turns the others off with its +// show… switches. A hidden compartment is neither listed nor counted as +// clipped; one left on keeps the box's fit and clipping rules. +package Camera { + private import ScalarValues::Real; + + attribute def Pixels :> Real; + port def DataPort; + part def Lens; + part def Sensor; + part def Mount; + constraint def Focus; + item def Frame; + + part def Imaging { + attribute resolution : Pixels = 4096; + attribute exposure : Real; + part lens : Lens; + part sensor : Sensor; + ref part mount : Mount; + port data : DataPort; + constraint focus : Focus; + action capture; + item frame : Frame; + } +} + +package CameraViews { + private import Views::*; + private import DiagramLayout::*; + + // The box lists attributes, ports, constraints and the ref part; the + // parts, items and actions the modeler collapsed are not listed, and the + // box, sized for the rest, clips nothing. + view selective { + expose Camera::Imaging; + metadata Layout about Camera::Imaging { + x = 10; y = 10; width = 240; height = 220; + showParts = false; showItems = false; showActions = false; + } + render asTreeDiagram; + } + + // Every compartment collapsed: the 200×33 bar lists nothing and reports + // nothing unwritten. + view collapsed { + expose Camera::Imaging; + metadata Layout about Camera::Imaging { + x = 10; y = 10; width = 200; height = 33; + showAttributes = false; showParts = false; showReferences = false; showPorts = false; + showConstraints = false; showItems = false; showActions = false; + } + render asTreeDiagram; + } + + // Parts collapsed in a bar sized for nothing more: the compartments left + // on do not fit and are reported, the parts are not. + view cramped { + expose Camera::Imaging; + metadata Layout about Camera::Imaging { + x = 10; y = 10; width = 200; height = 33; + showParts = false; + } + render asTreeDiagram; + } + + // Parts collapsed in a box with room for every row: the parts are left + // out of a box that would have listed them, nothing is clipped. + view roomy { + expose Camera::Imaging; + metadata Layout about Camera::Imaging { x = 10; y = 10; width = 300; height = 420; showParts = false; } + render asTreeDiagram; + } + + // The tree rendering without compartments. + rendering bareTree :> Views::asTreeDiagram { + attribute showCompartments : ScalarValues::Boolean default = false; + } + + // The rendering's switch wins: nothing is listed though only parts are off. + view bare { + expose Camera::Imaging; + metadata Layout about Camera::Imaging { x = 10; y = 10; width = 240; height = 260; showParts = false; } + render bareTree; + } +} diff --git a/internal/ir/view/tree.go b/internal/ir/view/tree.go index 6c1fa50b8d..3469a14a6b 100644 --- a/internal/ir/view/tree.go +++ b/internal/ir/view/tree.go @@ -160,7 +160,7 @@ func (r *Renderer) treeNodeShallow(view, sym *symbols.Symbol, ids *nodeIDs, seen } node := &Node{ID: ids.take(), Kind: declKind(sym), Name: name, NameSynthesized: r.model.NameSynthesized(sym), Type: declType(sym), Typings: r.declTypings(sym), Value: r.declValue(sym), Origin: symbolOrigin(sym), Geometry: r.geometryOf(view, sym, out), - Style: r.styleOf(view, sym, out)} + Style: r.styleOf(view, sym, out), Referential: referenceRow(sym)} out.noteTreeSite(node, view, sym) if seen[sym] { node.Detail = detailWith(node.Detail, "already shown") diff --git a/internal/ir/view/view.go b/internal/ir/view/view.go index 81c77566a2..90abd88efc 100644 --- a/internal/ir/view/view.go +++ b/internal/ir/view/view.go @@ -279,6 +279,9 @@ type Node struct { // NameSynthesized marks a name the model did not give: a migration made it up, or // it is the language's `start`/`done`. A name to key by, not one a picture shows. NameSynthesized bool + // Referential marks a structural usage that refers to its value rather + // than owning it (`ref part`): a row of a tool's references compartment. + Referential bool // StandIn marks a node a migration made up that stands for no element of its // source, so no diagram symbol is at it: a join it wrote several edges through. StandIn bool diff --git a/internal/semantic/semantics/layout.go b/internal/semantic/semantics/layout.go index bb3f37d825..f47af635bb 100644 --- a/internal/semantic/semantics/layout.go +++ b/internal/semantic/semantics/layout.go @@ -38,6 +38,56 @@ type Layout struct { Width, Height float64 HasSize bool Collapsed bool + // Hidden are the compartments the annotation's show… switches turn off + // for the element's symbol. + Hidden HiddenCompartments +} + +// HiddenCompartments are the compartments of one symbol a Layout's show… +// switches turn off, one per switch bound false: showAttributes, showParts, +// showReferences, showPorts, showConstraints, showItems, showActions. +type HiddenCompartments struct { + Attributes, Parts, References, Ports, Constraints, Items, Actions bool +} + +// Any reports whether any compartment is hidden. +func (h HiddenCompartments) Any() bool { + return h != HiddenCompartments{} +} + +// Hides reports whether the compartment named as the graphical notation +// heads it ("attributes", "parts", "ports", …) is hidden; "references" is the +// `ref` rows, whichever compartment lists them. +func (h HiddenCompartments) Hides(compartment string) bool { + switch compartment { + case "attributes": + return h.Attributes + case "parts": + return h.Parts + case "references": + return h.References + case "ports": + return h.Ports + case "constraints": + return h.Constraints + case "items": + return h.Items + case "actions": + return h.Actions + } + return false +} + +// layoutSwitches are Layout's per-compartment show… attributes and the +// compartment each hides when bound false. +var layoutSwitches = map[string]func(*HiddenCompartments) *bool{ + "showAttributes": func(h *HiddenCompartments) *bool { return &h.Attributes }, + "showParts": func(h *HiddenCompartments) *bool { return &h.Parts }, + "showReferences": func(h *HiddenCompartments) *bool { return &h.References }, + "showPorts": func(h *HiddenCompartments) *bool { return &h.Ports }, + "showConstraints": func(h *HiddenCompartments) *bool { return &h.Constraints }, + "showItems": func(h *HiddenCompartments) *bool { return &h.Items }, + "showActions": func(h *HiddenCompartments) *bool { return &h.Actions }, } // Waypoint is one point of a Route, in the units of Layout. @@ -540,7 +590,7 @@ func sameElement(a, b *symbols.Symbol) bool { } // readLayout reads a Layout body: x and y are required reals, width and -// height an optional pair, collapsed an optional boolean. +// height an optional pair, collapsed and the show… switches optional booleans. func (m *Model) readLayout(site *LayoutSite, bindings []MetadataBinding) { layout := &Layout{} var hasX, hasY, hasWidth, hasHeight bool @@ -556,6 +606,13 @@ func (m *Model) readLayout(site *LayoutSite, bindings []MetadataBinding) { hasHeight = m.readReal(site, b, &layout.Height) case "collapsed": m.readBool(site, b, &layout.Collapsed) + default: + if hidden, ok := layoutSwitches[b.Feature]; ok { + show := true + if m.readBool(site, b, &show) && !show { + *hidden(&layout.Hidden) = true + } + } } } if !bindsFeature(bindings, "x") || !bindsFeature(bindings, "y") { diff --git a/internal/translate/migrate/compartments.go b/internal/translate/migrate/compartments.go new file mode 100644 index 0000000000..8a97529682 --- /dev/null +++ b/internal/translate/migrate/compartments.go @@ -0,0 +1,68 @@ +package migrate + +import ( + "sort" + + "github.com/Open-MBEE/OpenSysML/internal/translate/xmi/sysmlv1" +) + +// compartmentKinds maps a Cameo symbol's SUPPRESS_* compartment property to +// the DiagramLayout::Layout show… switch of the v2 compartment its members +// become: parts → part usages, references → ref usages, properties and values +// (and a class's attributes) → attributes, constraints → constraint usages, +// flow properties → item features, proxy, full and class ports → port usages, +// operations → action usages. The IBD_ properties are a part symbol's +// compartments on an internal block diagram and map the same way. +var compartmentKinds = map[string]string{ + "SUPPRESS_PARTS_COMPARTMENT": "Parts", + "SUPPRESS_IBD_PARTS_COMPARTMENT": "Parts", + "SUPPRESS_REFERENCES_COMPARTMENT": "References", + "SUPPRESS_IBD_REFERENCES_COMPARTMENT": "References", + "SUPPRESS_PROPERTIES_COMPARTMENT": "Attributes", + "SUPPRESS_VALUES_COMPARTMENT": "Attributes", + "SUPPRESS_CLASS_ATTRIBUTES": "Attributes", + "SUPPRESS_IBD_VALUES_COMPARTMENT": "Attributes", + "SUPPRESS_IBD_UMLPROPS_COMPARTMENT": "Attributes", + "SUPPRESS_IBD_INITIALVALUES_COMPARTMENT": "Attributes", + "SUPPRESS_CONSTRAINTS_COMPARTMENT": "Constraints", + "SUPPRESS_IBD_CONSTRAINTS_COMPARTMENT": "Constraints", + "SUPPRESS_FLOWPROPERTIES_COMPARTMENT": "Items", + "SUPPRESS_IBD_FLOWPROPERTIES_COMPARTMENT": "Items", + "SUPPRESS_PROXYPORTS_COMPARTMENT": "Ports", + "SUPPRESS_FULLPORTS_COMPARTMENT": "Ports", + "SUPPRESS_CLASS_PORTS": "Ports", + "SUPPRESS_CLASS_OPERATIONS": "Actions", + "SUPPRESS_IBD_OPERATIONS_COMPARTMENT": "Actions", +} + +// hiddenCompartments names the v2 compartment kinds sym suppresses, sorted. A +// v2 compartment lists what several Cameo compartments list between them (the +// class attributes and the values compartments both become attributes), so a +// kind is hidden when every Cameo compartment the symbol states for it is +// suppressed. The suppressed compartments no kind maps to are named apart, +// sorted, for the report's unsupported presentation properties. +func hiddenCompartments(sym *sysmlv1.Symbol) (kinds, unsupported []string) { + shown := map[string]bool{} + stated := map[string]bool{} + for id, suppressed := range sym.Compartments { + kind, ok := compartmentKinds[id] + if !ok { + if suppressed { + unsupported = append(unsupported, id) + } + continue + } + stated[kind] = true + if !suppressed { + shown[kind] = true + } + } + for kind := range stated { + if !shown[kind] { + kinds = append(kinds, kind) + } + } + sort.Strings(kinds) + sort.Strings(unsupported) + return kinds, unsupported +} diff --git a/internal/translate/migrate/compartments_internal_test.go b/internal/translate/migrate/compartments_internal_test.go new file mode 100644 index 0000000000..0bfb391b0d --- /dev/null +++ b/internal/translate/migrate/compartments_internal_test.go @@ -0,0 +1,34 @@ +package migrate + +import ( + "reflect" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/translate/xmi/sysmlv1" +) + +func TestHiddenCompartments(t *testing.T) { + sym := &sysmlv1.Symbol{Compartments: map[string]bool{ + "SUPPRESS_PARTS_COMPARTMENT": false, + "SUPPRESS_VALUES_COMPARTMENT": true, + "SUPPRESS_CLASS_ATTRIBUTES": true, + "SUPPRESS_PROXYPORTS_COMPARTMENT": true, + "SUPPRESS_FULLPORTS_COMPARTMENT": false, + "SUPPRESS_FLOWPROPERTIES_COMPARTMENT": true, + "SUPPRESS_STRUCTURE": true, + "SUPPRESS_BEHAVIORS": false, + }} + // Values and class attributes both suppressed hide the attributes; a + // proxy ports compartment suppressed beside a full ports one shown leaves + // the ports listed; parts shown stay. + hidden, unsupported := hiddenCompartments(sym) + if want := []string{"Attributes", "Items"}; !reflect.DeepEqual(hidden, want) { + t.Errorf("hidden = %q, want %q", hidden, want) + } + if want := []string{"SUPPRESS_STRUCTURE"}; !reflect.DeepEqual(unsupported, want) { + t.Errorf("unsupported = %q, want %q", unsupported, want) + } + if hidden, unsupported := hiddenCompartments(&sysmlv1.Symbol{}); len(hidden) != 0 || len(unsupported) != 0 { + t.Errorf("a symbol without settings hides %q, unsupported %q; want none", hidden, unsupported) + } +} diff --git a/internal/translate/migrate/diagrams.go b/internal/translate/migrate/diagrams.go index 9cdae3b314..3da0372291 100644 --- a/internal/translate/migrate/diagrams.go +++ b/internal/translate/migrate/diagrams.go @@ -1335,6 +1335,16 @@ func (p *layoutPlan) placements(rec *mtip.Diagram) []string { m.diagramLayoutAttributeForHost(p.prefix, "Layout", "width", layoutNumber(pl.Width), v.host, p.x), m.diagramLayoutAttributeForHost(p.prefix, "Layout", "height", layoutNumber(pl.Height), v.host, p.x), } + for _, kind := range pl.HiddenCompartments { + attrs = append(attrs, m.diagramLayoutAttributeForHost(p.prefix, "Layout", "show"+kind, "false", v.host, p.x)) + } + if len(pl.HiddenCompartments) > 0 { + s.CompartmentsSuppressed += len(pl.HiddenCompartments) + s.SymbolsSuppressing++ + } + for _, id := range pl.UnsupportedCompartments { + s.Unsupported[id]++ + } placements = append(placements, fmt.Sprintf("metadata %sLayout about %s { %s; }", p.prefix, ref, strings.Join(attrs, "; "))) p.grow(pl.X+pl.Width, pl.Y+pl.Height) diff --git a/internal/translate/migrate/migrate.go b/internal/translate/migrate/migrate.go index d22f17cb59..0689f106fc 100644 --- a/internal/translate/migrate/migrate.go +++ b/internal/translate/migrate/migrate.go @@ -1,5 +1,16 @@ // Package migrate writes a SysML v1 model, read from XMI, as SysML v2 textual // notation, and reports what each v1 element became. +// +// A diagram's symbols become a view's DiagramLayout annotations. The +// compartments a modeler collapsed on a symbol (its SUPPRESS_* properties, own +// or inherited from its symbol style) become the Layout's show… switches by +// the kind their members take in v2: parts → showParts, references → +// showReferences, properties, values and class attributes → showAttributes, +// constraints → showConstraints, flow properties → showItems, proxy, full and +// class ports → showPorts, operations → showActions (compartmentKinds). A kind +// is hidden when every Cameo compartment the symbol states for it is +// suppressed; a suppressed compartment with no v2 kind is reported as an +// unsupported presentation property. package migrate import ( diff --git a/internal/translate/migrate/report.go b/internal/translate/migrate/report.go index 296ce2029c..5f4236ba9e 100644 --- a/internal/translate/migrate/report.go +++ b/internal/translate/migrate/report.go @@ -142,6 +142,10 @@ type LayoutSummary struct { // StylesWritten those whose element the view lays out, so a Style is written. Styles int `json:"styles"` StylesWritten int `json:"stylesWritten"` + // CompartmentsSuppressed counts the compartment switches written, over + // SymbolsSuppressing placed symbols that suppress at least one compartment. + CompartmentsSuppressed int `json:"compartmentsSuppressed,omitempty"` + SymbolsSuppressing int `json:"symbolsSuppressing,omitempty"` // Notes counts the comments and text boxes drawn, every one written as a // Note; NotesAnchored those anchored to an element the view lays out, // NotesFreed those whose anchor reaches none, written free on the view. @@ -320,6 +324,9 @@ func (l *LayoutSummary) writeText(b *strings.Builder) { l.RoutesWritten, l.Routes, l.RoutesUnexposed, l.RoutesDangling, l.Malformed) fmt.Fprintf(b, "# styles: %d of %d written; notes: %d written (%d anchored, %d freed of an anchor the view does not lay out)\n", l.StylesWritten, l.Styles, l.Notes, l.NotesAnchored, l.NotesFreed) + if l.CompartmentsSuppressed > 0 { + fmt.Fprintf(b, "# compartments suppressed: %d on %d symbols\n", l.CompartmentsSuppressed, l.SymbolsSuppressing) + } if l.Pictures > 0 { l.writePicturesText(b) } diff --git a/internal/translate/migrate/streams.go b/internal/translate/migrate/streams.go index 28d70b6b13..e4e65f39bc 100644 --- a/internal/translate/migrate/streams.go +++ b/internal/translate/migrate/streams.go @@ -70,8 +70,10 @@ func (m *migration) streamRecord(d *sysmlv1.Diagram) *mtip.Diagram { rec.Connectors = append(rec.Connectors, c) case s.Bounds != nil: b := s.Bounds + hidden, unsupported := hiddenCompartments(s) rec.Placements = append(rec.Placements, mtip.Placement{ ID: s.ElementID, Type: s.Class, X: b.X, Y: b.Y, Width: b.Width, Height: b.Height, OnPath: s.OnPath(), + HiddenCompartments: hidden, UnsupportedCompartments: unsupported, }) } } @@ -116,10 +118,25 @@ func (m *migration) layoutRecord(v *view) (*mtip.Diagram, layoutSources) { onPath[p.ID] = p.OnPath } } + // Nor about suppressed compartments: the stream's settings stay with the + // export's box when the element's symbols agree on the hidden kinds. + compartments := map[string]mtip.Placement{} + disagree := map[string]bool{} + for _, p := range stream.Placements { + if first, ok := compartments[p.ID]; !ok { + compartments[p.ID] = p + } else if !slices.Equal(first.HiddenCompartments, p.HiddenCompartments) { + disagree[p.ID] = true + } + } for i := range merged.Placements { if onPath[merged.Placements[i].ID] { merged.Placements[i].OnPath = true } + if c, ok := compartments[merged.Placements[i].ID]; ok && !disagree[c.ID] { + merged.Placements[i].HiddenCompartments = c.HiddenCompartments + merged.Placements[i].UnsupportedCompartments = c.UnsupportedCompartments + } } routed := map[string]bool{} for _, c := range export.Connectors { diff --git a/internal/translate/mtip/mtip.go b/internal/translate/mtip/mtip.go index e8f0fd3660..f3e4377b6f 100644 --- a/internal/translate/mtip/mtip.go +++ b/internal/translate/mtip/mtip.go @@ -40,6 +40,10 @@ type Placement struct { // OnPath reports a shape the tool draws on a path symbol — an association // end or connector end at the line's end — rather than as a node of its own. OnPath bool + // HiddenCompartments names the compartment kinds the symbol suppresses + // (DiagramLayout's show… switches, sorted); only a symbol stream states them. + // UnsupportedCompartments are the suppressed compartments no kind answers to. + HiddenCompartments, UnsupportedCompartments []string } // Connector is one drawn edge's route, source to target, flattened as diff --git a/internal/translate/xmi/sysmlv1/stream.go b/internal/translate/xmi/sysmlv1/stream.go index 93b3bcd04d..2f7d705cbf 100644 --- a/internal/translate/xmi/sysmlv1/stream.go +++ b/internal/translate/xmi/sysmlv1/stream.go @@ -49,6 +49,11 @@ type Symbol struct { // Hidden reports a symbol the tool keeps but does not draw (visible false): // it takes no room on the diagram, and a hidden frame does not bound it. Hidden bool + // Compartments are the symbol's SUPPRESS_* compartment properties by + // property id, its own and then its symbol style's: true when suppressed. + Compartments map[string]bool + // StyleID names the symbol style the symbol is drawn in (symbolStyleID). + StyleID string } // Bounds is a rectangle in diagram pixels: its top-left corner and size. @@ -187,7 +192,7 @@ var symbolContainers = map[string]bool{"mdOwnedViews": true, "parts": true, "reg func readSymbols(data []byte, diagramID string) (*symbols, error) { dec := xml.NewDecoder(bytes.NewReader(data)) dec.Strict = false - r := &symbolReader{syms: &symbols{free: map[string]int{}}, diagramID: diagramID} + r := &symbolReader{syms: &symbols{free: map[string]int{}}, diagramID: diagramID, styles: map[string]*symbolStyle{}} for { tok, err := dec.RawToken() if err == io.EOF { @@ -217,6 +222,7 @@ func readSymbols(data []byte, diagramID string) (*symbols, error) { if len(r.stack) > 0 { return nil, errTornSymbols } + r.inherit() r.syms.settle(diagramID) return r.syms, nil } @@ -224,10 +230,11 @@ func readSymbols(data []byte, diagramID string) (*symbols, error) { // symbolFrame is one open element of a symbol stream. type symbolFrame struct { tag string - sym *Symbol // the symbol this mdElement is, nil for anything else - prop *property // the property this mdElement is, nil for anything else - listed *listing // the element this mdElement names, nil when it names none - hidden bool // marked not visible, kept for a listing its elementID has yet to open + sym *Symbol // the symbol this mdElement is, nil for anything else + prop *property // the property this mdElement is, nil for anything else + style *symbolStyle // the style manager this mdElement is, nil for anything else + listed *listing // the element this mdElement names, nil when it names none + hidden bool // marked not visible, kept for a listing its elementID has yet to open text strings.Builder valued bool // whether a value child appeared } @@ -239,6 +246,53 @@ type symbolReader struct { stack []*symbolFrame rooted bool symbolPath []*Symbol + styles map[string]*symbolStyle +} + +// symbolStyle is one property manager of the stream's symbolStyles: the +// SUPPRESS_* settings the symbols drawn in it inherit, and the manager it extends. +type symbolStyle struct { + id, parent string + compartments map[string]bool +} + +// openStyle is the nearest open style manager, nil when none is. +func (r *symbolReader) openStyle() *symbolStyle { + for i := len(r.stack) - 1; i >= 0; i-- { + if r.stack[i].style != nil { + return r.stack[i].style + } + } + return nil +} + +// inStyles reports whether the open element is a style of the symbolStyles list. +func (r *symbolReader) inStyles() bool { + return len(r.stack) >= 2 && r.stack[len(r.stack)-2].tag == "symbolStyles" +} + +// inherit gives each symbol the compartment settings of its style chain that +// its own properties do not state. +func (r *symbolReader) inherit() { + for _, sym := range r.syms.list { + seen := map[string]bool{} + for id := sym.StyleID; id != "" && !seen[id]; { + seen[id] = true + st := r.styles[id] + if st == nil { + break + } + for prop, suppressed := range st.compartments { + if _, own := sym.Compartments[prop]; !own { + if sym.Compartments == nil { + sym.Compartments = map[string]bool{} + } + sym.Compartments[prop] = suppressed + } + } + id = st.parent + } + } } // enclosing is the nearest open symbol, the one a tag belongs to. @@ -279,7 +333,9 @@ func (r *symbolReader) start(t xml.StartElement) error { f.sym = &Symbol{ID: attr(t, "id"), Class: attr(t, "elementClass"), Parent: r.enclosing()} r.syms.list = append(r.syms.list, f.sym) r.symbolPath = append(r.symbolPath, f.sym) - case t.Name.Local == "mdElement" && r.enclosing() != nil: + case t.Name.Local == "mdElement" && parentTag == "mdElement" && r.inStyles(): + f.style = &symbolStyle{compartments: map[string]bool{}} + case t.Name.Local == "mdElement" && (r.enclosing() != nil || r.openStyle() != nil): f.prop = &property{class: attr(t, "elementClass")} case parentTag == "mdElement": r.startField(t) @@ -348,9 +404,24 @@ func (r *symbolReader) end(t xml.EndElement) error { switch { case f.sym != nil: r.symbolPath = r.symbolPath[:len(r.symbolPath)-1] + case f.style != nil: + if f.style.id != "" { + r.styles[f.style.id] = f.style + } case f.prop != nil: if sym := r.enclosing(); sym != nil { f.prop.apply(sym) + } else if st := r.openStyle(); st != nil { + if id, suppressed, ok := f.prop.compartmentSwitch(); ok { + st.compartments[id] = suppressed + } + } + case parent != nil && parent.style != nil: + switch f.tag { + case "propertyManagerID": + parent.style.id = text + case "parentPropertyManager": + parent.style.parent = text } case parent != nil && parent.sym != nil: readSymbolField(parent.sym, f.tag, text, r.diagramID) @@ -406,6 +477,8 @@ func readSymbolField(sym *Symbol, tag, text, diagramID string) { sym.Bounds, sym.Points = parseGeometry(text) case tag == "text": sym.Text = text + case tag == "symbolStyleID": + sym.StyleID = text case tag == "image" && text != "": data, offset, octet := decodeOctets(text) if octet != "" { @@ -476,6 +549,16 @@ type property struct { fontStyle string } +// compartmentSwitch reads p as a SUPPRESS_* compartment property: its id and +// whether the compartment is suppressed; a property with no value is Cameo's +// default, shown. +func (p *property) compartmentSwitch() (id string, suppressed, ok bool) { + if p.class != "BooleanProperty" || !strings.HasPrefix(p.id, "SUPPRESS_") { + return "", false, false + } + return p.id, p.value == "true", true +} + // set records a property's child by tag. func (p *property) set(tag, text string) { switch tag { @@ -517,6 +600,12 @@ func (p *property) apply(sym *Symbol) { if font.Name != "" || font.Size != 0 { sym.Style.Font = font } + case p.class == "BooleanProperty" && strings.HasPrefix(p.id, "SUPPRESS_"): + id, suppressed, _ := p.compartmentSwitch() + if sym.Compartments == nil { + sym.Compartments = map[string]bool{} + } + sym.Compartments[id] = suppressed case p.class == "BooleanProperty" && p.id == "USE_FILL_COLOR": sym.Style.NoFill = p.value == "false" case (p.class == "FileProperty" || p.class == "StringProperty") && p.value != "" && sym.Attachment == "": diff --git a/internal/translate/xmi/sysmlv1/stream_test.go b/internal/translate/xmi/sysmlv1/stream_test.go index 0e7346009f..2651c3b7d3 100644 --- a/internal/translate/xmi/sysmlv1/stream_test.go +++ b/internal/translate/xmi/sysmlv1/stream_test.go @@ -776,3 +776,118 @@ func TestReadSymbolsDecodesPastedImages(t *testing.T) { t.Errorf("free = %v, want %v", syms.free, want) } } + +// A symbol's compartment settings come from its own SUPPRESS_* properties and +// then from its symbol style's property-manager chain; a property with no +// value is Cameo's default, shown. +func TestReadSymbolsCompartmentsInheritStyle(t *testing.T) { + stream := ` + + + + 5, 5, 533, 457 + + + + _byStereotype + + + SUPPRESS_PARTS_COMPARTMENT + + + SUPPRESS_VALUES_COMPARTMENT + + + + 35, 392, 200, 33 + + + + _missing + 266, 392, 119, 50 + + + + 266, 100, 119, 50 + + + + + _byStereotype + _classes + + FILL_COLOR + + + + SUPPRESS_PARTS_COMPARTMENT + + + + SUPPRESS_CLASS_ATTRIBUTES + + + SHOW_STEREOTYPES + + + + + _classes + _root + + SUPPRESS_CLASS_ATTRIBUTES + + + + SUPPRESS_CLASS_OPERATIONS + + + + + _root + + SUPPRESS_STRUCTURE + + + + + +` + syms, err := readSymbols([]byte(stream), "_diag") + if err != nil { + t.Fatal(err) + } + byID := map[string]*Symbol{} + for _, s := range syms.list { + byID[s.ID] = s + } + if len(byID) != 4 { + t.Fatalf("read %d symbols, want 4: %v", len(byID), syms.list) + } + if want := []string{"_block1", "_block2", "_block3"}; !reflect.DeepEqual(syms.shown, want) { + t.Errorf("shown = %q, want %q", syms.shown, want) + } + c1 := byID["_c1"] + if c1.StyleID != "_byStereotype" { + t.Errorf("style id = %q, want _byStereotype", c1.StyleID) + } + if c1.Style.Fill != "" { + t.Errorf("style colour inherited as %q, want none", c1.Style.Fill) + } + want := map[string]bool{ + "SUPPRESS_PARTS_COMPARTMENT": false, // the symbol's own, unvalued, over the style's true + "SUPPRESS_VALUES_COMPARTMENT": true, // the symbol's own + "SUPPRESS_CLASS_ATTRIBUTES": false, // the style's unvalued over its parent's true + "SUPPRESS_CLASS_OPERATIONS": true, // the parent manager + "SUPPRESS_STRUCTURE": true, // the grandparent manager + } + if !reflect.DeepEqual(c1.Compartments, want) { + t.Errorf("compartments = %v, want %v", c1.Compartments, want) + } + if c2 := byID["_c2"]; c2.Compartments != nil { + t.Errorf("compartments of a symbol with an unknown style = %v, want none", c2.Compartments) + } + if c3 := byID["_c3"]; c3.Compartments != nil || c3.StyleID != "" { + t.Errorf("compartments of an unstyled symbol = %v (style %q), want none", c3.Compartments, c3.StyleID) + } +} diff --git a/internal/workspace/libs/stdlib.snapshot b/internal/workspace/libs/stdlib.snapshot index ea50d9ad80..0e1eef065d 100644 Binary files a/internal/workspace/libs/stdlib.snapshot and b/internal/workspace/libs/stdlib.snapshot differ diff --git a/internal/workspace/libs/stdlib/OpenSysML Libraries/DiagramLayout.sysml b/internal/workspace/libs/stdlib/OpenSysML Libraries/DiagramLayout.sysml index 06b96419b3..06f1c6e9cb 100644 --- a/internal/workspace/libs/stdlib/OpenSysML Libraries/DiagramLayout.sysml +++ b/internal/workspace/libs/stdlib/OpenSysML Libraries/DiagramLayout.sysml @@ -7,12 +7,25 @@ library package DiagramLayout { metadata def Layout { doc /* Where the annotated element is drawn: the top-left corner of its - * box, an optional fixed size, and whether its body is folded. */ + * box, an optional fixed size, whether its body is folded, and + * which of its compartments are drawn. Each show… switch names + * one compartment kind of the box: attributes, parts (part + * usages), references (ref usages), ports, constraints, items + * (item and flow features) and actions. Unset means drawn; + * false means the modeler suppressed that compartment on this + * symbol, so a renderer omits it rather than clipping it. */ attribute x : ScalarValues::Real; attribute y : ScalarValues::Real; attribute width : ScalarValues::Real[0..1]; attribute height : ScalarValues::Real[0..1]; attribute collapsed : ScalarValues::Boolean[0..1]; + attribute showAttributes : ScalarValues::Boolean[0..1]; + attribute showParts : ScalarValues::Boolean[0..1]; + attribute showReferences : ScalarValues::Boolean[0..1]; + attribute showPorts : ScalarValues::Boolean[0..1]; + attribute showConstraints : ScalarValues::Boolean[0..1]; + attribute showItems : ScalarValues::Boolean[0..1]; + attribute showActions : ScalarValues::Boolean[0..1]; } metadata def Route { diff --git a/internal/workspace/libs/stdlib/OpenSysML Libraries/engine-contract.json b/internal/workspace/libs/stdlib/OpenSysML Libraries/engine-contract.json index 03e64579d0..27de97305c 100644 --- a/internal/workspace/libs/stdlib/OpenSysML Libraries/engine-contract.json +++ b/internal/workspace/libs/stdlib/OpenSysML Libraries/engine-contract.json @@ -72,7 +72,14 @@ "y", "width", "height", - "collapsed" + "collapsed", + "showAttributes", + "showParts", + "showReferences", + "showPorts", + "showConstraints", + "showItems", + "showActions" ], "migratorEmitted": true, "consumers": [ diff --git a/scripts/extension-libraries-pin.sh b/scripts/extension-libraries-pin.sh index be34f8bf95..9c8398b4d8 100755 --- a/scripts/extension-libraries-pin.sh +++ b/scripts/extension-libraries-pin.sh @@ -8,4 +8,4 @@ # verifies, because a ref is mutable. Change them together. EXTENSIONS_REPO="${EXTENSIONS_REPO:-https://github.com/Open-MBEE/OpenSysML-Extensions-Library.git}" EXTENSIONS_REF="${EXTENSIONS_REF:-main}" -EXTENSIONS_COMMIT="${EXTENSIONS_COMMIT:-b6af2597941045fe32d8c08f8d86d150cb5418a8}" +EXTENSIONS_COMMIT="${EXTENSIONS_COMMIT:-5134a14d313246beaf5f8cd526cfb3a127001b0b}" diff --git a/tests/migrate/suppressed_compartments_test.go b/tests/migrate/suppressed_compartments_test.go new file mode 100644 index 0000000000..34bb2d9c43 --- /dev/null +++ b/tests/migrate/suppressed_compartments_test.go @@ -0,0 +1,70 @@ +package migrate_test + +import ( + "bytes" + "strings" + "testing" +) + +// The compartments a modeler collapsed on a symbol are written as the symbol's +// Layout show… switches, from the symbol's own SUPPRESS_* properties and then +// its symbol style's chain; a property with no value is the tool's default, +// shown, and overrides an inherited suppression. A suppressed compartment no +// v2 compartment answers to is an unsupported presentation property. +func TestGoldenSuppressedCompartments(t *testing.T) { + r := migrateStream(t, "suppressed_compartments") + l := r.Report.Layout + if l == nil || l.StreamDiagrams != 1 || l.PlacementsWritten != 4 || l.CompartmentsSuppressed != 8 || l.SymbolsSuppressing != 2 { + t.Fatalf("layout summary: %+v", l) + } + if l.Unsupported["SUPPRESS_STRUCTURE"] != 2 { + t.Errorf("unsupported = %v, want SUPPRESS_STRUCTURE twice", l.Unsupported) + } + checkGolden(t, "testdata/xmi/suppressed_compartments.golden.sysml", r.Notation) + var report bytes.Buffer + if err := r.Report.WriteText(&report); err != nil { + t.Fatal(err) + } + checkGolden(t, "testdata/xmi/suppressed_compartments.golden.report.txt", report.Bytes()) + for _, d := range errors(t, "suppressed_compartments.sysml", r.Notation) { + t.Errorf("%v", d) + } + notation := string(r.Notation) + for _, want := range []string{ + // Camera: its own parts suppression, attributes and references from the + // style, ports from the style's parent; its own unvalued operations + // property overrides the parent's suppression. + "metadata DiagramLayout::Layout about Camera { x = 40; y = 20; width = 200; height = 100; showAttributes = false; showParts = false; showPorts = false; showReferences = false; }", + // Lens: the style's settings alone. + "metadata DiagramLayout::Layout about Lens { x = 40; y = 220; width = 120; height = 33; showActions = false; showAttributes = false; showPorts = false; showReferences = false; }", + // Mount: no style, one unvalued property: nothing suppressed. + "metadata DiagramLayout::Layout about Mount { x = 300; y = 220; width = 120; height = 60; }", + "metadata DiagramLayout::Layout about Optics::Camera::data { x = 235; y = 60; width = 10; height = 10; }", + "# compartments suppressed: 8 on 2 symbols", + } { + if !strings.Contains(notation, want) && !strings.Contains(report.String(), want) { + t.Errorf("output lacks %q", want) + } + } +} + +// An MTIP export places a symbol but records nothing of its compartments: the +// export's box wins and the stream's suppressions stay with it when the +// element's symbols agree; Lens has two symbols that differ, so its export +// box takes neither's. +func TestSuppressedCompartmentsSurviveAnExportRecord(t *testing.T) { + r := migrateStreamLaidOut(t, "suppressed_compartments", false) + l := r.Report.Layout + if l == nil || l.Diagrams != 1 || l.StreamSupplemented != 1 || l.CompartmentsSuppressed != 4 || l.SymbolsSuppressing != 1 { + t.Fatalf("layout summary: %+v", l) + } + notation := string(r.Notation) + for _, want := range []string{ + "metadata DiagramLayout::Layout about Camera { x = 50; y = 30; width = 200; height = 100; showAttributes = false; showParts = false; showPorts = false; showReferences = false; }", + "metadata DiagramLayout::Layout about Lens { x = 60; y = 230; width = 120; height = 60; }", + } { + if !strings.Contains(notation, want) { + t.Errorf("output lacks %q", want) + } + } +} diff --git a/tests/migrate/testdata/xmi/suppressed_compartments.golden.report.txt b/tests/migrate/testdata/xmi/suppressed_compartments.golden.report.txt new file mode 100644 index 0000000000..32e23427ec --- /dev/null +++ b/tests/migrate/testdata/xmi/suppressed_compartments.golden.report.txt @@ -0,0 +1,26 @@ +# SysML v1 to v2 migration report: suppressed_compartments.xmi +# exported by Example UML Tool +# migrated 13 element(s): 13 mapped, 0 approximated, 0 unmapped (0 skipped as profile, library or notation-only content, 0 as model elements nothing refers to); laid out 1 of 1 diagrams from the diagrams' own symbol streams: 4 elements positioned, 0 connectors routed, 0 styled, 0 notes + +## layout +# source: the diagrams' own symbol streams +# 1 views laid out from their own symbol stream, 0 without layout +# placements: 4 of 5 written (0 not exposed, 0 resolving to no element); routes: 0 of 0 written (0 not pinned, 0 resolving to no element); malformed: 0 +# styles: 0 of 0 written; notes: 0 written (0 anchored, 0 freed of an anchor the view does not lay out) +# compartments suppressed: 8 on 2 symbols +# unsupported presentation properties, dropped: SUPPRESS_STRUCTURE (2) + +## mapped (13) +Model Model _m -> Model (written as package Model) +Package Optics _pkg_optics -> Model::Optics +«Block» Class Optics::Camera _blk_camera -> Model::Optics::Camera +Diagram Optics::Camera BDD _diag_bdd -> Model::Optics::'Camera BDD' (a SysML Block Definition Diagram written as a view rendered asTreeDiagram; 6 of 10 shown elements are compartment rows or end labels of exposed elements, not exposed; laid out from the diagram's own symbol stream: 4 of 5 shown elements positioned, 0 of 0 connectors routed) +Operation Optics::Camera::capture _op_capture -> Model::Optics::Camera::capture (written as an action usage of Optics::Camera, which a call on an object performs, so its body runs on the object and reaches its features) +Port Optics::Camera::data _port_data -> Model::Optics::Camera::data +Property Optics::Camera::lens _prop_lens -> Model::Optics::Camera::lens +Property Optics::Camera::mount _prop_mount -> Model::Optics::Camera::mount +Property Optics::Camera::resolution _prop_resolution -> Model::Optics::Camera::resolution +«Block» Class Optics::Lens _blk_lens -> Model::Optics::Lens +Property Optics::Lens::focalLength _prop_focal -> Model::Optics::Lens::focalLength +«Block» Class Optics::Mount _blk_mount -> Model::Optics::Mount +Property Optics::Mount::thread _prop_thread -> Model::Optics::Mount::thread diff --git a/tests/migrate/testdata/xmi/suppressed_compartments.golden.sysml b/tests/migrate/testdata/xmi/suppressed_compartments.golden.sysml new file mode 100644 index 0000000000..d243094863 --- /dev/null +++ b/tests/migrate/testdata/xmi/suppressed_compartments.golden.sysml @@ -0,0 +1,29 @@ +package Model { + package Optics { + part def Camera { + attribute resolution : ScalarValues::Integer; + part lens : Lens; + ref part mount : Mount; + port data; + abstract action capture; + } + part def Lens { + attribute focalLength : ScalarValues::Real; + } + part def Mount { + attribute thread : ScalarValues::String; + } + view 'Camera BDD' { + expose Camera; + expose Optics::Camera::data; + expose Lens; + expose Mount; + @DiagramLayout::Canvas { unit = "px"; width = 480; height = 320; } + metadata DiagramLayout::Layout about Camera { x = 40; y = 20; width = 200; height = 100; showAttributes = false; showParts = false; showPorts = false; showReferences = false; } + metadata DiagramLayout::Layout about Optics::Camera::data { x = 235; y = 60; width = 10; height = 10; } + metadata DiagramLayout::Layout about Lens { x = 40; y = 220; width = 120; height = 33; showActions = false; showAttributes = false; showPorts = false; showReferences = false; } + metadata DiagramLayout::Layout about Mount { x = 300; y = 220; width = 120; height = 60; } + render Views::asTreeDiagram; + } + } +} diff --git a/tests/migrate/testdata/xmi/suppressed_compartments.layout.xml b/tests/migrate/testdata/xmi/suppressed_compartments.layout.xml new file mode 100644 index 0000000000..9d6eb5e0d9 --- /dev/null +++ b/tests/migrate/testdata/xmi/suppressed_compartments.layout.xml @@ -0,0 +1,54 @@ + + + 2022x v1.0.0 + 2022x Refresh2 + 2025-06-12T21:38:50.779458500Z + + + + + Model + + + + _m + + + sysml.Model + + + + + Camera BDD + + + + _diag_bdd + + + + + + -30 + -130 + 50 + 250 + + _blk_camera + sysml.Block + + + + -230 + -290 + 60 + 180 + + _blk_lens + sysml.Block + + + + sysml.BlockDefinitionDiagram + + diff --git a/tests/migrate/testdata/xmi/suppressed_compartments.stream.xml b/tests/migrate/testdata/xmi/suppressed_compartments.stream.xml new file mode 100644 index 0000000000..ffcaa86d94 --- /dev/null +++ b/tests/migrate/testdata/xmi/suppressed_compartments.stream.xml @@ -0,0 +1,77 @@ + + + + + 0, 0, 480, 320 + + + + _style_blocks + + + SUPPRESS_PARTS_COMPARTMENT + + + + SUPPRESS_CLASS_OPERATIONS + + + 40, 20, 200, 100 + + + + 235, 60, 10, 10 + + + + + + + _style_blocks + 40, 220, 120, 33 + + + + 200, 220, 120, 60 + + + + + + SUPPRESS_CLASS_ATTRIBUTES + + + 300, 220, 120, 60 + + + + + _style_blocks + _style_classes + + SUPPRESS_CLASS_ATTRIBUTES + + + + SUPPRESS_REFERENCES_COMPARTMENT + + + + SUPPRESS_STRUCTURE + + + + + _style_classes + + SUPPRESS_CLASS_OPERATIONS + + + + SUPPRESS_CLASS_PORTS + + + + + + diff --git a/tests/migrate/testdata/xmi/suppressed_compartments.xmi b/tests/migrate/testdata/xmi/suppressed_compartments.xmi new file mode 100644 index 0000000000..8fc432dd8e --- /dev/null +++ b/tests/migrate/testdata/xmi/suppressed_compartments.xmi @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + _blk_camera + _prop_resolution + _prop_lens + _prop_mount + _port_data + _op_capture + _blk_lens + _prop_focal + _blk_mount + _prop_thread + + + + + + + + + + + + +