diff --git a/capellambse_context_diagrams/collectors/default.py b/capellambse_context_diagrams/collectors/default.py
index 5f320c6e..195b6ca9 100644
--- a/capellambse_context_diagrams/collectors/default.py
+++ b/capellambse_context_diagrams/collectors/default.py
@@ -52,6 +52,10 @@ def __init__(
self.made_boxes = {self.centerbox.id: self.centerbox}
self.boxes_to_delete = {self.centerbox.id}
self.edges: list[fa.AbstractExchange] = []
+ self.stack_heights: dict[str, float | int] = {
+ "input": -makers.NEIGHBOR_VMARGIN,
+ "output": -makers.NEIGHBOR_VMARGIN,
+ }
if self.diagram.display_parent_relation:
self.diagram_target_owners = generic.get_all_owners(
self.diagram.target
@@ -72,11 +76,7 @@ def process_context(self):
box.children = [self.centerbox]
del self.data.children[0]
- stack_heights: dict[str, float | int] = {
- "input": -makers.NEIGHBOR_VMARGIN,
- "output": -makers.NEIGHBOR_VMARGIN,
- }
- self._process_ports(stack_heights)
+ self._process_ports()
if self.diagram.display_parent_relation and self.diagram.target.owner:
current = self.diagram.target.owner
@@ -108,7 +108,7 @@ def process_context(self):
generic.move_edges(owner_boxes, self.edges, self.data)
self.centerbox.height = max(
- self.centerbox.height, *stack_heights.values()
+ self.centerbox.height, *self.stack_heights.values()
)
def _process_exchanges(self) -> tuple[
@@ -147,7 +147,7 @@ def _process_exchanges(self) -> tuple[
return ports, ex_datas
- def _process_ports(self, stack_heights: dict[str, float | int]) -> None:
+ def _process_ports(self) -> None:
ports, ex_datas = self._process_exchanges()
for port, local_ports, side in port_context_collector(ex_datas, ports):
_, label_height = helpers.get_text_extent(port.name)
@@ -182,7 +182,7 @@ def _process_ports(self, stack_heights: dict[str, float | int]) -> None:
current = self._make_owner_box(self.diagram, current)
self.common_owners.add(current.uuid)
- stack_heights[side] += makers.NEIGHBOR_VMARGIN + height
+ self.stack_heights[side] += makers.NEIGHBOR_VMARGIN + height
def _make_box(
self,
diff --git a/capellambse_context_diagrams/collectors/exchanges.py b/capellambse_context_diagrams/collectors/exchanges.py
index cff5941b..5fd61029 100644
--- a/capellambse_context_diagrams/collectors/exchanges.py
+++ b/capellambse_context_diagrams/collectors/exchanges.py
@@ -4,6 +4,7 @@
from __future__ import annotations
import abc
+import collections.abc as cabc
import logging
import operator
import typing as t
@@ -56,11 +57,11 @@ def __init__(
self.obj = self.diagram.target
self.params = params
- src, trg, alloc_fex, fncs = self.intermap[diagram.type]
+ src, trg, alloc_fex, alloc_fncs = self.intermap[diagram.type]
self.get_source = operator.attrgetter(src)
self.get_target = operator.attrgetter(trg)
self.get_alloc_fex = operator.attrgetter(alloc_fex)
- self.get_alloc_functions = operator.attrgetter(fncs)
+ self.get_alloc_functions = operator.attrgetter(alloc_fncs)
def get_functions_and_exchanges(
self, comp: common.GenericElement, interface: common.GenericElement
@@ -154,8 +155,9 @@ def make_ports_and_update_children_size(
)
child.height = height
stack_height += makers.NEIGHBOR_VMARGIN + height
-
- data.height = stack_height
+ self.make_ports_and_update_children_size(child, exchanges)
+ if data.children:
+ data.height = stack_height
@abc.abstractmethod
def collect(self) -> None:
@@ -186,12 +188,14 @@ class InterfaceContextCollector(ExchangeCollector):
for building the interface context.
"""
- left: _elkjs.ELKInputChild | None
+ left_box: _elkjs.ELKInputChild | None
"""Left (source) Component Box of the interface."""
- right: _elkjs.ELKInputChild | None
+ right_box: _elkjs.ELKInputChild | None
"""Right (target) Component Box of the interface."""
- outgoing_edges: dict[str, common.GenericElement]
- incoming_edges: dict[str, common.GenericElement]
+ outgoing_edges: list[common.GenericElement]
+ incoming_edges: list[common.GenericElement]
+ global_boxes: dict[str, _elkjs.ELKInputChild]
+ boxes_to_delete: set[str]
def __init__(
self,
@@ -199,10 +203,12 @@ def __init__(
data: _elkjs.ELKInputData,
params: dict[str, t.Any],
) -> None:
- self.left = None
- self.right = None
- self.incoming_edges = {}
- self.outgoing_edges = {}
+ self.left_box = None
+ self.right_box = None
+ self.incoming_edges = []
+ self.outgoing_edges = []
+ self.global_boxes = {}
+ self.boxes_to_delete = set()
super().__init__(diagram, data, params)
@@ -210,41 +216,86 @@ def __init__(
if diagram.include_interface:
self.add_interface()
- def get_left_and_right(self) -> None:
- made_children: set[str] = set()
+ def _find_or_make_box(
+ self,
+ obj: t.Any,
+ **kwargs: t.Any,
+ ) -> _elkjs.ELKInputChild:
+ if not (box := self.global_boxes.get(obj.uuid)):
+ box = makers.make_box(
+ obj,
+ **kwargs,
+ )
+ self.global_boxes[obj.uuid] = box
+ else:
+ for key, value in kwargs.items():
+ setattr(box, key, value)
+ return box
+
+ def _process_ports(self, element: common.GenericElement) -> None:
+ for port in element.inputs + element.outputs:
+ for ex in port.exchanges:
+ elem = (
+ self.get_source(ex)
+ if port in element.inputs
+ else self.get_target(ex)
+ )
+ parent_comp = self._find_or_make_box(
+ elem.owner, no_symbol=True
+ )
+ if box := self.make_boxes(
+ elem, elem.functions, [], self._process_ports
+ ):
+ parent_comp.children.append(box)
+ self.boxes_to_delete.add(elem.uuid)
+ if port in element.inputs:
+ self.incoming_edges.extend(port.exchanges)
+ else:
+ self.outgoing_edges.extend(port.exchanges)
+
+ def make_boxes(
+ self,
+ element: common.GenericElement,
+ functions: list[common.GenericElement],
+ components: list[common.GenericElement],
+ process_ports: (
+ cabc.Callable[[common.GenericElement], None] | None
+ ) = None,
+ ) -> _elkjs.ELKInputChild | None:
+ children = []
+ if element.uuid in self.global_boxes:
+ return None
+ for fnc in functions:
+ if process_ports:
+ process_ports(fnc)
+ if fnc_box := self.make_boxes(
+ fnc, fnc.functions, [], self._process_ports
+ ):
+ children.append(fnc_box)
+ self.boxes_to_delete.add(fnc.uuid)
+ for cmp in components:
+ if child := self.make_boxes(**cmp):
+ children.append(child)
+ self.boxes_to_delete.add(cmp["element"].uuid)
+ if children:
+ layout_options = makers.DEFAULT_LABEL_LAYOUT_OPTIONS
+ else:
+ layout_options = makers.CENTRIC_LABEL_LAYOUT_OPTIONS
+
+ box = self._find_or_make_box(
+ element, no_symbol=True, layout_options=layout_options
+ )
+ box.children = children
+ return box
+ def get_left_and_right(self) -> None:
def get_capella_order(
- comp: common.GenericElement, functions: list[common.GenericElement]
+ comp: common.GenericElement,
+ functions: list[common.GenericElement],
) -> list[common.GenericElement]:
alloc_functions = self.get_alloc_functions(comp)
return [fnc for fnc in alloc_functions if fnc in functions]
- 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(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:
- layout_options = makers.CENTRIC_LABEL_LAYOUT_OPTIONS
-
- box = makers.make_box(
- comp, no_symbol=True, layout_options=layout_options
- )
- box.children = children
- made_children.add(comp.uuid)
- return box
- return None
-
try:
comp = self.get_source(self.obj)
left_context, incs, outs = self.collect_context(comp, self.obj)
@@ -258,26 +309,30 @@ def make_boxes(cntxt: dict[str, t.Any]) -> _elkjs.ELKInputChild | None:
_out_port_ids = set(ex.source.uuid for ex in incs.values())
_port_spread = len(_out_port_ids) - len(_inc_port_ids)
- left_context["functions"] = get_capella_order(
- comp, left_context["functions"]
- )
- right_context["functions"] = get_capella_order(
- _comp, right_context["functions"]
- )
+ left_context["functions"] = [
+ f
+ for f in get_capella_order(comp, left_context["functions"])
+ if f in self.get_alloc_functions(left_context["element"])
+ ]
+ right_context["functions"] = [
+ f
+ for f in get_capella_order(_comp, right_context["functions"])
+ if f in self.get_alloc_functions(right_context["element"])
+ ]
if port_spread >= _port_spread:
- self.incoming_edges = incs
- self.outgoing_edges = outs
+ self.incoming_edges = list(incs.values())
+ self.outgoing_edges = list(outs.values())
else:
- self.incoming_edges = outs
- self.outgoing_edges = incs
+ self.incoming_edges = list(outs.values())
+ self.outgoing_edges = list(incs.values())
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
+ self.left_box = self.make_boxes(**left_context)
+ self.right_box = self.make_boxes(**right_context)
+
+ for uuid in self.boxes_to_delete:
+ del self.global_boxes[uuid]
+ self.data.children.extend(self.global_boxes.values())
except AttributeError:
pass
@@ -290,19 +345,19 @@ def add_interface(self) -> None:
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:
+ assert self.right_box is not None
+ if self.get_source(self.obj).uuid == self.right_box.id:
self.data.edges[-1].sources = [tgt.uuid]
self.data.edges[-1].targets = [src.uuid]
- assert self.left is not None
- self.left.ports.append(makers.make_port(src.uuid))
- self.right.ports.append(makers.make_port(tgt.uuid))
+ assert self.left_box is not None
+ self.left_box.ports.append(makers.make_port(src.uuid))
+ self.right_box.ports.append(makers.make_port(tgt.uuid))
def collect(self) -> None:
"""Collect all allocated `FunctionalExchange`s in the context."""
try:
- for ex in (self.incoming_edges | self.outgoing_edges).values():
+ for ex in self.incoming_edges + self.outgoing_edges:
ex_data = generic.ExchangeData(
ex,
self.data,
@@ -312,7 +367,7 @@ def collect(self) -> None:
)
src, tgt = generic.exchange_data_collector(ex_data)
- if ex in self.incoming_edges.values():
+ if ex in self.incoming_edges:
self.data.edges[-1].sources = [tgt.uuid]
self.data.edges[-1].targets = [src.uuid]
diff --git a/tests/data/ContextDiagram.aird b/tests/data/ContextDiagram.aird
index 262c9dc7..d52a1bfe 100644
--- a/tests/data/ContextDiagram.aird
+++ b/tests/data/ContextDiagram.aird
@@ -142,6 +142,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -14427,4 +14443,837 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+ italic
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+ italic
+
+
+
+
+
+
+
+
+
+
+
+ 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 0d4c574a..0c0064fb 100644
--- a/tests/data/ContextDiagram.capella
+++ b/tests/data/ContextDiagram.capella
@@ -62,7 +62,7 @@
definition="#682bd51d-5451-4930-a97e-8bfca6c3a127" value="true"/>
+ value="2021-07-23T13:00:00.000+0000"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -3342,6 +3375,15 @@ The predator is far away
+
+
+
id="c92eb72c-69ad-4bb0-b5ef-6b00f8716222" targetElement="#5f67436c-9742-4b84-b99e-7ee719ff02a0"
sourceElement="#2f8ed849-fbda-4902-82ec-cbf8104ae686"/>
+
+
+
+
+
+
name="LC 1" abstractType="#44d051f2-875d-4a51-93f0-6c7b20a45842"/>
+
+
+
+
@@ -4258,6 +4323,67 @@ The predator is far away
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
None:
+ obj = model.by_uuid(uuid)
+
+ diag = obj.context_diagram
+ diag.render(
+ "svgdiagram",
+ include_interface=True,
+ ).save(pretty=True)
+
+ assert len(diag.nodes) > 1