diff --git a/capellambse/aird/capstyle.py b/capellambse/aird/capstyle.py
index 008c95bde..9b38798ea 100644
--- a/capellambse/aird/capstyle.py
+++ b/capellambse/aird/capstyle.py
@@ -650,6 +650,24 @@ class in the form::
"stroke-dasharray": "5",
},
},
+ "Operational Activity Interaction Blank": {
+ "Box.OperationalActivity": {
+ "fill": COLORS["_CAP_Activity_Orange"],
+ "rx": "10px",
+ "ry": "10px",
+ "stroke": COLORS["_CAP_Activity_Border_Orange"],
+ "text_fill": COLORS["_CAP_xAB_Activity_Label_Orange"],
+ },
+ "Box.OperationalProcess": {
+ "stroke": COLORS["black"],
+ "text_fill": COLORS["black"],
+ },
+ "Edge.FunctionalExchange": {
+ "marker-end": "ArrowMark",
+ "stroke-width": 2,
+ "stroke": COLORS["_CAP_Activity_Border_Orange"],
+ },
+ },
"Physical Architecture Blank": {
**dict.fromkeys(
["Box.CP_IN", "Box.CP_OUT", "Box.CP_INOUT"],
@@ -658,6 +676,14 @@ class in the form::
"stroke": COLORS["black"],
},
),
+ "Box.FIP": {
+ "fill": COLORS["dark_orange"],
+ "stroke-width": 0,
+ },
+ "Box.FOP": {
+ "fill": COLORS["_CAP_xAB_Function_Border_Green"],
+ "stroke-width": 0,
+ },
"Box.PP": {
"fill": COLORS["_CAP_PhysicalPort_Yellow"],
"stroke": COLORS["_CAP_Class_Border_Brown"],
@@ -670,12 +696,68 @@ class in the form::
"stroke": COLORS["_CAP_Node_Yellow_Border"],
"text_fill": COLORS["_CAP_Node_Yellow_Label"],
},
+ "Box.PhysicalFunction": {
+ "fill": COLORS["_CAP_xAB_Function_Green"],
+ "stroke": COLORS["_CAP_xAB_Function_Border_Green"],
+ },
+ "Edge.ComponentExchange": {
+ "stroke": COLORS["_CAP_Component_Border_Blue"],
+ "stroke-width": 2,
+ "text_fill": COLORS["_CAP_Component_Border_Blue"],
+ },
+ "Edge.FunctionalExchange": {
+ "stroke": COLORS["_CAP_xAB_Function_Border_Green"],
+ "stroke-width": 2,
+ "text_fill": COLORS["_CAP_xAB_Function_Border_Green"],
+ },
"Edge.PhysicalLink": {
"stroke": COLORS["red"],
"stroke-width": 2,
"text_fill": COLORS["red"],
},
},
+ "Physical Data Flow Blank": {
+ "Box.FIP": {
+ "fill": COLORS["dark_orange"],
+ "stroke-width": 0,
+ },
+ "Box.FOP": {
+ "fill": COLORS["_CAP_xAB_Function_Border_Green"],
+ "stroke-width": 0,
+ },
+ "Box.PhysicalFunction": {
+ "fill": COLORS["_CAP_xAB_Function_Green"],
+ "stroke": COLORS["_CAP_xAB_Function_Border_Green"],
+ },
+ "Edge.FunctionalExchange": {
+ "stroke": COLORS["_CAP_xAB_Function_Border_Green"],
+ "stroke-width": 2,
+ "text_fill": COLORS["_CAP_xAB_Function_Border_Green"],
+ },
+ },
+ "System Architecture Blank": {
+ **dict.fromkeys(
+ ["Box.CP_IN", "Box.CP_OUT", "Box.CP_INOUT"],
+ {
+ "fill": COLORS["white"],
+ "stroke": COLORS["black"],
+ },
+ ),
+ "Box.SystemComponent": {
+ "fill": [COLORS["_CAP_Actor_Blue_min"], COLORS["_CAP_Actor_Blue"]],
+ "stroke": COLORS["_CAP_Actor_Border_Blue"],
+ "text_fill": COLORS["_CAP_Actor_Blue_label"],
+ },
+ "Box.SystemFunction": {
+ "fill": COLORS["_CAP_xAB_Function_Green"],
+ "stroke": COLORS["_CAP_xAB_Function_Border_Green"],
+ },
+ "Edge.ComponentExchange": {
+ "stroke": COLORS["_CAP_Component_Border_Blue"],
+ "stroke-width": 2,
+ "text_fill": COLORS["_CAP_Component_Border_Blue"],
+ },
+ },
"System Data Flow Blank": {
"Box.FIP": {
"fill": COLORS["dark_orange"],
diff --git a/capellambse/aird/diagram.py b/capellambse/aird/diagram.py
index 28194e1ef..f712f804a 100644
--- a/capellambse/aird/diagram.py
+++ b/capellambse/aird/diagram.py
@@ -190,36 +190,14 @@ def snap_to_parent(self) -> None:
if self.port:
padding = (self.parent.PORT_OVERHANG, self.parent.PORT_OVERHANG)
- # Distances of all corner combinations
- # (our/parent's top-left/bottom-right)
- d_tl_tl = self.pos - self.parent.pos
- d_br_tl = self.pos + self.size - self.parent.pos
- d_tl_br = self.pos - self.parent.pos - self.parent.size
- d_br_br = self.pos + self.size - self.parent.pos - self.parent.size
-
- # Find closest parent border (left vs. right and top vs. bottom)
- d_tl = aird.Vector2D(
- min(d_tl_tl.x, d_br_tl.x), min(d_tl_tl.y, d_br_tl.y)
- )
- d_br = aird.Vector2D(
- min(d_tl_br.x, d_br_br.x), min(d_tl_br.y, d_br_br.y)
- )
- border = aird.Vector2D(
- -1 if abs(d_tl.x) <= abs(d_br.x) else 1,
- -1 if abs(d_tl.y) <= abs(d_br.y) else 1,
- )
-
- # Find out if horizontal or vertical border is closer
- horiz = abs(d_tl.x if border.x < 0 else d_br.x) <= abs(
- d_tl.y if border.y < 0 else d_br.y
- )
- border = border @ (horiz, not horiz)
-
- self.pos = self.pos.boxsnap(
- self.parent.pos - padding,
- self.parent.pos + self.parent.size + padding - self.size,
- border,
+ midbox_tl = self.parent.pos + self.size / 2 - padding
+ midbox_br = (
+ self.parent.pos + self.parent.size - self.size / 2 + padding
)
+ midbox = aird.Box(midbox_tl, midbox_br - midbox_tl)
+ mid = self.pos + self.size / 2
+ newmid = midbox.vector_snap(mid)
+ self.pos += newmid - mid
else:
padding = (self.parent.CHILD_MARGIN, self.parent.CHILD_MARGIN)
minpos = self.parent.pos + padding
@@ -253,7 +231,7 @@ def add_context(self, uuid: str) -> None:
self.parent.add_context(uuid)
def vector_snap(
- self, vector: aird.Vec2ish, direction: aird.Vec2ish
+ self, vector: aird.Vec2ish, direction: aird.Vec2ish = (0, 0)
) -> aird.Vector2D:
"""Snap the ``vector`` into this Box, preferably into ``direction``."""
if not isinstance(vector, aird.Vector2D):
diff --git a/capellambse/aird/vector2d.py b/capellambse/aird/vector2d.py
index 9c0778984..bdae65af0 100644
--- a/capellambse/aird/vector2d.py
+++ b/capellambse/aird/vector2d.py
@@ -170,23 +170,35 @@ def boxsnap(
corner2
A Vector2D describing the second corner of the target box.
dirvec
- A Vector2D pointing in the direction to snap towards.
+ Ignored.
"""
minx = min(corner1[0], corner2[0])
miny = min(corner1[1], corner2[1])
maxx = max(corner1[0], corner2[0])
maxy = max(corner1[1], corner2[1])
- if abs(dirvec[0]) >= abs(dirvec[1]):
- # Snap horizontally
- return Vector2D(
- (minx, maxx)[dirvec[0] > 0],
- min(maxy - 1, max(miny + 1, self[1])),
- )
- # Snap vertically
- return Vector2D(
- min(maxx - 1, max(minx + 1, self[0])), (miny, maxy)[dirvec[1] > 0]
- )
+ x, y = self.x, self.y
+ if x < minx:
+ x = minx
+ elif x > maxx:
+ x = maxx
+ if y < miny:
+ y = miny
+ elif y > maxy:
+ y = maxy
+
+ point = Vector2D(x, y)
+ if x != self.x or y != self.y:
+ return point
+
+ distances = [
+ Vector2D(self.x - minx, 0),
+ Vector2D(self.x - maxx, 0),
+ Vector2D(0, self.y - miny),
+ Vector2D(0, self.y - maxy),
+ ]
+ offset = min(distances, key=lambda i: i.sqlength)
+ return point + offset
def __map(
self,
diff --git a/capellambse/model/crosslayer/cs.py b/capellambse/model/crosslayer/cs.py
index dfb6bc7d5..b208bc09c 100644
--- a/capellambse/model/crosslayer/cs.py
+++ b/capellambse/model/crosslayer/cs.py
@@ -35,29 +35,6 @@
XT_PHYS_PATH_INV = "org.polarsys.capella.core.data.cs:PhysicalPathInvolvement"
-class Component(c.GenericElement):
- """A template class for components."""
-
- is_abstract = xmltools.BooleanAttributeProperty(
- "_element",
- "abstract",
- __doc__="Boolean flag for an abstract Component",
- )
- is_human = xmltools.BooleanAttributeProperty(
- "_element", "human", __doc__="Boolean flag for a human Component"
- )
- is_actor = xmltools.BooleanAttributeProperty(
- "_element", "actor", __doc__="Boolean flag for an actor Component"
- )
-
- owner = c.ParentAccessor(c.GenericElement)
- state_machines = c.ProxyAccessor(
- capellacommon.StateMachine, aslist=c.ElementList
- )
-
- parts: c.Accessor
-
-
@c.xtype_handler(None)
class Part(c.GenericElement):
"""A representation of a physical component"""
@@ -113,6 +90,30 @@ class PhysicalLink(PhysicalPort):
physical_paths: c.Accessor
+class Component(c.GenericElement):
+ """A template class for components."""
+
+ is_abstract = xmltools.BooleanAttributeProperty(
+ "_element",
+ "abstract",
+ __doc__="Boolean flag for an abstract Component",
+ )
+ is_human = xmltools.BooleanAttributeProperty(
+ "_element", "human", __doc__="Boolean flag for a human Component"
+ )
+ is_actor = xmltools.BooleanAttributeProperty(
+ "_element", "actor", __doc__="Boolean flag for an actor Component"
+ )
+
+ owner = c.ParentAccessor(c.GenericElement)
+ state_machines = c.ProxyAccessor(
+ capellacommon.StateMachine, aslist=c.ElementList
+ )
+ ports = c.ProxyAccessor(fa.ComponentPort, aslist=c.ElementList)
+ physical_ports = c.ProxyAccessor(PhysicalPort, aslist=c.ElementList)
+ parts = c.ReferenceSearchingAccessor(Part, "type", aslist=c.ElementList)
+
+
@c.xtype_handler(None)
class PhysicalPath(c.GenericElement):
"""A physical path."""
@@ -144,11 +145,6 @@ class ComponentRealization(c.GenericElement):
_xmltag = "ownedComponentRealizations"
-c.set_accessor(
- Component,
- "parts",
- c.ReferenceSearchingAccessor(Part, "type", aslist=c.ElementList),
-)
c.set_accessor(
InterfacePkg,
"packages",
diff --git a/capellambse/model/crosslayer/fa.py b/capellambse/model/crosslayer/fa.py
index 05220ffc6..bacc009e6 100644
--- a/capellambse/model/crosslayer/fa.py
+++ b/capellambse/model/crosslayer/fa.py
@@ -55,8 +55,30 @@ class FunctionRealization(c.GenericElement):
class AbstractExchange(c.GenericElement):
"""Common code for Exchanges."""
- source_port = c.AttrProxyAccessor(c.GenericElement, "source")
- target_port = c.AttrProxyAccessor(c.GenericElement, "target")
+ source = c.AttrProxyAccessor(c.GenericElement, "source")
+ target = c.AttrProxyAccessor(c.GenericElement, "target")
+
+ @property
+ def source_port(self) -> c.GenericElement:
+ import warnings
+
+ warnings.warn(
+ "source_port is deprecated, use source instead",
+ FutureWarning,
+ stacklevel=2,
+ )
+ return self.source
+
+ @property
+ def target_port(self) -> c.GenericElement:
+ import warnings
+
+ warnings.warn(
+ "target_port is deprecated, use target instead",
+ FutureWarning,
+ stacklevel=2,
+ )
+ return self.target
@c.xtype_handler(None)
@@ -101,6 +123,18 @@ class FunctionOutputPort(FunctionPort):
)
+class Function(AbstractFunction):
+ """Common Code for Function's."""
+
+ is_leaf = property(lambda self: not self.functions)
+
+ inputs = c.ProxyAccessor(FunctionInputPort, aslist=c.ElementList)
+ outputs = c.ProxyAccessor(FunctionOutputPort, aslist=c.ElementList)
+
+ functions: c.Accessor
+ packages: c.Accessor
+
+
@c.xtype_handler(None)
class FunctionalExchange(AbstractExchange):
"""A functional exchange."""
@@ -136,7 +170,7 @@ class ComponentExchange(AbstractExchange):
_xmltag = "ownedComponentExchanges"
- func_exchanges = c.ProxyAccessor(
+ allocated_functional_exchanges = c.ProxyAccessor(
FunctionalExchange,
XT_COMP_EX_FNC_EX_ALLOC,
aslist=c.ElementList,
@@ -148,12 +182,23 @@ class ComponentExchange(AbstractExchange):
aslist=c.ElementList,
)
+ @property
+ def func_exchanges(self) -> c.ElementList[FunctionalExchange]:
+ import warnings
+
+ warnings.warn(
+ "func_exchanges is deprecated, use allocated_functional_exchanges instead",
+ FutureWarning,
+ stacklevel=2,
+ )
+ return self.allocated_functional_exchanges
+
@property
def exchange_items(
self,
) -> c.ElementList[information.ExchangeItem]:
items = self.allocated_exchange_items
- func_exchanges = self.func_exchanges
+ func_exchanges = self.allocated_functional_exchanges
assert isinstance(func_exchanges, cabc.Iterable)
for exchange in func_exchanges:
items += exchange.exchange_items
@@ -169,10 +214,7 @@ def exchange_items(
_port,
"exchanges",
c.ReferenceSearchingAccessor(
- _exchange,
- "source_port",
- "target_port",
- aslist=c.ElementList,
+ _exchange, "source", "target", aslist=c.ElementList
),
)
del _port, _exchange
diff --git a/capellambse/model/layers/ctx.py b/capellambse/model/layers/ctx.py
index 4902bfb04..5fb6753e7 100644
--- a/capellambse/model/layers/ctx.py
+++ b/capellambse/model/layers/ctx.py
@@ -30,14 +30,11 @@
@c.xtype_handler(XT_ARCH)
-class SystemFunction(fa.AbstractFunction):
+class SystemFunction(fa.Function):
"""A system function."""
_xmltag = "ownedFunctions"
- inputs = c.ProxyAccessor(fa.FunctionInputPort, aslist=c.ElementList)
- outputs = c.ProxyAccessor(fa.FunctionOutputPort, aslist=c.ElementList)
- is_leaf = property(lambda self: not self.functions)
realized_operational_activities = c.ProxyAccessor(
oa.OperationalActivity,
fa.FunctionRealization,
@@ -46,8 +43,6 @@ class SystemFunction(fa.AbstractFunction):
)
owner: c.Accessor
- functions: c.Accessor
- packages: c.Accessor
@c.xtype_handler(XT_ARCH)
diff --git a/capellambse/model/layers/la.py b/capellambse/model/layers/la.py
index 7a71046c1..2c9394e77 100644
--- a/capellambse/model/layers/la.py
+++ b/capellambse/model/layers/la.py
@@ -28,14 +28,11 @@
@c.xtype_handler(XT_ARCH)
-class LogicalFunction(fa.AbstractFunction):
+class LogicalFunction(fa.Function):
"""A logical function on the Logical Architecture layer."""
_xmltag = "ownedLogicalFunctions"
- inputs = c.ProxyAccessor(fa.FunctionInputPort, aslist=c.ElementList)
- outputs = c.ProxyAccessor(fa.FunctionOutputPort, aslist=c.ElementList)
- is_leaf = property(lambda self: not self.functions)
realized_system_functions = c.ProxyAccessor(
ctx.SystemFunction,
fa.FunctionRealization,
@@ -43,8 +40,6 @@ class LogicalFunction(fa.AbstractFunction):
follow="targetElement",
)
- functions: c.Accessor
-
@property
def owner(self) -> LogicalComponent | None:
try:
@@ -86,7 +81,6 @@ class LogicalComponent(cs.Component):
aslist=c.ElementList,
follow="targetElement",
)
- ports = c.ProxyAccessor(fa.ComponentPort, aslist=c.ElementList)
components: c.Accessor
@@ -251,6 +245,14 @@ class LogicalArchitecture(crosslayer.BaseArchitectureLayer):
aslist=c.ElementList,
),
)
+c.set_accessor(
+ LogicalFunction,
+ "packages",
+ c.ProxyAccessor(
+ LogicalFunctionPkg,
+ aslist=c.ElementList,
+ ),
+)
c.set_self_references(
(LogicalComponent, "components"),
(LogicalComponentPkg, "packages"),
diff --git a/capellambse/model/layers/oa.py b/capellambse/model/layers/oa.py
index d9952ecc1..194c22a4c 100644
--- a/capellambse/model/layers/oa.py
+++ b/capellambse/model/layers/oa.py
@@ -44,6 +44,18 @@ class OperationalActivity(fa.AbstractFunction):
matchtransform=operator.attrgetter("activities"),
)
+ @property
+ def inputs(self) -> c.ElementList[fa.FunctionalExchange]:
+ return self._model.oa.all_activity_exchanges.by_target(self)
+
+ @property
+ def outputs(self) -> c.ElementList[fa.FunctionalExchange]:
+ return self._model.oa.all_activity_exchanges.by_source(self)
+
+ @property
+ def exchanges(self) -> c.ElementList[fa.FunctionalExchange]:
+ return self.inputs + self.outputs
+
@c.xtype_handler(XT_ARCH)
class OperationalProcess(c.GenericElement):
@@ -133,6 +145,18 @@ class Entity(AbstractEntity):
entities: c.Accessor
+ @property
+ def inputs(self) -> c.ElementList[fa.FunctionalExchange]:
+ return self._model.oa.all_entity_exchanges.by_target(self)
+
+ @property
+ def outputs(self) -> c.ElementList[fa.FunctionalExchange]:
+ return self._model.oa.all_entity_exchanges.by_source(self)
+
+ @property
+ def exchanges(self) -> c.ElementList[fa.FunctionalExchange]:
+ return self.inputs + self.outputs
+
@c.xtype_handler(XT_ARCH)
class OperationalActivityPkg(c.GenericElement):
@@ -146,14 +170,11 @@ class OperationalActivityPkg(c.GenericElement):
@c.xtype_handler(XT_ARCH)
-class CommunicationMean(c.GenericElement):
+class CommunicationMean(fa.AbstractExchange):
"""An operational entity exchange"""
_xmltag = "ownedComponentExchanges"
- source = c.AttrProxyAccessor(c.GenericElement, "source")
- target = c.AttrProxyAccessor(c.GenericElement, "target")
-
allocated_interactions = c.ProxyAccessor(
fa.FunctionalExchange,
fa.XT_COMP_EX_FNC_EX_ALLOC,
@@ -246,6 +267,14 @@ class OperationalAnalysis(crosslayer.BaseArchitectureLayer):
aslist=c.ElementList,
),
)
+c.set_accessor(
+ OperationalActivity,
+ "packages",
+ c.ProxyAccessor(
+ OperationalActivityPkg,
+ aslist=c.ElementList,
+ ),
+)
c.set_self_references(
(OperationalActivityPkg, "packages"),
(OperationalCapabilityPkg, "packages"),
diff --git a/capellambse/model/layers/pa.py b/capellambse/model/layers/pa.py
index 47cc8761c..0646cdf51 100644
--- a/capellambse/model/layers/pa.py
+++ b/capellambse/model/layers/pa.py
@@ -34,13 +34,11 @@
@c.xtype_handler(XT_ARCH)
-class PhysicalFunction(fa.AbstractFunction):
+class PhysicalFunction(fa.Function):
"""A physical function on the Physical Architecture layer."""
_xmltag = "ownedPhysicalFunctions"
- inputs = c.ProxyAccessor(fa.FunctionInputPort, aslist=c.ElementList)
- outputs = c.ProxyAccessor(fa.FunctionOutputPort, aslist=c.ElementList)
owner = c.CustomAccessor(
c.GenericElement,
operator.attrgetter("_model.pa.all_components"),
@@ -53,10 +51,6 @@ class PhysicalFunction(fa.AbstractFunction):
follow="targetElement",
)
- is_leaf = property(lambda self: not self.functions)
-
- functions: c.Accessor
-
@c.xtype_handler(XT_ARCH)
class PhysicalFunctionPkg(c.GenericElement):
@@ -94,7 +88,6 @@ class PhysicalComponent(cs.Component):
aslist=c.ElementList,
follow="targetElement",
)
- ports = c.ProxyAccessor(cs.PhysicalPort, aslist=c.ElementList)
owned_components: c.Accessor
deploying_components: c.Accessor
@@ -214,6 +207,14 @@ class PhysicalArchitecture(crosslayer.BaseArchitectureLayer):
aslist=c.ElementList,
),
)
+c.set_accessor(
+ PhysicalFunction,
+ "packages",
+ c.ProxyAccessor(
+ PhysicalFunctionPkg,
+ aslist=c.ElementList,
+ ),
+)
c.set_self_references(
(PhysicalComponent, "owned_components"),
(PhysicalComponentPkg, "packages"),
diff --git a/capellambse/svg/decorations.py b/capellambse/svg/decorations.py
index 8157afbaf..0f9465bc5 100644
--- a/capellambse/svg/decorations.py
+++ b/capellambse/svg/decorations.py
@@ -37,16 +37,16 @@
"LogicalHumanComponent",
}
only_icons = {"Requirement"}
-# TODO: Instead of dirty-patching either rename FunctionalExchange in OA-Layer as
-# ActivityExchange/OperationalExchange
DiagramClass = str
FaultyClass = str
PatchClass = str
-
-needs_patch: dict[DiagramClass, dict[FaultyClass, PatchClass]] = {
- "Operational Entity Blank": {"FunctionalExchange": "OperationalExchange"}
+always_top_label = {
+ "Class",
+ "Enumeration",
+ "Note",
+ "OperationalActivity",
+ "PhysicalComponent",
}
-always_top_label = {"Note", "Class", "Enumeration"}
needs_feature_line = {"Class", "Enumeration"}
diff --git a/capellambse/svg/drawing.py b/capellambse/svg/drawing.py
index f29932ab5..920a2385a 100644
--- a/capellambse/svg/drawing.py
+++ b/capellambse/svg/drawing.py
@@ -62,9 +62,6 @@ def __init__(self, metadata: generate.DiagramMetadata):
self.diagram_class = metadata.class_
self.stylesheet = self.make_stylesheet()
self.obj_cache: dict[str | None, t.Any] = {}
- self.requires_deco_patch = decorations.needs_patch.get(
- self.diagram_class, {}
- )
self.add_backdrop(pos=metadata.pos, size=metadata.size)
def add_backdrop(
@@ -536,7 +533,13 @@ def _draw_symbol(
if class_ in decorations.all_ports:
grp = self.add_port(
- pos, size, text_style, parent_, class_=class_, label=label_
+ pos,
+ size,
+ text_style,
+ parent_,
+ class_=class_,
+ label=label_,
+ id_=id_,
)
else:
grp = self.g(class_=f"Box {class_}", id_=id_)
@@ -684,7 +687,6 @@ def _draw_edge(
# Received text space doesn't allow for anything else than the text
for label_ in labels_:
label_["class"] = "Annotation"
-
self._draw_label_bbox(label_, grp, "AnnotationBB")
self._draw_edge_label(
label_,
@@ -707,9 +709,9 @@ def _draw_edge_label(
text_anchor: str = "start",
y_margin: int | float,
) -> container.Group:
- if class_ in self.requires_deco_patch:
- class_ = self.requires_deco_patch[class_]
-
+ class_ = (
+ style.get_symbol_styleclass(class_, self.diagram_class) or class_
+ )
if f"{class_}Symbol" in decorations.deco_factories:
additional_space = (
decorations.icon_size + 2 * decorations.icon_padding
diff --git a/capellambse/svg/generate.py b/capellambse/svg/generate.py
index c939a9eb0..ed8095938 100644
--- a/capellambse/svg/generate.py
+++ b/capellambse/svg/generate.py
@@ -16,6 +16,7 @@
import collections.abc as cabc
import dataclasses
import json
+import os
import pathlib
import typing as t
@@ -98,7 +99,7 @@ def from_json(cls, jsonstring: str) -> SVGDiagram:
return cls(metadata, jsondict["contents"])
@classmethod
- def from_json_path(cls, path: str) -> SVGDiagram:
+ def from_json_path(cls, path: str | os.PathLike) -> SVGDiagram:
"""Create an SVGDiagram from the given JSON file.
Parameters
diff --git a/capellambse/svg/style.py b/capellambse/svg/style.py
index 6d56a90ac..2b5f9b6fa 100644
--- a/capellambse/svg/style.py
+++ b/capellambse/svg/style.py
@@ -95,6 +95,10 @@
"OrControlNodeSymbol",
"OperationalExchangeSymbol",
),
+ "Operational Activity Interaction Blank": (
+ "OperationalActivitySymbol",
+ "OperationalExchangeSymbol",
+ ),
"Physical Architecture Blank": (
"PhysicalLinkSymbol",
"ComponentExchangeSymbol",
@@ -119,6 +123,28 @@
"SystemHumanActorSymbol",
),
}
+MODIFY_STYLECLASS = {"FunctionalExchange"}
+
+
+def get_symbol_styleclass(style: str | None, dstyle: str) -> str | None:
+ if (
+ style not in MODIFY_STYLECLASS
+ or dstyle not in STATIC_DECORATIONS
+ or style in STATIC_DECORATIONS[dstyle]
+ ):
+ return None
+
+ capitals = dstyle.split(" ")
+ assert capitals
+ layer = capitals[0]
+ if style.startswith(layer):
+ return None
+
+ scapitals = re.findall(r"[A-Z][^A-Z]*", style)
+ symbol = f'{layer}{"".join(scapitals[1:])}'
+ if f"{symbol}Symbol" in STATIC_DECORATIONS[dstyle]:
+ return symbol
+ return None
class Styling:
diff --git a/tests/data/melodymodel/5_0/Melody Model Test.aird b/tests/data/melodymodel/5_0/Melody Model Test.aird
index 175a3a6dc..efb77d9e0 100644
--- a/tests/data/melodymodel/5_0/Melody Model Test.aird
+++ b/tests/data/melodymodel/5_0/Melody Model Test.aird
@@ -87,6 +87,10 @@
+
+
+
+
@@ -97,7 +101,7 @@
-
+
@@ -105,6 +109,10 @@
+
+
+
+
@@ -135,10 +143,14 @@
-
+
+
+
+
+
@@ -8081,6 +8093,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -8167,6 +8190,15 @@
+
+
+
+
+
+
+
+
+
backgroundColor
@@ -8805,6 +8837,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -8968,6 +9033,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -12607,4 +12696,1429 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ routingStyle
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+
+
+
+
+
+
+
+
+ borderColor
+ borderSize
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+ borderColor
+ borderSize
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+ borderColor
+ borderSize
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+ borderColor
+ borderSize
+
+
+
+
+
+
+
+
+ borderColor
+ borderSize
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ strokeColor
+ size
+
+
+
+
+
+
+
+
+
+ strokeColor
+ size
+
+
+
+
+
+
+
+
+
+ strokeColor
+ size
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ strokeColor
+ size
+
+
+
+
+
+
+
+
+
+ strokeColor
+ size
+
+
+
+
+
+
+
+
+
+ strokeColor
+ size
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ color
+
+
+
+
+
+
+
+ KEEP_LOCATION
+ KEEP_SIZE
+ KEEP_RATIO
+
+ color
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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/melodymodel/5_0/Melody Model Test.capella b/tests/data/melodymodel/5_0/Melody Model Test.capella
index 004f96101..a1a8743c3 100644
--- a/tests/data/melodymodel/5_0/Melody Model Test.capella
+++ b/tests/data/melodymodel/5_0/Melody Model Test.capella
@@ -1135,6 +1135,27 @@ The predator is far away
+
+
+
+
+
+
+
+
+
+
+
+
id="fe4cdb52-906d-49f2-83c6-449e5c6642af" targetElement="#b8f34893-c50b-4b7d-9326-1d01990434d8"
sourceElement="#1a414995-f4cd-488c-8152-486e459fb9de"/>
+
+
+
+ name="HollyWood" abstractType="#d4a22478-5717-4ca7-bfc9-9a193e6218a8"/>
+ name="Affleck" abstractType="#da12377b-fb70-4441-8faa-3a5c153c5de2"/>
+
+
+
+ human="true">
+
+
+
+
+
+ id="d4a22478-5717-4ca7-bfc9-9a193e6218a8" name="HollyWood" actor="true">
+
+ id="da12377b-fb70-4441-8faa-3a5c153c5de2" name="Affleck" actor="true">
+
+
@@ -2029,7 +2090,14 @@ The predator is far away
id="145327ea-d6eb-4ad2-abe5-79876938b0a7" name="First Class">
+ human="true">
+
+
+
+
+
+
+
-
-
-
-
diff --git a/tests/test_model_layers.py b/tests/test_model_layers.py
index eaaea23e5..b7aa9f26e 100644
--- a/tests/test_model_layers.py
+++ b/tests/test_model_layers.py
@@ -25,7 +25,8 @@
StateTransition,
)
from capellambse.model.crosslayer.capellacore import Constraint
-from capellambse.model.crosslayer.fa import FunctionOutputPort
+from capellambse.model.crosslayer.cs import PhysicalPort
+from capellambse.model.crosslayer.fa import ComponentPort
from capellambse.model.crosslayer.information import Class
from capellambse.model.layers.ctx import SystemComponentPkg
from capellambse.model.layers.la import CapabilityRealization
@@ -705,3 +706,37 @@ def test_pa_component_exchange_is_found(self, model: MelodyModel) -> None:
"3aa006b1-f954-4e8f-a4e9-2e9cd38555de"
)
assert expected_exchange in model.pa.all_component_exchanges
+
+ @pytest.mark.parametrize(
+ "uuid,port_attr,ports,class_",
+ [
+ pytest.param(
+ "b51ccc6f-5f96-4e28-b90e-72463a3b50cf",
+ "physical_ports",
+ 3,
+ PhysicalPort,
+ id="PP",
+ ),
+ pytest.param(
+ "c78b5d7c-be0c-4ed4-9d12-d447cb39304e",
+ "ports",
+ 3,
+ ComponentPort,
+ id="CP",
+ ),
+ ],
+ )
+ def test_pa_component_finds_ports(
+ self,
+ model: MelodyModel,
+ uuid: str,
+ port_attr: str,
+ ports: int,
+ class_: type,
+ ) -> None:
+ comp = model.by_uuid(uuid)
+ port_list = getattr(comp, port_attr)
+
+ assert ports == len(port_list)
+ for p in port_list:
+ assert isinstance(p, class_)
diff --git a/tests/test_svg.py b/tests/test_svg.py
index f57efafcf..536884e69 100644
--- a/tests/test_svg.py
+++ b/tests/test_svg.py
@@ -14,14 +14,12 @@
from __future__ import annotations
import collections.abc as cabc
-import io
import json
import logging
import math
import pathlib
import random
import string
-import sys
import cssutils
import pytest
@@ -38,8 +36,6 @@
symbols,
)
-from . import TEST_MODEL, TEST_ROOT
-
cssutils.log.setLevel(logging.CRITICAL)
TEST_LAB = "[LAB] Wizzard Education"
@@ -67,15 +63,8 @@
}
-@pytest.fixture
-def model(monkeypatch) -> capellambse.MelodyModel:
- """Return test model"""
- monkeypatch.setattr(sys, "stderr", io.StringIO)
- return capellambse.MelodyModel(TEST_ROOT / "5_0" / TEST_MODEL)
-
-
-@pytest.fixture
-def tmp_json(
+@pytest.fixture(name="tmp_json")
+def tmp_json_fixture(
model: capellambse.MelodyModel, tmp_path: pathlib.Path
) -> pathlib.Path:
"""Return tmp path of diagram json file"""
@@ -85,9 +74,10 @@ def tmp_json(
return dest
-@pytest.mark.usefixtures("tmp_json")
class TestSVG:
- def test_diagram_meta_data_attributes(self, tmp_json) -> None:
+ def test_diagram_meta_data_attributes(
+ self, tmp_json: pathlib.Path
+ ) -> None:
diag_meta = generate.DiagramMetadata.from_dict(
json.loads(tmp_json.read_text())
)
@@ -97,7 +87,9 @@ def test_diagram_meta_data_attributes(self, tmp_json) -> None:
assert diag_meta.viewbox == "10 10 1162 611"
assert diag_meta.class_ == "Logical Architecture Blank"
- def test_diagram_from_json_path_componentports(self, tmp_json) -> None:
+ def test_diagram_from_json_path_componentports(
+ self, tmp_json: pathlib.Path
+ ) -> None:
tree = etree.fromstring(
SVGDiagram.from_json_path(tmp_json).to_string()
)
@@ -136,7 +128,7 @@ def test_diagram_from_json_path_componentports(self, tmp_json) -> None:
assert cp_reference_exists
@pytest.fixture
- def tmp_svg(self, tmp_path) -> SVGDiagram:
+ def tmp_svg(self, tmp_path: pathlib.Path) -> SVGDiagram:
name = "Test svg"
meta = generate.DiagramMetadata(
pos=(0, 0), size=(1, 1), name=name, class_="TEST"
@@ -150,7 +142,7 @@ def test_diagram_saves(self, tmp_svg: SVGDiagram) -> None:
assert pathlib.Path(tmp_svg.drawing.filename).is_file()
# FIXME: change this to a parametrized test, do not use if- or for-statements in a unit test
- def test_css_colors(self, tmp_json) -> None:
+ def test_css_colors(self, tmp_json: pathlib.Path) -> None:
COLORS_TO_CHECK = {
".LogicalArchitectureBlank g.Box.CP_IN > line": {
"stroke": "#000000"
@@ -370,12 +362,12 @@ def test_css_colors(self, tmp_json) -> None:
tree = etree.fromstring(
SVGDiagram.from_json_path(tmp_json).to_string()
)
- style = tree.xpath(
+ style_ = tree.xpath(
"/x:svg/x:defs/x:style",
namespaces={"x": "http://www.w3.org/2000/svg"},
)[0]
- stylesheet = cssutils.parseString(style.text)
+ stylesheet = cssutils.parseString(style_.text)
for rule in stylesheet:
for (element, prop) in COLORS_TO_CHECK.items():
if (
@@ -407,7 +399,7 @@ def test_diagram_decorations(
):
"""Test diagrams get rendered successfully"""
diag = model.diagrams.by_name(diagram_name)
- svg_string = diag.render("svg")
+ diag.render("svg")
@pytest.mark.parametrize("diagram_type", TEST_DECO)
@pytest.mark.parametrize(
@@ -583,20 +575,43 @@ def check_label_for_containment_and_overlap(
assert symbol.attribs["y"] + symbol.attribs["height"] <= lower_bound
-class TestSVGStylesheet:
- def test_svg_stylesheet_as_str(self, tmp_json) -> None:
- svg = SVGDiagram.from_json_path(tmp_json)
- for line in str(svg.drawing.stylesheet).splitlines():
- assert line.startswith(".LogicalArchitectureBlank")
+class TestSVGStyling:
+ LAB = "Logical Architecture Blank"
+ FCD = "Functional Chain Description"
+ OEB = "Operational Entity Blank"
+ FEX = "FunctionalExchange"
+ OEX = "OperationalExchange"
+ BOX = "LogicalComponentSymbol"
- def test_svg_stylesheet_builder_fails_when_no_class_was_given(self):
- with pytest.raises(TypeError) as error:
- style.SVGStylesheet(None) # type: ignore[arg-type]
+ class TestSVGStylesheet:
+ def test_svg_stylesheet_as_str(self, tmp_json) -> None:
+ svg = SVGDiagram.from_json_path(tmp_json)
+ for line in str(svg.drawing.stylesheet).splitlines():
+ assert line.startswith(".LogicalArchitectureBlank")
- assert (
- error.value.args[0]
- == "Invalid type for class_ 'NoneType'. This needs to be a str."
- )
+ def test_svg_stylesheet_builder_fails_when_no_class_was_given(self):
+ with pytest.raises(TypeError) as error:
+ style.SVGStylesheet(None) # type: ignore[arg-type]
+
+ assert (
+ error.value.args[0]
+ == "Invalid type for class_ 'NoneType'. This needs to be a str."
+ )
+
+ @pytest.mark.parametrize(
+ "style_,diagstyle,expected",
+ [
+ pytest.param(None, "None", None, id="No modify"),
+ pytest.param(FEX, "None", None, id="Unregistered diagramstyle"),
+ pytest.param(BOX, LAB, None, id="Found in deco"),
+ pytest.param(FEX, FCD, None, id="Layer is prefix"),
+ pytest.param(FEX, OEB, OEX, id="Fixed FExchange styleclass"),
+ ],
+ )
+ def test_get_symbol_styleclass(
+ self, style_: str | None, diagstyle: str, expected: str | None
+ ) -> None:
+ assert style.get_symbol_styleclass(style_, diagstyle) == expected
class TestDecoFactory: