diff --git a/capellambse_context_diagrams/_elkjs.py b/capellambse_context_diagrams/_elkjs.py index 7a7bc2c1..337588b1 100644 --- a/capellambse_context_diagrams/_elkjs.py +++ b/capellambse_context_diagrams/_elkjs.py @@ -56,6 +56,7 @@ "hierarchyHandling": "INCLUDE_CHILDREN", "layered.edgeLabels.sideSelection": "ALWAYS_DOWN", "layered.nodePlacement.strategy": "BRANDES_KOEPF", + "layered.considerModelOrder.strategy": "NODES_AND_EDGES", "spacing.labelNode": "0.0", } """ diff --git a/capellambse_context_diagrams/collectors/exchanges.py b/capellambse_context_diagrams/collectors/exchanges.py index f4eaab46..83f64933 100644 --- a/capellambse_context_diagrams/collectors/exchanges.py +++ b/capellambse_context_diagrams/collectors/exchanges.py @@ -9,6 +9,7 @@ import typing as t from capellambse.model import common +from capellambse.model.crosslayer import cs from capellambse.model.modeltypes import DiagramType as DT from .. import _elkjs, context @@ -65,31 +66,60 @@ def get_functions_and_exchanges( self, comp: common.GenericElement, interface: common.GenericElement ) -> tuple[ list[common.GenericElement], - list[common.GenericElement], - list[common.GenericElement], + dict[str, common.GenericElement], + dict[str, common.GenericElement], ]: """Return `Function`s, incoming and outgoing `FunctionalExchange`s for given `Component` and `interface`. """ - functions, outgoings, incomings = [], [], [] + functions, incomings, outgoings = [], {}, {} alloc_functions = self.get_alloc_functions(comp) for fex in self.get_alloc_fex(interface): source = self.get_source(fex) if source in alloc_functions: - if fex not in outgoings: - outgoings.append(fex) + if fex.uuid not in outgoings: + outgoings[fex.uuid] = fex if source not in functions: functions.append(source) target = self.get_target(fex) if target in alloc_functions: - if fex not in incomings: - incomings.append(fex) + if fex.uuid not in incomings: + incomings[fex.uuid] = fex if target not in functions: functions.append(target) return functions, incomings, outgoings + def collect_context( + self, comp: common.GenericElement, interface: common.GenericElement + ) -> tuple[ + dict[str, t.Any], + dict[str, common.GenericElement], + dict[str, common.GenericElement], + ]: + functions, incomings, outgoings = self.get_functions_and_exchanges( + comp, interface + ) + components = [] + for cmp in comp.components: + fncs, _, _ = self.get_functions_and_exchanges(cmp, interface) + functions.extend(fncs) + if fncs: + c, incs, outs = self.collect_context(cmp, interface) + components.append(c) + incomings |= incs + outgoings |= outs + return ( + { + "element": comp, + "functions": functions, + "components": components, + }, + incomings, + outgoings, + ) + def make_ports_and_update_children_size( self, data: _elkjs.ELKInputChild, @@ -100,6 +130,9 @@ def make_ports_and_update_children_size( for child in data["children"]: inputs, outputs = [], [] obj = self.obj._model.by_uuid(child["id"]) + if isinstance(obj, cs.Component): + self.make_ports_and_update_children_size(child, exchanges) + return port_ids = {p.uuid for p in obj.inputs + obj.outputs} for ex in exchanges: source, target = ex["sources"][0], ex["targets"][0] @@ -153,12 +186,12 @@ class InterfaceContextCollector(ExchangeCollector): for building the interface context. """ - left: common.GenericElement - """Source or target Component of the interface.""" - right: common.GenericElement - """Source or target Component of the interface.""" - outgoing_edges: list[common.GenericElement] - incoming_edges: list[common.GenericElement] + left: _elkjs.ELKInputChild | None + """Left (source) Component Box of the interface.""" + right: _elkjs.ELKInputChild | None + """Right (target) Component Box of the interface.""" + outgoing_edges: dict[str, common.GenericElement] + incoming_edges: dict[str, common.GenericElement] def __init__( self, @@ -166,8 +199,16 @@ def __init__( data: _elkjs.ELKInputData, params: dict[str, t.Any], ) -> None: + self.left = None + self.right = None + self.incoming_edges = {} + self.outgoing_edges = {} + super().__init__(diagram, data, params) + self.get_left_and_right() + if diagram.include_interface: + self.add_interface() def get_left_and_right(self) -> None: made_children: set[str] = set() @@ -178,15 +219,19 @@ def get_capella_order( alloc_functions = self.get_alloc_functions(comp) return [fnc for fnc in alloc_functions if fnc in functions] - def make_boxes( - comp: common.GenericElement, functions: list[common.GenericElement] - ) -> None: + def make_boxes(cntxt: dict[str, t.Any]) -> _elkjs.ELKInputChild | None: + comp = cntxt["element"] + functions = cntxt["functions"] + components = cntxt["components"] if comp.uuid not in made_children: children = [ - makers.make_box(c) - for c in functions - if c in self.get_alloc_functions(comp) + makers.make_box(fnc) + for fnc in functions + if fnc in self.get_alloc_functions(comp) ] + for cmp in components: + if child := make_boxes(cmp): + children.append(child) if children: layout_options = makers.DEFAULT_LABEL_LAYOUT_OPTIONS else: @@ -196,52 +241,68 @@ def make_boxes( comp, no_symbol=True, layout_options=layout_options ) box["children"] = children - self.data["children"].append(box) made_children.add(comp.uuid) + return box + return None try: comp = self.get_source(self.obj) - functions, incs, outs = self.get_functions_and_exchanges( - comp, self.obj - ) - inc_port_ids = set(ex.target.uuid for ex in incs) - out_port_ids = set(ex.source.uuid for ex in outs) + left_context, incs, outs = self.collect_context(comp, self.obj) + inc_port_ids = set(ex.target.uuid for ex in incs.values()) + out_port_ids = set(ex.source.uuid for ex in outs.values()) port_spread = len(out_port_ids) - len(inc_port_ids) _comp = self.get_target(self.obj) - _functions, _, _ = self.get_functions_and_exchanges( - _comp, self.obj - ) - _inc_port_ids = set(ex.target.uuid for ex in outs) - _out_port_ids = set(ex.source.uuid for ex in incs) + right_context, _, _ = self.collect_context(_comp, self.obj) + _inc_port_ids = set(ex.target.uuid for ex in outs.values()) + _out_port_ids = set(ex.source.uuid for ex in incs.values()) _port_spread = len(_out_port_ids) - len(_inc_port_ids) - functions = get_capella_order(comp, functions) - _functions = get_capella_order(_comp, _functions) + left_context["functions"] = get_capella_order( + comp, left_context["functions"] + ) + right_context["functions"] = get_capella_order( + _comp, right_context["functions"] + ) if port_spread >= _port_spread: - self.left = comp - self.right = _comp - self.outgoing_edges = outs self.incoming_edges = incs - left_functions = functions - right_functions = _functions + self.outgoing_edges = outs else: - self.left = _comp - self.right = comp - self.outgoing_edges = incs self.incoming_edges = outs - left_functions = _functions - right_functions = functions - - make_boxes(self.left, left_functions) - make_boxes(self.right, right_functions) + self.outgoing_edges = incs + left_context, right_context = right_context, left_context + + if left_child := make_boxes(left_context): + self.data["children"].append(left_child) + self.left = left_child + if right_child := make_boxes(right_context): + self.data["children"].append(right_child) + self.right = right_child except AttributeError: pass + def add_interface(self) -> None: + ex_data = generic.ExchangeData( + self.obj, + self.data, + self.diagram.filters, + self.params, + is_hierarchical=False, + ) + src, tgt = generic.exchange_data_collector(ex_data) + assert self.right is not None + if self.get_source(self.obj).uuid == self.right["id"]: + self.data["edges"][-1]["sources"] = [tgt.uuid] + self.data["edges"][-1]["targets"] = [src.uuid] + + assert self.left is not None + self.left.setdefault("ports", []).append(makers.make_port(src.uuid)) + self.right.setdefault("ports", []).append(makers.make_port(tgt.uuid)) + def collect(self) -> None: - """Return all allocated `FunctionalExchange`s in the context.""" + """Collect all allocated `FunctionalExchange`s in the context.""" try: - for ex in self.incoming_edges + self.outgoing_edges: + for ex in (self.incoming_edges | self.outgoing_edges).values(): ex_data = generic.ExchangeData( ex, self.data, @@ -251,7 +312,7 @@ def collect(self) -> None: ) src, tgt = generic.exchange_data_collector(ex_data) - if ex in self.incoming_edges: + if ex in self.incoming_edges.values(): self.data["edges"][-1]["sources"] = [tgt.uuid] self.data["edges"][-1]["targets"] = [src.uuid] @@ -300,7 +361,7 @@ def collect(self) -> None: made_children.add(comp.uuid) all_functions.extend(functions) - functional_exchanges.extend(inc + outs) + functional_exchanges.extend(inc | outs) self.data["children"][0]["children"] = [ makers.make_box(c) diff --git a/capellambse_context_diagrams/context.py b/capellambse_context_diagrams/context.py index 419574cb..a672d365 100644 --- a/capellambse_context_diagrams/context.py +++ b/capellambse_context_diagrams/context.py @@ -368,7 +368,14 @@ class InterfaceContextDiagram(ContextDiagram): ``ComponentExchange``s. """ - def __init__(self, class_: str, obj: common.GenericElement, **kw) -> None: + def __init__( + self, + class_: str, + obj: common.GenericElement, + include_interface: bool = False, + **kw, + ) -> None: + self.include_interface = include_interface super().__init__(class_, obj, **kw, display_symbols_as_boxes=True) @property diff --git a/docs/data_flow_view.md b/docs/data_flow_view.md index fa101353..9c9127e2 100644 --- a/docs/data_flow_view.md +++ b/docs/data_flow_view.md @@ -24,7 +24,7 @@ The diagram elements are collected from the diag.as_svgdiagram.save(pretty=True) ```
- +
[OAIB] DataFlow View Diagram of Eat food
diff --git a/docs/gen_images.py b/docs/gen_images.py index b07f7488..d48e173c 100644 --- a/docs/gen_images.py +++ b/docs/gen_images.py @@ -5,6 +5,7 @@ import logging import pathlib +import typing as t import mkdocs_gen_files from capellambse import MelodyModel, diagram @@ -16,19 +17,23 @@ dest = pathlib.Path("assets") / "images" model_path = pathlib.Path(__file__).parent.parent / "tests" / "data" model = MelodyModel(path=model_path, entrypoint="ContextDiagram.aird") -general_context_diagram_uuids = { - "Environment": "e37510b9-3166-4f80-a919-dfaac9b696c7", - "Eat": "8bcb11e6-443b-4b92-bec2-ff1d87a224e7", - "Middle": "da08ddb6-92ba-4c3b-956a-017424dbfe85", - "Capability": "9390b7d5-598a-42db-bef8-23677e45ba06", - "Lost": "a5642060-c9cc-4d49-af09-defaa3024bae", - "Left": "f632888e-51bc-4c9f-8e81-73e9404de784", - "educate Wizards": "957c5799-1d4a-4ac0-b5de-33a65bf1519c", - "Weird guy": "098810d9-0325-4ae8-a111-82202c0d2016", - "Top secret": "5bf3f1e3-0f5e-4fec-81d5-c113d3a1b3a6", +general_context_diagram_uuids: dict[str, tuple[str, dict[str, t.Any]]] = { + "Environment": ("e37510b9-3166-4f80-a919-dfaac9b696c7", {}), + "Eat": ("8bcb11e6-443b-4b92-bec2-ff1d87a224e7", {}), + "Middle": ("da08ddb6-92ba-4c3b-956a-017424dbfe85", {}), + "Capability": ("9390b7d5-598a-42db-bef8-23677e45ba06", {}), + "Lost": ("a5642060-c9cc-4d49-af09-defaa3024bae", {}), + "Left": ("f632888e-51bc-4c9f-8e81-73e9404de784", {}), + "educate Wizards": ("957c5799-1d4a-4ac0-b5de-33a65bf1519c", {}), + "Weird guy": ("098810d9-0325-4ae8-a111-82202c0d2016", {}), + "Top secret": ("5bf3f1e3-0f5e-4fec-81d5-c113d3a1b3a6", {}), } -interface_context_diagram_uuids = { - "Left to right": "3ef23099-ce9a-4f7d-812f-935f47e7938d", +interface_context_diagram_uuids: dict[str, tuple[str, dict[str, t.Any]]] = { + "Left to right": ("3ef23099-ce9a-4f7d-812f-935f47e7938d", {}), + "Interface": ( + "fbb7f735-3c1f-48de-9791-179d35ca7b98", + {"include_interface": True}, + ), } hierarchy_context = "16b4fcc5-548d-4721-b62a-d3d5b1c1d2eb" diagram_uuids = general_context_diagram_uuids | interface_context_diagram_uuids @@ -40,15 +45,16 @@ def generate_index_images() -> None: - for uuid in diagram_uuids.values(): + for uuid, render_params in diagram_uuids.values(): diag: context.ContextDiagram = model.by_uuid(uuid).context_diagram with mkdocs_gen_files.open(f"{str(dest / diag.name)}.svg", "w") as fd: - print(diag.render("svg", transparent_background=False), file=fd) + render_params["transparent_background"] = False # type: ignore[index] + print(diag.render("svg", **render_params), file=fd) # type: ignore[arg-type] def generate_no_symbol_images() -> None: for name in ("Capability", "Middle"): - uuid = general_context_diagram_uuids[name] + uuid, _ = general_context_diagram_uuids[name] diag: context.ContextDiagram = model.by_uuid(uuid).context_diagram diag.display_symbols_as_boxes = True diag.invalidate_cache() @@ -170,23 +176,23 @@ def generate_derived_image() -> None: generate_hierarchy_image() generate_no_symbol_images() -wizard = general_context_diagram_uuids["educate Wizards"] -generate_no_edgelabel_image(wizard) +wizard_uuid = general_context_diagram_uuids["educate Wizards"][0] +generate_no_edgelabel_image(wizard_uuid) -lost = general_context_diagram_uuids["Lost"] -generate_filter_image(lost, filters.EX_ITEMS, "ex") -generate_filter_image(lost, filters.FEX_EX_ITEMS, "fex and ex") -generate_filter_image(lost, filters.FEX_OR_EX_ITEMS, "fex or ex") +lost_uuid = general_context_diagram_uuids["Lost"][0] +generate_filter_image(lost_uuid, filters.EX_ITEMS, "ex") +generate_filter_image(lost_uuid, filters.FEX_EX_ITEMS, "fex and ex") +generate_filter_image(lost_uuid, filters.FEX_OR_EX_ITEMS, "fex or ex") generate_styling_image( - lost, + lost_uuid, dict( styling.BLUE_ACTOR_FNCS, junction=lambda o, s: {"stroke": diagram.RGB(220, 20, 60)}, ), "red junction", ) -generate_styling_image(wizard, {}, "no_styles") +generate_styling_image(wizard_uuid, {}, "no_styles") generate_class_tree_images() generate_realization_view_images() generate_data_flow_image() diff --git a/docs/index.md b/docs/index.md index 93ab4d46..93f44bfb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,8 +17,8 @@ base class with [`ContextDiagram`s][capellambse_context_diagrams.context.Context Generate **Context Diagrams** from your model data!
- -
Interface context diagram of Left to right
+ +
Interface context diagram of Interface
## Features @@ -192,6 +192,21 @@ The data is collected by [get_elkdata_for_exchanges][capellambse_context_diagram
Interface context diagram of Left to right LogicalComponentExchange with type [LAB]
+??? example "Include the interface the ([`fa.ComponentExchange`][capellambse.model.crosslayer.fa.ComponentExchange])" + + ``` py + import capellambse + + model = capellambse.MelodyModel("tests/data/ContextDiagram.aird") + diag = model.by_uuid("fbb7f735-3c1f-48de-9791-179d35ca7b98").context_diagram + diag.render("svgdiagram", include_interface=True).save(pretty=True) + ``` +
+ +
Interface context diagram of Interface LogicalComponentExchange with type [LAB]
+
+ + !!! warning "Interface context only supported for the LogicalComponentExchanges" ### Customized edge routing diff --git a/tests/data/ContextDiagram.aird b/tests/data/ContextDiagram.aird index 61b5b7b3..ca8830e5 100644 --- a/tests/data/ContextDiagram.aird +++ b/tests/data/ContextDiagram.aird @@ -54,7 +54,7 @@ - +
@@ -62,7 +62,7 @@ - +
@@ -78,7 +78,7 @@ - +
@@ -102,6 +102,14 @@ + + +
+
+ + + + @@ -2041,7 +2049,7 @@ - + @@ -2052,7 +2060,7 @@ - + @@ -2063,10 +2071,10 @@ - + - + @@ -2083,7 +2091,7 @@ - + @@ -2128,22 +2136,6 @@ - - - - - - - - - - - - - - - - @@ -2162,17 +2154,17 @@ - + - + - + - + @@ -2224,22 +2216,6 @@ - - - - - - - - - - - - - - - - @@ -2258,17 +2234,17 @@ - + - + - + - + @@ -2290,17 +2266,17 @@ - + - + - + @@ -2322,17 +2298,17 @@ - + - + - + - + @@ -2448,6 +2424,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2500,7 +2508,7 @@ - + @@ -2541,7 +2549,7 @@ - + @@ -2671,7 +2679,7 @@ - + @@ -2681,7 +2689,7 @@ - + @@ -2691,7 +2699,7 @@ - + @@ -2755,7 +2763,7 @@ - + @@ -2765,7 +2773,7 @@ - + @@ -2920,17 +2928,6 @@ - - - - - - - - - - - @@ -2968,6 +2965,28 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -3425,15 +3444,6 @@ - - - - - - - - - @@ -3461,6 +3471,24 @@ + + + + + + + + + + + + + + + + + + KEEP_LOCATION KEEP_SIZE KEEP_RATIO @@ -3508,8 +3536,8 @@ KEEP_LOCATION KEEP_SIZE KEEP_RATIO - - + + @@ -7753,7 +7781,7 @@ - + @@ -9451,4 +9479,733 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + KEEP_LOCATION + KEEP_SIZE + KEEP_RATIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/ContextDiagram.capella b/tests/data/ContextDiagram.capella index a20b61b3..1a673425 100644 --- a/tests/data/ContextDiagram.capella +++ b/tests/data/ContextDiagram.capella @@ -2971,6 +2971,35 @@ The predator is far away + + + + + + + + + + + + + + + + + @@ -3008,20 +3037,20 @@ The predator is far away id="0fe9e999-3bd8-4590-9dc2-f51d2c1af173" name="FunctionalExchange 11" target="#494979bd-d301-419b-8a91-fd1c28dec21c" source="#aaf1f13c-270f-4d67-bc65-37a1c9afb17a"/> - + @@ -3067,6 +3096,18 @@ The predator is far away + + + + + + + + + + name="LC 16" abstractType="#749a54c8-1ee6-41ac-851d-08994c5fb403"/> + + @@ -3837,6 +3898,71 @@ The predator is far away id="b30ee5f7-e2ce-43c5-b8cc-fc0f7335929f" targetElement="#24cfdc5b-918d-4f62-98e3-260d91d635fc" sourceElement="#84525f1a-a5e5-4ca5-bbe8-7e5c64aa18d0"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + None: + obj = model.by_uuid(TEST_INTERFACE_UUID) + + diag = obj.context_diagram + + assert diag.nodes + + +def test_interface_diagram_with_included_interface( + model: capellambse.MelodyModel, +) -> None: + obj = model.by_uuid(TEST_INTERFACE_UUID) + + diag = obj.context_diagram.render(None, include_interface=True) + + assert diag[TEST_INTERFACE_UUID]