Skip to content

Display geometry

The mccode_antlr.display submodule parses a component's MCDISPLAY section into a symbolic Python geometry model backed by Expr. The geometry can be evaluated at any parameter values and rendered in 3-D — without compiling the instrument.

Quick start

from mccode_antlr.loader import load_mcstas_instr
from mccode_antlr.display import InstrumentDisplay
from mccode_antlr.display.render.matplotlib import plot_geometry
import matplotlib.pyplot as plt

instr = load_mcstas_instr('my_instrument.instr')
disp  = InstrumentDisplay(instr)
polys = disp.to_polylines({'E_i': 5.0})   # dict[name → list[np.ndarray]]

fig, ax = plot_geometry(polys)
plt.show()

Single-component use:

from mccode_antlr.display import ComponentDisplay

cd   = ComponentDisplay(comp)
pls  = cd.to_polylines({'xwidth': 0.05, 'yheight': 0.1})

Architecture

Comp.display (tuple[RawC])
      ▼  parse_display_source()  ←  DisplayVisitor wraps C99 CParser
  list[Primitive]   ← every argument is an Expr
      ├──  ComponentDisplay.to_polylines(params) → list[np.ndarray]
  InstrumentDisplay.to_polylines(params, global_frame=True)
  (applies Orient: Rotation × pts + translation, all Expr-backed)
  dict[comp_name → list[np.ndarray (N,3)]]
      ├──▶  render/matplotlib.py   (zero new hard deps)
      └──▶  render/threejs.py      (soft-import pythreejs / K3D)

The parsing is done by DisplayVisitor, a subclass of the existing C99 CVisitor ANTLR visitor. It handles:

  • Simple callscircle("xy", 0, 0, 0, r);
  • Math in argumentsmultiline(5, -xw/2, -yh/2, 0, ...);
  • Local variable declarationsdouble t = height/2; line(0,0,-t, 0,0,t);
  • Conditionalsif (show_guide) { rectangle(...); }ConditionalBlock
  • Loopsfor (int i = ...) { ... }LoopBlock placeholder

API reference

parse_display_source

mccode_antlr.display.visitor.parse_display_source(source, local_vars=None)

Parse the raw C source of a MCDISPLAY body into geometry primitives.

Parameters

source: The raw C text found between %{ and %} in a component MCDISPLAY section. local_vars: Optional pre-defined variable bindings (e.g. from the component's SETTING PARAMETERS made available to the display function).

Returns

list[Primitive | ConditionalBlock | LoopBlock] The extracted geometry primitives in source order.

Source code in src/mccode_antlr/display/visitor.py
def parse_display_source(
    source: str,
    local_vars: dict[str, Expr] | None = None,
) -> list[AnyBlock]:
    """Parse the raw C source of a MCDISPLAY body into geometry primitives.

    Parameters
    ----------
    source:
        The raw C text found between ``%{`` and ``%}`` in a component
        MCDISPLAY section.
    local_vars:
        Optional pre-defined variable bindings (e.g. from the component's
        SETTING PARAMETERS made available to the display function).

    Returns
    -------
    list[Primitive | ConditionalBlock | LoopBlock]
        The extracted geometry primitives in source order.
    """
    from antlr4 import InputStream, CommonTokenStream
    from ..grammar import CLexer

    wrapped = f'void __display__(void) {{\n{source}\n}}'
    stream = InputStream(wrapped)
    lexer = CLexer(stream)
    lexer.removeErrorListeners()
    tokens = CommonTokenStream(lexer)
    parser = CParser(tokens)
    parser.removeErrorListeners()
    tree = parser.compilationUnit()
    visitor = DisplayVisitor(local_vars)
    visitor.visitCompilationUnit(tree)
    return visitor.primitives

ComponentDisplay

mccode_antlr.display.component_display.ComponentDisplay

Parse and evaluate the MCDISPLAY section of a component.

Parameters

comp: The component whose display RawC blocks to parse.

Source code in src/mccode_antlr/display/component_display.py
class ComponentDisplay:
    """Parse and evaluate the ``MCDISPLAY`` section of a component.

    Parameters
    ----------
    comp:
        The component whose ``display`` RawC blocks to parse.
    """

    def __init__(self, comp: 'Comp'):
        self._comp = comp
        self._primitives: list[AnyBlock] | None = None

    @property
    def name(self) -> str:
        return self._comp.name or '(unnamed)'

    @property
    def primitives(self) -> list[AnyBlock]:
        """Lazily parse the display source and cache the result."""
        if self._primitives is None:
            self._primitives = self._parse()
        return self._primitives

    def _parse(self) -> list[AnyBlock]:
        if not self._comp.display:
            return []
        # Combine all RawC blocks into a single source string
        source = '\n'.join(block.source for block in self._comp.display)
        if not source.strip():
            return []
        return parse_display_source(source)

    def is_empty(self) -> bool:
        """Return ``True`` if this component has no display geometry."""
        return len(self.primitives) == 0

    def to_polylines(self, params: dict[str, float] | None = None) -> list[np.ndarray]:
        """Return all geometry as a flat list of ``(N, 3)`` numpy arrays.

        Parameters
        ----------
        params:
            Component parameter values (and any instrument parameters
            referenced in the display expressions).  Values that are unknown
            in *params* remain symbolic and may cause evaluation errors;
            they are silently skipped.
        """
        p = params or {}
        result: list[np.ndarray] = []
        for prim in self.primitives:
            try:
                result.extend(prim.to_polylines(p))
            except Exception:
                pass
        return result

    def __repr__(self) -> str:
        n = len(self.primitives)
        return f'ComponentDisplay({self.name!r}, {n} primitive{"s" if n != 1 else ""})'

primitives property

Lazily parse the display source and cache the result.

is_empty()

Return True if this component has no display geometry.

Source code in src/mccode_antlr/display/component_display.py
def is_empty(self) -> bool:
    """Return ``True`` if this component has no display geometry."""
    return len(self.primitives) == 0

to_polylines(params=None)

Return all geometry as a flat list of (N, 3) numpy arrays.

Parameters

params: Component parameter values (and any instrument parameters referenced in the display expressions). Values that are unknown in params remain symbolic and may cause evaluation errors; they are silently skipped.

Source code in src/mccode_antlr/display/component_display.py
def to_polylines(self, params: dict[str, float] | None = None) -> list[np.ndarray]:
    """Return all geometry as a flat list of ``(N, 3)`` numpy arrays.

    Parameters
    ----------
    params:
        Component parameter values (and any instrument parameters
        referenced in the display expressions).  Values that are unknown
        in *params* remain symbolic and may cause evaluation errors;
        they are silently skipped.
    """
    p = params or {}
    result: list[np.ndarray] = []
    for prim in self.primitives:
        try:
            result.extend(prim.to_polylines(p))
        except Exception:
            pass
    return result

InstrumentDisplay

mccode_antlr.display.instrument_display.InstrumentDisplay

Parse and evaluate the geometry for an entire instrument.

Parameters

instr: The :class:~mccode_antlr.instr.instr.Instr to display.

Source code in src/mccode_antlr/display/instrument_display.py
class InstrumentDisplay:
    """Parse and evaluate the geometry for an entire instrument.

    Parameters
    ----------
    instr:
        The :class:`~mccode_antlr.instr.instr.Instr` to display.
    """

    def __init__(self, instr: 'Instr'):
        self._instr = instr
        self._components: dict[str, ComponentDisplay] = {}
        for instance in instr.components:
            cd = ComponentDisplay(instance.type)
            if not cd.is_empty():
                self._components[instance.name] = cd

    @property
    def component_names(self) -> list[str]:
        """Names of components that have display geometry."""
        return list(self._components.keys())

    def component_display(self, name: str) -> ComponentDisplay:
        """Return the :class:`ComponentDisplay` for the named instance."""
        return self._components[name]

    def to_polylines(
        self,
        params: dict[str, float] | None = None,
        *,
        global_frame: bool = True,
    ) -> dict[str, list[np.ndarray]]:
        """Evaluate all component geometries and return polylines.

        Parameters
        ----------
        params:
            Instrument parameter values.  Component SETTING parameters that
            match instance parameter assignments are also injected automatically.
        global_frame:
            If ``True`` (default) transform all polylines to the global
            instrument frame using each component's :class:`~mccode_antlr.instr.orientation.Orient`.
            If ``False`` return component-local coordinates.

        Returns
        -------
        dict[str, list[np.ndarray]]
            Mapping from component instance name to a list of ``(N, 3)``
            polyline arrays.
        """
        p = dict(params or {})
        result: dict[str, list[np.ndarray]] = {}

        for instance in self._instr.components:
            name = instance.name
            if name not in self._components:
                continue

            # Build merged parameter dict: instr params + instance overrides
            comp_params = dict(p)
            for cp in instance.parameters:
                try:
                    val = cp.value.evaluate(p).simplify()
                    comp_params[cp.name] = float(val)
                except Exception:
                    pass

            cd = self._components[name]
            local_polylines = cd.to_polylines(comp_params)

            if not global_frame or instance.orientation is None:
                result[name] = local_polylines
                continue

            # Transform to global frame
            try:
                orient = instance.orientation
                rotation = orient.rotation()
                translation = orient.position()
            except Exception:
                result[name] = local_polylines
                continue

            global_polylines = []
            for pts in local_polylines:
                try:
                    global_polylines.append(_transform_polyline(pts, rotation, translation, p))
                except Exception:
                    global_polylines.append(pts)
            result[name] = global_polylines

        return result

    def __repr__(self) -> str:
        n = len(self._components)
        return (f'InstrumentDisplay({self._instr.name!r}, '
                f'{n} component{"s" if n != 1 else ""} with display)')

component_names property

Names of components that have display geometry.

component_display(name)

Return the :class:ComponentDisplay for the named instance.

Source code in src/mccode_antlr/display/instrument_display.py
def component_display(self, name: str) -> ComponentDisplay:
    """Return the :class:`ComponentDisplay` for the named instance."""
    return self._components[name]

to_polylines(params=None, *, global_frame=True)

Evaluate all component geometries and return polylines.

Parameters

params: Instrument parameter values. Component SETTING parameters that match instance parameter assignments are also injected automatically. global_frame: If True (default) transform all polylines to the global instrument frame using each component's :class:~mccode_antlr.instr.orientation.Orient. If False return component-local coordinates.

Returns

dict[str, list[np.ndarray]] Mapping from component instance name to a list of (N, 3) polyline arrays.

Source code in src/mccode_antlr/display/instrument_display.py
def to_polylines(
    self,
    params: dict[str, float] | None = None,
    *,
    global_frame: bool = True,
) -> dict[str, list[np.ndarray]]:
    """Evaluate all component geometries and return polylines.

    Parameters
    ----------
    params:
        Instrument parameter values.  Component SETTING parameters that
        match instance parameter assignments are also injected automatically.
    global_frame:
        If ``True`` (default) transform all polylines to the global
        instrument frame using each component's :class:`~mccode_antlr.instr.orientation.Orient`.
        If ``False`` return component-local coordinates.

    Returns
    -------
    dict[str, list[np.ndarray]]
        Mapping from component instance name to a list of ``(N, 3)``
        polyline arrays.
    """
    p = dict(params or {})
    result: dict[str, list[np.ndarray]] = {}

    for instance in self._instr.components:
        name = instance.name
        if name not in self._components:
            continue

        # Build merged parameter dict: instr params + instance overrides
        comp_params = dict(p)
        for cp in instance.parameters:
            try:
                val = cp.value.evaluate(p).simplify()
                comp_params[cp.name] = float(val)
            except Exception:
                pass

        cd = self._components[name]
        local_polylines = cd.to_polylines(comp_params)

        if not global_frame or instance.orientation is None:
            result[name] = local_polylines
            continue

        # Transform to global frame
        try:
            orient = instance.orientation
            rotation = orient.rotation()
            translation = orient.position()
        except Exception:
            result[name] = local_polylines
            continue

        global_polylines = []
        for pts in local_polylines:
            try:
                global_polylines.append(_transform_polyline(pts, rotation, translation, p))
            except Exception:
                global_polylines.append(pts)
        result[name] = global_polylines

    return result

Geometry primitives

All non-string arguments are Expr objects and are resolved lazily at .to_polylines(params) time.

Primitive C call Polylines
Line line(x1,y1,z1, x2,y2,z2) 1 segment
DashedLine dashed_line(x1,y1,z1, x2,y2,z2, n) n segments
Multiline multiline(count, x1,y1,z1,...) 1 open polyline
Circle circle(plane, cx,cy,cz, r) 1 closed polyline (24 pts)
Rectangle rectangle(plane, cx,cy,cz, w,h) 1 closed rect (5 pts)
Box box(cx,cy,cz, w,h,d) 12 edges
Sphere sphere(cx,cy,cz, r) 3 great circles
Cylinder cylinder(cx,cy,cz, r,h, nx,ny,nz) 2 end caps + 4 lines
Cone cone(cx,cy,cz, r,h, nx,ny,nz) tapered circles + 4 lines
Magnify magnify(scale) metadata only
ConditionalBlock if (cond) { ... } filtered at evaluate-time
LoopBlock for/while (...) unrolled at evaluate-time

mccode_antlr.display.primitives

Geometry primitives for the McCode MCDISPLAY section.

Each primitive stores its arguments as :class:~mccode_antlr.common.expression.Expr objects and can:

  • :meth:evaluate — substitute a parameter dict and return a resolved copy with all :class:~mccode_antlr.common.expression.Expr values replaced by float.
  • :meth:to_polylines — return a list of (N, 3) numpy arrays (open or closed polylines) suitable for 3-D rendering.
  • :meth:to_mesh — return (vertices, faces) arrays for solid-surface rendering, or None for wire-only primitives.

Primitive dataclass

Abstract base for all MCDISPLAY geometry primitives.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Primitive:
    """Abstract base for all MCDISPLAY geometry primitives."""
    condition: Optional[Expr] = field(default=None, compare=False, kw_only=True)

    def evaluate(self, params: dict) -> 'Primitive':
        """Return a copy with all :class:`Expr` arguments resolved to floats."""
        raise NotImplementedError

    def to_polylines(self, params: dict | None = None) -> list[np.ndarray]:
        """Return a list of ``(N, 3)`` polyline arrays in component-local coordinates."""
        raise NotImplementedError

    def to_mesh(self, params: dict | None = None) -> tuple[np.ndarray, np.ndarray] | None:
        """Return ``(vertices, faces)`` for solid-surface rendering, or ``None``.

        vertices : (V, 3) float32 array of 3-D positions.
        faces    : (F, 3) int32 array of triangle vertex indices.
        Wire-only primitives return ``None``.
        """
        return None

    def is_active(self, params: dict) -> bool:
        """Return ``True`` if this primitive's condition is satisfied (or absent)."""
        if self.condition is None:
            return True
        val = _eval(self.condition, params or {})
        return bool(val)

evaluate(params)

Return a copy with all :class:Expr arguments resolved to floats.

Source code in src/mccode_antlr/display/primitives.py
def evaluate(self, params: dict) -> 'Primitive':
    """Return a copy with all :class:`Expr` arguments resolved to floats."""
    raise NotImplementedError

to_polylines(params=None)

Return a list of (N, 3) polyline arrays in component-local coordinates.

Source code in src/mccode_antlr/display/primitives.py
def to_polylines(self, params: dict | None = None) -> list[np.ndarray]:
    """Return a list of ``(N, 3)`` polyline arrays in component-local coordinates."""
    raise NotImplementedError

to_mesh(params=None)

Return (vertices, faces) for solid-surface rendering, or None.

vertices : (V, 3) float32 array of 3-D positions. faces : (F, 3) int32 array of triangle vertex indices. Wire-only primitives return None.

Source code in src/mccode_antlr/display/primitives.py
def to_mesh(self, params: dict | None = None) -> tuple[np.ndarray, np.ndarray] | None:
    """Return ``(vertices, faces)`` for solid-surface rendering, or ``None``.

    vertices : (V, 3) float32 array of 3-D positions.
    faces    : (F, 3) int32 array of triangle vertex indices.
    Wire-only primitives return ``None``.
    """
    return None

is_active(params)

Return True if this primitive's condition is satisfied (or absent).

Source code in src/mccode_antlr/display/primitives.py
def is_active(self, params: dict) -> bool:
    """Return ``True`` if this primitive's condition is satisfied (or absent)."""
    if self.condition is None:
        return True
    val = _eval(self.condition, params or {})
    return bool(val)

Magnify dataclass

Bases: Primitive

magnify(what) — sets the magnification scale (metadata only).

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Magnify(Primitive):
    """``magnify(what)`` — sets the magnification scale (metadata only)."""
    what: str = ''

    def evaluate(self, params):
        return Magnify(what=self.what, condition=self.condition)

    def to_polylines(self, params=None):
        return []

Line dataclass

Bases: Primitive

line(x1,y1,z1, x2,y2,z2) — a single line segment.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Line(Primitive):
    """``line(x1,y1,z1, x2,y2,z2)`` — a single line segment."""
    x1: Expr = field(default_factory=Expr._null)
    y1: Expr = field(default_factory=Expr._null)
    z1: Expr = field(default_factory=Expr._null)
    x2: Expr = field(default_factory=Expr._null)
    y2: Expr = field(default_factory=Expr._null)
    z2: Expr = field(default_factory=Expr._null)

    def evaluate(self, params):
        return Line(
            _eval(self.x1, params), _eval(self.y1, params), _eval(self.z1, params),
            _eval(self.x2, params), _eval(self.y2, params), _eval(self.z2, params),
            condition=self.condition,
        )

    def to_polylines(self, params=None):
        p = params or {}
        return [np.array([
            [_eval(self.x1, p), _eval(self.y1, p), _eval(self.z1, p)],
            [_eval(self.x2, p), _eval(self.y2, p), _eval(self.z2, p)],
        ])]

DashedLine dataclass

Bases: Primitive

dashed_line(x1,y1,z1, x2,y2,z2, n) — dashed line with n gaps.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class DashedLine(Primitive):
    """``dashed_line(x1,y1,z1, x2,y2,z2, n)`` — dashed line with *n* gaps."""
    x1: Expr = field(default_factory=Expr._null)
    y1: Expr = field(default_factory=Expr._null)
    z1: Expr = field(default_factory=Expr._null)
    x2: Expr = field(default_factory=Expr._null)
    y2: Expr = field(default_factory=Expr._null)
    z2: Expr = field(default_factory=Expr._null)
    n: Expr = field(default_factory=lambda: Expr.integer(4))

    def evaluate(self, params):
        return DashedLine(
            _eval(self.x1, params), _eval(self.y1, params), _eval(self.z1, params),
            _eval(self.x2, params), _eval(self.y2, params), _eval(self.z2, params),
            _eval(self.n, params), condition=self.condition,
        )

    def to_polylines(self, params=None):
        p = params or {}
        p1 = np.array([_eval(self.x1, p), _eval(self.y1, p), _eval(self.z1, p)])
        p2 = np.array([_eval(self.x2, p), _eval(self.y2, p), _eval(self.z2, p)])
        n = max(1, int(round(_eval(self.n, p))))
        segments = []
        for i in range(n):
            t0, t1 = i / n, (i + 0.5) / n
            segments.append(np.array([p1 + t0 * (p2 - p1), p1 + t1 * (p2 - p1)]))
        return segments

Multiline dataclass

Bases: Primitive

multiline(count, x1,y1,z1,...) — open polyline with count vertices.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Multiline(Primitive):
    """``multiline(count, x1,y1,z1,...)`` — open polyline with *count* vertices."""
    points: list[tuple[Expr, Expr, Expr]] = field(default_factory=list)

    def evaluate(self, params):
        return Multiline(
            [(_eval(x, params), _eval(y, params), _eval(z, params)) for x, y, z in self.points],
            condition=self.condition,
        )

    def to_polylines(self, params=None):
        p = params or {}
        if not self.points:
            return []
        pts = np.array([(_eval(x, p), _eval(y, p), _eval(z, p)) for x, y, z in self.points])
        return [pts]

Circle dataclass

Bases: Primitive

circle(plane, cx,cy,cz, r) — circle in a coordinate plane.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Circle(Primitive):
    """``circle(plane, cx,cy,cz, r)`` — circle in a coordinate plane."""
    plane: str = 'xy'
    cx: Expr = field(default_factory=Expr._null)
    cy: Expr = field(default_factory=Expr._null)
    cz: Expr = field(default_factory=Expr._null)
    r: Expr = field(default_factory=Expr._null)

    def evaluate(self, params):
        return Circle(self.plane,
                      _eval(self.cx, params), _eval(self.cy, params), _eval(self.cz, params),
                      _eval(self.r, params), condition=self.condition)

    def to_polylines(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        r = _eval(self.r, p)
        return [_circle_points(cx, cy, cz, r, self.plane)]

Rectangle dataclass

Bases: Primitive

rectangle(plane, cx,cy,cz, w,h) — filled (closed) rectangle.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Rectangle(Primitive):
    """``rectangle(plane, cx,cy,cz, w,h)`` — filled (closed) rectangle."""
    plane: str = 'xy'
    cx: Expr = field(default_factory=Expr._null)
    cy: Expr = field(default_factory=Expr._null)
    cz: Expr = field(default_factory=Expr._null)
    w: Expr = field(default_factory=Expr._null)
    h: Expr = field(default_factory=Expr._null)

    def evaluate(self, params):
        return Rectangle(self.plane,
                         _eval(self.cx, params), _eval(self.cy, params), _eval(self.cz, params),
                         _eval(self.w, params), _eval(self.h, params),
                         condition=self.condition)

    def to_polylines(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        w2, h2 = _eval(self.w, p) / 2, _eval(self.h, p) / 2
        if self.plane == 'xy':
            pts = np.array([(cx-w2,cy-h2,cz),(cx+w2,cy-h2,cz),(cx+w2,cy+h2,cz),(cx-w2,cy+h2,cz),(cx-w2,cy-h2,cz)])
        elif self.plane == 'xz':
            pts = np.array([(cx-w2,cy,cz-h2),(cx+w2,cy,cz-h2),(cx+w2,cy,cz+h2),(cx-w2,cy,cz+h2),(cx-w2,cy,cz-h2)])
        elif self.plane == 'yz':
            pts = np.array([(cx,cy-w2,cz-h2),(cx,cy+w2,cz-h2),(cx,cy+w2,cz+h2),(cx,cy-w2,cz+h2),(cx,cy-w2,cz-h2)])
        else:
            raise ValueError(f"Unknown plane '{self.plane}'")
        return [pts]

Box dataclass

Bases: Primitive

box(cx,cy,cz, xw,yh,zd[, thickness[, nx,ny,nz]]) — rectangular box.

The new McCode overhaul adds an optional thickness (hollow-wall depth) and an orientation normal (nx, ny, nz). When thickness is zero or absent the box is rendered as 12 wire edges, otherwise a second inner box is also drawn. The normal is currently used only by the 3-D renderer; the wire representation always uses the legacy axis-aligned form.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Box(Primitive):
    """``box(cx,cy,cz, xw,yh,zd[, thickness[, nx,ny,nz]])`` — rectangular box.

    The new McCode overhaul adds an optional *thickness* (hollow-wall depth) and an
    orientation normal (*nx*, *ny*, *nz*).  When *thickness* is zero or absent the
    box is rendered as 12 wire edges, otherwise a second inner box is also drawn.
    The normal is currently used only by the 3-D renderer; the wire representation
    always uses the legacy axis-aligned form.
    """
    cx: Expr = field(default_factory=Expr._null)
    cy: Expr = field(default_factory=Expr._null)
    cz: Expr = field(default_factory=Expr._null)
    xw: Expr = field(default_factory=Expr._null)
    yh: Expr = field(default_factory=Expr._null)
    zd: Expr = field(default_factory=Expr._null)
    thickness: Expr = field(default_factory=lambda: Expr.float(0))
    nx: Expr = field(default_factory=lambda: Expr.float(0))
    ny: Expr = field(default_factory=lambda: Expr.float(1))
    nz: Expr = field(default_factory=lambda: Expr.float(0))

    def evaluate(self, params):
        return Box(
            _eval(self.cx, params), _eval(self.cy, params), _eval(self.cz, params),
            _eval(self.xw, params), _eval(self.yh, params), _eval(self.zd, params),
            _eval(self.thickness, params),
            _eval(self.nx, params), _eval(self.ny, params), _eval(self.nz, params),
            condition=self.condition,
        )

    def _corners(self, cx, cy, cz, dx, dy, dz):
        return np.array([
            (cx-dx, cy-dy, cz-dz), (cx+dx, cy-dy, cz-dz),
            (cx+dx, cy+dy, cz-dz), (cx-dx, cy+dy, cz-dz),
            (cx-dx, cy-dy, cz+dz), (cx+dx, cy-dy, cz+dz),
            (cx+dx, cy+dy, cz+dz), (cx-dx, cy+dy, cz+dz),
        ])

    def to_polylines(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        dx, dy, dz = _eval(self.xw, p)/2, _eval(self.yh, p)/2, _eval(self.zd, p)/2
        edges = [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),(0,4),(1,5),(2,6),(3,7)]
        result = [self._corners(cx, cy, cz, dx, dy, dz)[list(e)] for e in edges]
        t = _eval(self.thickness, p)
        if t:
            inner = self._corners(cx, cy, cz, max(0, dx-t/2), max(0, dy-t/2), max(0, dz-t/2))
            result += [inner[list(e)] for e in edges]
        return result

    def to_mesh(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        dx, dy, dz = _eval(self.xw, p)/2, _eval(self.yh, p)/2, _eval(self.zd, p)/2
        verts = self._corners(cx, cy, cz, dx, dy, dz).astype(np.float32)
        # 6 faces, each split into 2 triangles
        faces = np.array([
            [0,1,2],[0,2,3],  # -z face
            [4,6,5],[4,7,6],  # +z face
            [0,4,5],[0,5,1],  # -y face
            [2,6,7],[2,7,3],  # +y face
            [0,3,7],[0,7,4],  # -x face
            [1,5,6],[1,6,2],  # +x face
        ], dtype=np.int32)
        return verts, faces

Sphere dataclass

Bases: Primitive

sphere(cx,cy,cz, r) — sphere rendered as three great circles.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Sphere(Primitive):
    """``sphere(cx,cy,cz, r)`` — sphere rendered as three great circles."""
    cx: Expr = field(default_factory=Expr._null)
    cy: Expr = field(default_factory=Expr._null)
    cz: Expr = field(default_factory=Expr._null)
    r: Expr = field(default_factory=Expr._null)

    def evaluate(self, params):
        return Sphere(
            _eval(self.cx, params), _eval(self.cy, params), _eval(self.cz, params),
            _eval(self.r, params), condition=self.condition,
        )

    def to_polylines(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        r = _eval(self.r, p)
        return [
            _circle_points(cx, cy, cz, r, 'xy'),
            _circle_points(cx, cy, cz, r, 'xz'),
            _circle_points(cx, cy, cz, r, 'yz'),
        ]

    def to_mesh(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        r = _eval(self.r, p)
        stacks, slices = 16, 32
        verts = []
        for i in range(stacks + 1):
            phi = math.pi * i / stacks
            for j in range(slices):
                theta = 2 * math.pi * j / slices
                x = cx + r * math.sin(phi) * math.cos(theta)
                y = cy + r * math.cos(phi)
                z = cz + r * math.sin(phi) * math.sin(theta)
                verts.append((x, y, z))
        faces = []
        for i in range(stacks):
            for j in range(slices):
                a = i * slices + j
                b = i * slices + (j + 1) % slices
                c = (i + 1) * slices + (j + 1) % slices
                d = (i + 1) * slices + j
                faces.append((a, b, c))
                faces.append((a, c, d))
        return np.array(verts, dtype=np.float32), np.array(faces, dtype=np.int32)

Cylinder dataclass

Bases: Primitive

cylinder(cx,cy,cz, r,h[, thickness[, nx,ny,nz]]) — cylinder.

The new McCode overhaul adds an optional thickness for hollow cylinders. The axis orientation (nx, ny, nz) was already supported.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Cylinder(Primitive):
    """``cylinder(cx,cy,cz, r,h[, thickness[, nx,ny,nz]])`` — cylinder.

    The new McCode overhaul adds an optional *thickness* for hollow cylinders.
    The axis orientation (*nx*, *ny*, *nz*) was already supported.
    """
    cx: Expr = field(default_factory=Expr._null)
    cy: Expr = field(default_factory=Expr._null)
    cz: Expr = field(default_factory=Expr._null)
    r: Expr = field(default_factory=Expr._null)
    h: Expr = field(default_factory=Expr._null)
    thickness: Expr = field(default_factory=lambda: Expr.float(0))
    nx: Expr = field(default_factory=lambda: Expr.float(0))
    ny: Expr = field(default_factory=lambda: Expr.float(1))
    nz: Expr = field(default_factory=lambda: Expr.float(0))

    def evaluate(self, params):
        return Cylinder(
            _eval(self.cx, params), _eval(self.cy, params), _eval(self.cz, params),
            _eval(self.r, params), _eval(self.h, params),
            _eval(self.thickness, params),
            _eval(self.nx, params), _eval(self.ny, params), _eval(self.nz, params),
            condition=self.condition,
        )

    def _axis_frame(self, nx, ny, nz):
        normal = np.array([nx, ny, nz], dtype=float)
        norm = np.linalg.norm(normal)
        if norm < 1e-10:
            normal = np.array([0.0, 1.0, 0.0])
        else:
            normal /= norm
        return normal

    def to_polylines(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        r, h = _eval(self.r, p), _eval(self.h, p)
        nx, ny, nz = _eval(self.nx, p), _eval(self.ny, p), _eval(self.nz, p)
        normal = self._axis_frame(nx, ny, nz)
        half = 0.5 * h * normal
        c1 = np.array([cx, cy, cz]) - half
        c2 = np.array([cx, cy, cz]) + half
        cap1 = _circle_with_normal(*c1, r, *normal)
        cap2 = _circle_with_normal(*c2, r, *normal)
        n_pts = cap1.shape[0] - 1
        lines = [np.array([cap1[i * n_pts // 4], cap2[i * n_pts // 4]]) for i in range(4)]
        return [cap1, cap2] + lines

    def to_mesh(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        r, h = _eval(self.r, p), _eval(self.h, p)
        nx, ny, nz = _eval(self.nx, p), _eval(self.ny, p), _eval(self.nz, p)
        normal = self._axis_frame(nx, ny, nz)
        half = 0.5 * h * normal
        centre = np.array([cx, cy, cz])
        c1, c2 = centre - half, centre + half
        slices = 32
        # Build perimeter points for both caps (without the closing duplicate)
        ring1 = _circle_with_normal(*c1, r, *normal, n=slices)[:-1]
        ring2 = _circle_with_normal(*c2, r, *normal, n=slices)[:-1]
        verts = np.vstack([ring1, ring2, [c1], [c2]]).astype(np.float32)
        cap1_centre = slices * 2      # index of c1
        cap2_centre = slices * 2 + 1  # index of c2
        faces = []
        for i in range(slices):
            j = (i + 1) % slices
            # lateral quad → 2 triangles
            faces.append((i, j, slices + j))
            faces.append((i, slices + j, slices + i))
            # bottom cap (winding inward)
            faces.append((cap1_centre, j, i))
            # top cap
            faces.append((cap2_centre, slices + i, slices + j))
        return verts, np.array(faces, dtype=np.int32)

Cone dataclass

Bases: Primitive

cone(cx,cy,cz, r,h[, nx,ny,nz]) — cone.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Cone(Primitive):
    """``cone(cx,cy,cz, r,h[, nx,ny,nz])`` — cone."""
    cx: Expr = field(default_factory=Expr._null)
    cy: Expr = field(default_factory=Expr._null)
    cz: Expr = field(default_factory=Expr._null)
    r: Expr = field(default_factory=Expr._null)
    h: Expr = field(default_factory=Expr._null)
    nx: Expr = field(default_factory=lambda: Expr.float(0))
    ny: Expr = field(default_factory=lambda: Expr.float(1))
    nz: Expr = field(default_factory=lambda: Expr.float(0))

    def evaluate(self, params):
        return Cone(
            _eval(self.cx, params), _eval(self.cy, params), _eval(self.cz, params),
            _eval(self.r, params), _eval(self.h, params),
            _eval(self.nx, params), _eval(self.ny, params), _eval(self.nz, params),
            condition=self.condition,
        )

    def to_polylines(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        r, h = _eval(self.r, p), _eval(self.h, p)
        nx, ny, nz = _eval(self.nx, p), _eval(self.ny, p), _eval(self.nz, p)
        normal = np.array([nx, ny, nz], dtype=float)
        norm = np.linalg.norm(normal)
        if norm < 1e-10:
            normal = np.array([0.0, 1.0, 0.0])
        else:
            normal /= norm
        base = np.array([cx, cy, cz])
        apex = base + h * normal
        base_ring = _circle_with_normal(*base, r, *normal)
        n_pts = base_ring.shape[0] - 1
        lines = [np.array([base_ring[i * n_pts // 4], apex]) for i in range(4)]
        return [base_ring] + lines

    def to_mesh(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        r, h = _eval(self.r, p), _eval(self.h, p)
        nx, ny, nz = _eval(self.nx, p), _eval(self.ny, p), _eval(self.nz, p)
        normal = np.array([nx, ny, nz], dtype=float)
        norm = np.linalg.norm(normal)
        if norm < 1e-10:
            normal = np.array([0.0, 1.0, 0.0])
        else:
            normal /= norm
        slices = 32
        base = np.array([cx, cy, cz])
        apex = base + h * normal
        ring = _circle_with_normal(*base, r, *normal, n=slices)[:-1]
        verts = np.vstack([ring, [apex], [base]]).astype(np.float32)
        apex_idx = slices
        base_centre_idx = slices + 1
        faces = []
        for i in range(slices):
            j = (i + 1) % slices
            faces.append((i, j, apex_idx))          # lateral
            faces.append((base_centre_idx, j, i))   # base cap
        return verts, np.array(faces, dtype=np.int32)

CircleNormal dataclass

Bases: Primitive

Circle(x,y,z,r,nx,ny,nz) / new_circle(…) — circle with arbitrary normal.

Unlike :class:Circle, the plane is specified by a normal vector rather than a plane string. Corresponds to mcdis_Circle / mcdis_new_circle in C.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class CircleNormal(Primitive):
    """``Circle(x,y,z,r,nx,ny,nz)`` / ``new_circle(…)`` — circle with arbitrary normal.

    Unlike :class:`Circle`, the plane is specified by a normal vector rather than
    a plane string.  Corresponds to ``mcdis_Circle`` / ``mcdis_new_circle`` in C.
    """
    cx: Expr = field(default_factory=Expr._null)
    cy: Expr = field(default_factory=Expr._null)
    cz: Expr = field(default_factory=Expr._null)
    r: Expr = field(default_factory=Expr._null)
    nx: Expr = field(default_factory=lambda: Expr.float(0))
    ny: Expr = field(default_factory=lambda: Expr.float(1))
    nz: Expr = field(default_factory=lambda: Expr.float(0))

    def evaluate(self, params):
        return CircleNormal(
            _eval(self.cx, params), _eval(self.cy, params), _eval(self.cz, params),
            _eval(self.r, params),
            _eval(self.nx, params), _eval(self.ny, params), _eval(self.nz, params),
            condition=self.condition,
        )

    def to_polylines(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        r = _eval(self.r, p)
        nx, ny, nz = _eval(self.nx, p), _eval(self.ny, p), _eval(self.nz, p)
        return [_circle_with_normal(cx, cy, cz, r, nx, ny, nz)]

Disc dataclass

Bases: Primitive

disc(x,y,z,r,nx,ny,nz) — filled disc with an arbitrary normal.

Corresponds to mcdis_disc in C. Wire representation is the perimeter circle; mesh representation is a fan-triangulated flat disc.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Disc(Primitive):
    """``disc(x,y,z,r,nx,ny,nz)`` — filled disc with an arbitrary normal.

    Corresponds to ``mcdis_disc`` in C.  Wire representation is the perimeter
    circle; mesh representation is a fan-triangulated flat disc.
    """
    cx: Expr = field(default_factory=Expr._null)
    cy: Expr = field(default_factory=Expr._null)
    cz: Expr = field(default_factory=Expr._null)
    r: Expr = field(default_factory=Expr._null)
    nx: Expr = field(default_factory=lambda: Expr.float(0))
    ny: Expr = field(default_factory=lambda: Expr.float(1))
    nz: Expr = field(default_factory=lambda: Expr.float(0))

    def evaluate(self, params):
        return Disc(
            _eval(self.cx, params), _eval(self.cy, params), _eval(self.cz, params),
            _eval(self.r, params),
            _eval(self.nx, params), _eval(self.ny, params), _eval(self.nz, params),
            condition=self.condition,
        )

    def to_polylines(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        r = _eval(self.r, p)
        nx, ny, nz = _eval(self.nx, p), _eval(self.ny, p), _eval(self.nz, p)
        return [_circle_with_normal(cx, cy, cz, r, nx, ny, nz)]

    def to_mesh(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        r = _eval(self.r, p)
        nx, ny, nz = _eval(self.nx, p), _eval(self.ny, p), _eval(self.nz, p)
        slices = 32
        ring = _circle_with_normal(cx, cy, cz, r, nx, ny, nz, n=slices)[:-1]
        centre_idx = slices
        verts = np.vstack([ring, [[cx, cy, cz]]]).astype(np.float32)
        faces = [(centre_idx, (i + 1) % slices, i) for i in range(slices)]
        return verts, np.array(faces, dtype=np.int32)

Annulus dataclass

Bases: Primitive

annulus(x,y,z,r_outer,r_inner,nx,ny,nz) — annular disc with arbitrary normal.

Corresponds to mcdis_annulus in C. Wire representation is two concentric circles; mesh representation is a ring of quads.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Annulus(Primitive):
    """``annulus(x,y,z,r_outer,r_inner,nx,ny,nz)`` — annular disc with arbitrary normal.

    Corresponds to ``mcdis_annulus`` in C.  Wire representation is two concentric
    circles; mesh representation is a ring of quads.
    """
    cx: Expr = field(default_factory=Expr._null)
    cy: Expr = field(default_factory=Expr._null)
    cz: Expr = field(default_factory=Expr._null)
    r_outer: Expr = field(default_factory=Expr._null)
    r_inner: Expr = field(default_factory=Expr._null)
    nx: Expr = field(default_factory=lambda: Expr.float(0))
    ny: Expr = field(default_factory=lambda: Expr.float(1))
    nz: Expr = field(default_factory=lambda: Expr.float(0))

    def evaluate(self, params):
        return Annulus(
            _eval(self.cx, params), _eval(self.cy, params), _eval(self.cz, params),
            _eval(self.r_outer, params), _eval(self.r_inner, params),
            _eval(self.nx, params), _eval(self.ny, params), _eval(self.nz, params),
            condition=self.condition,
        )

    def to_polylines(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        ro = _eval(self.r_outer, p)
        ri = _eval(self.r_inner, p)
        nx, ny, nz = _eval(self.nx, p), _eval(self.ny, p), _eval(self.nz, p)
        return [
            _circle_with_normal(cx, cy, cz, ro, nx, ny, nz),
            _circle_with_normal(cx, cy, cz, ri, nx, ny, nz),
        ]

    def to_mesh(self, params=None):
        p = params or {}
        cx, cy, cz = _eval(self.cx, p), _eval(self.cy, p), _eval(self.cz, p)
        ro = _eval(self.r_outer, p)
        ri = _eval(self.r_inner, p)
        nx, ny, nz = _eval(self.nx, p), _eval(self.ny, p), _eval(self.nz, p)
        slices = 32
        outer = _circle_with_normal(cx, cy, cz, ro, nx, ny, nz, n=slices)[:-1]
        inner = _circle_with_normal(cx, cy, cz, ri, nx, ny, nz, n=slices)[:-1]
        verts = np.vstack([outer, inner]).astype(np.float32)
        faces = []
        for i in range(slices):
            j = (i + 1) % slices
            # outer[i], outer[j], inner[j], inner[i] → 2 triangles
            faces.append((i, j, slices + j))
            faces.append((i, slices + j, slices + i))
        return verts, np.array(faces, dtype=np.int32)

Polygon dataclass

Bases: Primitive

polygon(count, x1,y1,z1,…) — closed flat polygon.

Corresponds to mcdis_polygon in C. Wire representation closes the path; mesh representation is fan-triangulated from the centroid.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Polygon(Primitive):
    """``polygon(count, x1,y1,z1,…)`` — closed flat polygon.

    Corresponds to ``mcdis_polygon`` in C.  Wire representation closes the path;
    mesh representation is fan-triangulated from the centroid.
    """
    points: list[tuple[Expr, Expr, Expr]] = field(default_factory=list)

    def evaluate(self, params):
        return Polygon(
            [(_eval(x, params), _eval(y, params), _eval(z, params)) for x, y, z in self.points],
            condition=self.condition,
        )

    def to_polylines(self, params=None):
        p = params or {}
        if not self.points:
            return []
        pts = np.array([(_eval(x, p), _eval(y, p), _eval(z, p)) for x, y, z in self.points])
        # close the polygon
        return [np.vstack([pts, pts[:1]])]

    def to_mesh(self, params=None):
        p = params or {}
        if len(self.points) < 3:
            return None
        pts = np.array([(_eval(x, p), _eval(y, p), _eval(z, p)) for x, y, z in self.points],
                       dtype=np.float32)
        centroid = pts.mean(axis=0)
        n = len(pts)
        verts = np.vstack([pts, [centroid]]).astype(np.float32)
        centre_idx = n
        faces = [(centre_idx, (i + 1) % n, i) for i in range(n)]
        return verts, np.array(faces, dtype=np.int32)

Polyhedron dataclass

Bases: Primitive

polyhedron(json_str) — 3-D polyhedron defined by a JSON string.

The JSON format produced by mcdis_polygon (and usable directly) is::

{ "vertices": [[x,y,z], ...],
  "faces":    [{"face": [i, j, k]}, ...] }

Corresponds to mcdis_polyhedron in C.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class Polyhedron(Primitive):
    """``polyhedron(json_str)`` — 3-D polyhedron defined by a JSON string.

    The JSON format produced by ``mcdis_polygon`` (and usable directly) is::

        { "vertices": [[x,y,z], ...],
          "faces":    [{"face": [i, j, k]}, ...] }

    Corresponds to ``mcdis_polyhedron`` in C.
    """
    json_str: str = ''
    _vertices: np.ndarray = field(default=None, init=False, compare=False, repr=False)
    _faces: np.ndarray = field(default=None, init=False, compare=False, repr=False)

    def __post_init__(self):
        self._parse()

    def _parse(self):
        if not self.json_str:
            self._vertices = np.zeros((0, 3), dtype=np.float32)
            self._faces = np.zeros((0, 3), dtype=np.int32)
            return
        try:
            # Unescape C-style \" → " that ANTLR preserves from string literals
            json_text = self.json_str.replace('\\"', '"')
            data = json.loads(json_text)
            self._vertices = np.array(data['vertices'], dtype=np.float32)
            self._faces = np.array(
                [f['face'] for f in data['faces']], dtype=np.int32
            )
        except Exception:
            self._vertices = np.zeros((0, 3), dtype=np.float32)
            self._faces = np.zeros((0, 3), dtype=np.int32)

    def evaluate(self, params):
        result = Polyhedron(self.json_str, condition=self.condition)
        return result

    def to_polylines(self, params=None):
        if self._vertices is None or len(self._faces) == 0:
            return []
        lines = []
        for face in self._faces:
            pts = self._vertices[list(face) + [face[0]]]
            lines.append(pts)
        return lines

    def to_mesh(self, params=None):
        if self._vertices is None or len(self._vertices) == 0:
            return None
        return self._vertices.copy(), self._faces.copy()

ConditionalBlock dataclass

A group of primitives guarded by a C if condition expression.

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class ConditionalBlock:
    """A group of primitives guarded by a C ``if`` condition expression."""
    condition: Expr
    body: list[Primitive | 'ConditionalBlock' | 'LoopBlock'] = field(default_factory=list)

    def to_polylines(self, params: dict | None = None) -> list[np.ndarray]:
        p = params or {}
        try:
            active = bool(_eval(self.condition, p))
        except Exception:
            active = True  # unknown condition — include by default
        if not active:
            return []
        result = []
        for prim in self.body:
            result.extend(prim.to_polylines(p))
        return result

LoopBlock dataclass

A for/while loop containing display primitives (not yet unrolled).

Source code in src/mccode_antlr/display/primitives.py
@dataclass
class LoopBlock:
    """A ``for``/``while`` loop containing display primitives (not yet unrolled)."""
    loop_text: str
    body: list[Primitive | ConditionalBlock | 'LoopBlock'] = field(default_factory=list)

    def to_polylines(self, params: dict | None = None) -> list[np.ndarray]:
        # Best-effort: emit all body primitives without loop evaluation
        p = params or {}
        result = []
        for prim in self.body:
            result.extend(prim.to_polylines(p))
        return result

Renderers

matplotlib (zero new dependencies)

mccode_antlr.display.render.matplotlib.plot_geometry(geometry, ax=None, colors=None, *, show_labels=True, label_offset=0.02, linewidth=1.0, alpha=1.0)

Draw instrument geometry using matplotlib's 3-D axes.

Parameters

geometry: Output from :meth:~mccode_antlr.display.InstrumentDisplay.to_polylines — a dict mapping component names to lists of (N, 3) numpy arrays. ax: An existing Axes3D instance to draw onto. If None a new figure and axes are created. colors: Optional mapping from component name to a matplotlib colour spec. Components not present in this dict receive auto-assigned colours. show_labels: If True (default) annotate each component with its name at the centroid of its geometry. label_offset: Fractional offset applied to the label position (relative to the overall scene bounding box diagonal). linewidth: Line width for all polylines. alpha: Opacity for all lines.

Returns

(fig, ax): The matplotlib Figure and Axes3D objects.

Source code in src/mccode_antlr/display/render/matplotlib.py
def plot_geometry(
    geometry: dict[str, list[np.ndarray]],
    ax=None,
    colors: dict[str, Any] | None = None,
    *,
    show_labels: bool = True,
    label_offset: float = 0.02,
    linewidth: float = 1.0,
    alpha: float = 1.0,
) -> tuple[Any, Any]:
    """Draw instrument geometry using matplotlib's 3-D axes.

    Parameters
    ----------
    geometry:
        Output from :meth:`~mccode_antlr.display.InstrumentDisplay.to_polylines`
        — a dict mapping component names to lists of ``(N, 3)`` numpy arrays.
    ax:
        An existing ``Axes3D`` instance to draw onto.  If ``None`` a new
        figure and axes are created.
    colors:
        Optional mapping from component name to a matplotlib colour spec.
        Components not present in this dict receive auto-assigned colours.
    show_labels:
        If ``True`` (default) annotate each component with its name at the
        centroid of its geometry.
    label_offset:
        Fractional offset applied to the label position (relative to the
        overall scene bounding box diagonal).
    linewidth:
        Line width for all polylines.
    alpha:
        Opacity for all lines.

    Returns
    -------
    (fig, ax):
        The matplotlib Figure and Axes3D objects.
    """
    try:
        import matplotlib.pyplot as plt
        from mpl_toolkits.mplot3d import Axes3D  # noqa: F401 — registers 3d projection
    except ImportError as exc:
        raise ImportError(
            "matplotlib is required for the matplotlib renderer. "
            "Install it with: pip install matplotlib"
        ) from exc

    if ax is None:
        fig = plt.figure()
        ax = fig.add_subplot(111, projection='3d')
    else:
        fig = ax.figure

    prop_cycle = plt.rcParams['axes.prop_cycle']
    auto_colors = [c['color'] for c in prop_cycle]
    colors = dict(colors or {})

    all_pts: list[np.ndarray] = []

    for i, (name, polylines) in enumerate(geometry.items()):
        color = colors.get(name, auto_colors[i % len(auto_colors)])
        centroid_pts: list[np.ndarray] = []
        for pts in polylines:
            if pts.ndim != 2 or pts.shape[1] != 3 or len(pts) < 2:
                continue
            ax.plot3D(pts[:, 0], pts[:, 1], pts[:, 2],
                      color=color, linewidth=linewidth, alpha=alpha)
            centroid_pts.append(pts)
            all_pts.append(pts)

        if show_labels and centroid_pts:
            concat = np.vstack(centroid_pts)
            cx, cy, cz = concat.mean(axis=0)
            ax.text(cx, cy, cz, name, fontsize=7, color=color)

    ax.set_xlabel('x (m)')
    ax.set_ylabel('z (m)')
    ax.set_zlabel('y (m)')

    if all_pts:
        all_data = np.vstack(all_pts)
        _set_equal_aspect(ax, all_data)

    return fig, ax

WebGL via pythreejs / K3D (optional)

mccode_antlr.display.render.threejs.show_geometry(geometry, instrument_display=None, *, backend='auto', use_mesh=True, params=None, width=800, height=600, background='#111111', opacity=0.6)

Display instrument geometry in a Jupyter notebook using WebGL.

Parameters

geometry: Output from :meth:~mccode_antlr.display.InstrumentDisplay.to_polylines. instrument_display: The :class:~mccode_antlr.display.InstrumentDisplay instance. When provided and use_mesh is True, solid surface meshes are drawn. backend: 'pythreejs', 'k3d', or 'auto' (tries pythreejs first). use_mesh: If True (default) and instrument_display is supplied, solid surface meshes are drawn alongside the wire representation. params: Parameter dict forwarded to to_mesh; defaults to {}. width, height: Widget dimensions in pixels. background: Background colour (CSS colour string, used by pythreejs backend). opacity: Opacity for mesh surfaces.

Returns

widget An ipywidget renderable in a Jupyter cell. Call display(widget) or simply return it as the last expression in a cell.

Source code in src/mccode_antlr/display/render/threejs.py
def show_geometry(
    geometry: dict[str, list[np.ndarray]],
    instrument_display: 'InstrumentDisplay | None' = None,
    *,
    backend: str = 'auto',
    use_mesh: bool = True,
    params: dict | None = None,
    width: int = 800,
    height: int = 600,
    background: str = '#111111',
    opacity: float = 0.6,
):
    """Display instrument geometry in a Jupyter notebook using WebGL.

    Parameters
    ----------
    geometry:
        Output from :meth:`~mccode_antlr.display.InstrumentDisplay.to_polylines`.
    instrument_display:
        The :class:`~mccode_antlr.display.InstrumentDisplay` instance.  When
        provided and *use_mesh* is ``True``, solid surface meshes are drawn.
    backend:
        ``'pythreejs'``, ``'k3d'``, or ``'auto'`` (tries pythreejs first).
    use_mesh:
        If ``True`` (default) and *instrument_display* is supplied, solid
        surface meshes are drawn alongside the wire representation.
    params:
        Parameter dict forwarded to ``to_mesh``; defaults to ``{}``.
    width, height:
        Widget dimensions in pixels.
    background:
        Background colour (CSS colour string, used by pythreejs backend).
    opacity:
        Opacity for mesh surfaces.

    Returns
    -------
    widget
        An ``ipywidget`` renderable in a Jupyter cell.  Call ``display(widget)``
        or simply return it as the last expression in a cell.
    """
    if backend == 'auto':
        for b in ('pythreejs', 'k3d'):
            try:
                return _show(geometry, b, instrument_display=instrument_display,
                             use_mesh=use_mesh, params=params,
                             width=width, height=height,
                             background=background, opacity=opacity)
            except ImportError:
                pass
        raise ImportError(
            "No WebGL backend found. Install one of:\n"
            "  pip install pythreejs\n"
            "  pip install k3d"
        )
    return _show(geometry, backend, instrument_display=instrument_display,
                 use_mesh=use_mesh, params=params,
                 width=width, height=height,
                 background=background, opacity=opacity)