Skip to content

Layout

pd2af.building.layout

Layout helpers shared by both language builders: styling, arcs and dot.

Functions:

Name Description
harmonize_root_layout

Set the root layout's fill to white and fit it around its children.

highlight_layout_elements

Dim everything in layout that is not part of layout_elements.

influence_layout_pairs

Return the (source_layout, target_layout) pairs an arc is drawn between.

make_arc_segments_from_source_and_target

Segments for a modulation / influence arc connecting two node layouts.

make_auto_layout

Reposition an already-built layout with graphviz (dot).

nearest_layout_pair

Return the (source_layout, target_layout) pair closest in Euclidean distance.

harmonize_root_layout

harmonize_root_layout(layout_builder: Any) -> None

Set the root layout's fill to white and fit it around its children.

Applied uniformly across all layout modes so the rendered canvas has consistent background and padding.

Source code in src/pd2af/building/layout.py
def harmonize_root_layout(layout_builder: typing.Any) -> None:
    """Set the root layout's fill to white and fit it around its children.

    Applied uniformly across all layout modes so the rendered canvas has
    consistent background and padding.
    """
    layout_builder.fill = momapy.coloring.white
    if not layout_builder.layout_elements:
        # An empty output map (e.g. every activity suppressed via
        # `--set-all-inactive`) has nothing to fit around; `set_fit` raises
        # on an empty element list. Give the root a minimal, valid canvas so
        # the map still has numeric width/height/position for the writer.
        side = 2 * _ROOT_LAYOUT_SEP
        layout_builder.width = side
        layout_builder.height = side
        layout_builder.position = momapy.geometry.Point(
            _ROOT_LAYOUT_SEP, _ROOT_LAYOUT_SEP
        )
        return
    momapy.positioning.set_fit(
        layout_builder,
        layout_builder.layout_elements,
        xsep=_ROOT_LAYOUT_SEP,
        ysep=_ROOT_LAYOUT_SEP,
    )

highlight_layout_elements

highlight_layout_elements(layout_elements: Any, layout: Any) -> Any

Dim everything in layout that is not part of layout_elements.

The overlay mode's greying pass: a style sheet selects every element whose id is outside the kept set and washes it out, leaving the foreground drawn at full strength.

Source code in src/pd2af/building/layout.py
def highlight_layout_elements(
    layout_elements: typing.Any, layout: typing.Any
) -> typing.Any:
    """Dim everything in ``layout`` that is not part of ``layout_elements``.

    The overlay mode's greying pass: a style sheet selects every element whose
    id is outside the kept set and washes it out, leaving the foreground drawn
    at full strength.
    """
    keep_ids = set()
    for layout_element in layout_elements:
        keep_ids.add(layout_element.id_)
        for descendant in layout_element.descendants():
            keep_ids.add(descendant.id_)
    not_selector = _NotInIdSetSelector(frozenset(keep_ids))

    def make_selector(selector: typing.Any) -> typing.Any:
        return momapy.styling.CompoundSelector((selector, not_selector))

    layout_element_selector = make_selector(
        momapy.styling.ClassSelector("LayoutElement")
    )
    active_border_selector = make_selector(_ClassNameSuffixSelector("ActiveLayout"))
    text_layout_selector = make_selector(momapy.styling.TypeSelector("TextLayout"))
    production_layout_selector = make_selector(
        momapy.styling.TypeSelector("ProductionLayout")
    )
    compartment_layout_selector = make_selector(
        momapy.styling.TypeSelector("RectangleCompartmentLayout")
    )
    reaction_layout_selector = make_selector(
        momapy.styling.OrSelector(
            tuple(
                [
                    momapy.styling.TypeSelector(class_name)
                    for class_name in [
                        "StateTransitionLayout",
                        "HeterodimerAssociationLayout",
                        "KnownTransitionOmittedLayout",
                        "UnknownTransitionLayout",
                        "TransportLayout",
                        "TranslationLayout",
                        "TranscriptionLayout",
                    ]
                ]
            )
        )
    )
    style_sheet = momapy.styling.StyleSheet(
        {
            layout_element_selector: momapy.styling.StyleCollection(
                {
                    "stroke": None,
                    "fill": momapy.coloring.white,
                    "path_stroke": None,
                    "end_arrowhead_stroke": None,
                    "start_arrowhead_stroke": None,
                    "arrowhead_stroke": None,
                    "reaction_node_stroke": None,
                    "active_stroke": None,
                    "inner_stroke": None,
                    "group_stroke": momapy.coloring.lightgray,
                }
            ),
            # Active-border sublayouts (`*ActiveLayout`) are transparent by
            # default and drawn larger, on top of the species body. The blanket
            # rule above sets `fill: white`, which makes the border opaque and
            # paints over the body; restore transparency so the dimmed body
            # shows through. Must follow `layout_element_selector`: matching
            # selectors apply in dict order with no specificity, last wins.
            active_border_selector: momapy.styling.StyleCollection(
                {
                    "fill": momapy.drawing.NoneValue,
                }
            ),
            text_layout_selector: momapy.styling.StyleCollection(
                {
                    "stroke": momapy.drawing.NoneValue,
                    "fill": momapy.coloring.lightgray,
                }
            ),
            production_layout_selector: momapy.styling.StyleCollection(
                {
                    "arrowhead_stroke": momapy.coloring.lightgray,
                    "arrowhead_fill": momapy.coloring.lightgray,
                }
            ),
            compartment_layout_selector: momapy.styling.StyleCollection(
                {
                    "fill": momapy.coloring.lightgray,
                }
            ),
            reaction_layout_selector: momapy.styling.StyleCollection(
                {
                    "end_arrowhead_stroke": momapy.coloring.lightgray,
                    "end_arrowhead_fill": momapy.coloring.lightgray,
                    "start_arrowhead_stroke": momapy.coloring.lightgray,
                    "start_arrowhead_fill": momapy.coloring.lightgray,
                    "_font_fill": momapy.coloring.lightgray,
                }
            ),
        }
    )
    layout = momapy.styling.apply_style_sheet(layout, style_sheet, strict=False)
    return layout

influence_layout_pairs

influence_layout_pairs(source_layouts: Any, target_layouts: Any, prefer_nearest: bool) -> list[tuple]

Return the (source_layout, target_layout) pairs an arc is drawn between.

By default the full source x target cross product. prefer_nearest (-p nearest) collapses the fan-out to the single closest pair, but only where there is fan-out (more than one layout on a side) and every layout has a real position -- in dot mode positions are throwaway placeholders that graphviz overwrites, so the cross product is kept.

Source code in src/pd2af/building/layout.py
def influence_layout_pairs(
    source_layouts: typing.Any, target_layouts: typing.Any, prefer_nearest: bool
) -> list[tuple]:
    """Return the (source_layout, target_layout) pairs an arc is drawn between.

    By default the full ``source x target`` cross product. ``prefer_nearest``
    (``-p nearest``) collapses the fan-out to the single closest pair, but only
    where there *is* fan-out (more than one layout on a side) and every layout
    has a real ``position`` -- in ``dot`` mode positions are throwaway
    placeholders that graphviz overwrites, so the cross product is kept.
    """
    use_nearest = (
        prefer_nearest
        and (len(source_layouts) > 1 or len(target_layouts) > 1)
        and all(
            layout.position is not None
            for layout in itertools.chain(source_layouts, target_layouts)
        )
    )
    if use_nearest:
        return [nearest_layout_pair(source_layouts, target_layouts)]
    return list(itertools.product(source_layouts, target_layouts))

make_arc_segments_from_source_and_target

make_arc_segments_from_source_and_target(source_layout: Any, target_layout: Any) -> list

Segments for a modulation / influence arc connecting two node layouts.

A self-loop (source is target) bows out into a visible loop via :func:_make_self_loop_segments; otherwise it is drawn straight by :func:_make_straight_segments. Returns a list of segments.

Source code in src/pd2af/building/layout.py
def make_arc_segments_from_source_and_target(
    source_layout: typing.Any, target_layout: typing.Any
) -> list:
    """Segments for a modulation / influence arc connecting two node layouts.

    A self-loop (source is target) bows out into a visible loop via
    :func:`_make_self_loop_segments`; otherwise it is drawn straight by
    :func:`_make_straight_segments`. Returns a list of segments.
    """
    if source_layout is target_layout:
        start_angle, end_angle = _make_self_loop_angles(0, 1)
        return _make_self_loop_segments(source_layout, start_angle, end_angle)
    return _make_straight_segments(source_layout, target_layout)

make_auto_layout

make_auto_layout(cd_map: Any, compartment_layout_classes: Any = (RectangleCompartmentLayout, OvalCompartmentLayout), reversed_arc_classes: Any = (), operator_arc_resolver: Any = None) -> Any

Reposition an already-built layout with graphviz (dot).

Generic over the map language except for the compartment-layout classes, which differ per language: pass compartment_layout_classes to identify the compartment containers (so they become dot clusters rather than nodes). Defaults to the CellDesigner compartment-layout classes.

reversed_arc_classes flips the dot-edge direction of the given arc classes for ranking only (see :func:_build_dot_graph); operator_arc_resolver overrides the rebuilt segments of selected arcs (see :func:_rebuild_arc_geometry). Both default to inert, so callers that pass neither (e.g. CellDesigner) are unaffected.

Source code in src/pd2af/building/layout.py
def make_auto_layout(
    cd_map: typing.Any,
    compartment_layout_classes: typing.Any = (
        momapy.celldesigner.RectangleCompartmentLayout,
        momapy.celldesigner.OvalCompartmentLayout,
    ),
    reversed_arc_classes: typing.Any = (),
    operator_arc_resolver: typing.Any = None,
) -> typing.Any:
    """Reposition an already-built layout with graphviz (dot).

    Generic over the map language except for the compartment-layout classes,
    which differ per language: pass ``compartment_layout_classes`` to identify
    the compartment containers (so they become dot clusters rather than nodes).
    Defaults to the CellDesigner compartment-layout classes.

    ``reversed_arc_classes`` flips the dot-edge direction of the given arc
    classes for ranking only (see :func:`_build_dot_graph`);
    ``operator_arc_resolver`` overrides the rebuilt segments of selected arcs
    (see :func:`_rebuild_arc_geometry`). Both default to inert, so callers
    that pass neither (e.g. CellDesigner) are unaffected.
    """
    new_map_builder = momapy.builder.builder_from_object(cd_map)
    new_layout_builder = new_map_builder.layout
    (
        dot_graph,
        id_to_layout_element,
        descendant_id_to_top_level_id,
        dot_cluster_name_to_compartment_layout_element,
    ) = _build_dot_graph(
        new_map_builder,
        new_layout_builder,
        compartment_layout_classes,
        reversed_arc_classes=reversed_arc_classes,
    )
    id_to_new_layout_element_builder = _reposition_from_dot(
        dot_graph,
        id_to_layout_element,
        dot_cluster_name_to_compartment_layout_element,
    )
    _rebuild_arc_geometry(
        new_layout_builder,
        id_to_new_layout_element_builder,
        descendant_id_to_top_level_id,
        operator_arc_resolver=operator_arc_resolver,
    )
    harmonize_root_layout(new_layout_builder)
    new_map = momapy.builder.object_from_builder(new_map_builder)
    return new_map

nearest_layout_pair

nearest_layout_pair(source_layouts: Any, target_layouts: Any) -> tuple

Return the (source_layout, target_layout) pair closest in Euclidean distance.

min over itertools.product is deterministic: on a distance tie it keeps the first pair in product order (source-major), so the choice mirrors the order the cross product would have emitted.

Source code in src/pd2af/building/layout.py
def nearest_layout_pair(
    source_layouts: typing.Any, target_layouts: typing.Any
) -> tuple:
    """Return the (source_layout, target_layout) pair closest in Euclidean distance.

    ``min`` over ``itertools.product`` is deterministic: on a distance tie it
    keeps the first pair in product order (source-major), so the choice mirrors
    the order the cross product would have emitted.
    """
    return min(
        itertools.product(source_layouts, target_layouts),
        key=lambda pair: math.dist(
            pair[0].position.to_tuple(), pair[1].position.to_tuple()
        ),
    )