"""
Pin decorators module for JITX Standard Library
This module provides pin decorator definitions for use with symbol pins.
Decorators are collections of artwork that can be attached to pins to indicate
special properties like active-low signals, open-collector outputs, clock inputs,
and pin cardinality (input/output/bidirectional).
"""
from __future__ import annotations
from enum import Enum
from dataclasses import dataclass, field
from jitx.net import Port
from jitx.property import Property
from jitx.shapes import Shape
from jitx.shapes.primitive import Polygon, Circle, Polyline
from jitx.symbol import Direction
from jitx.transform import Point, Transform
from .common import DEF_LINE_WIDTH
# Decorator placement constants
[docs]
class DecoratorPlacement(Enum):
"""
Placement reference for pin decorators
Indicates whether the decorator is placed inside or outside the box symbol.
"""
OUTSIDE = "outside"
INSIDE = "inside"
[docs]
class OpenCollectorType(Enum):
"""
Open collector output types
Defines whether the open collector is a low-side sink (NPN/NMOS) or
high-side source (PNP/PMOS).
"""
SINK = "sink"
"""Low side open collector (NPN BJT or NMOS FET)"""
SOURCE = "source"
"""High side open collector (PNP BJT or PMOS FET)"""
[docs]
class CardinalityType(Enum):
"""
Pin cardinality types
Defines the direction of signal flow for pin decorators.
"""
INPUT = "input"
OUTPUT = "output"
BIDIRECTIONAL = "bidirectional"
# Active-Low decorator constants
DEF_ACTIVE_LOW_DIAM = 0.7
DEF_ACTIVE_LOW_NUDGE = (0.0, 0.0)
# Open-Collector decorator constants
DEF_OC_DEC_SIZE = 0.7
DEF_OC_TYPE = OpenCollectorType.SINK
DEF_OC_PULL = False
# Clock decorator constants
DEF_CLOCK_SIZE = 0.85
# Cardinality decorator constants
DEF_CARD_SIZE = (0.85, 0.85)
[docs]
@dataclass
class DecoratorSpec:
"""
Base class for all pin decorator specifications
This is the base type for data structures that specify how pins should be decorated
(e.g., Cardinality, ActiveLow, etc.)
"""
[docs]
def shapes(self) -> tuple[Shape, ...]:
"""Return the shapes that make up this decorator"""
raise NotImplementedError("Subclasses must implement shapes()")
[docs]
def placement(self) -> DecoratorPlacement:
"""Return the placement type for this decorator"""
raise NotImplementedError("Subclasses must implement placement()")
# Not yet supported
[docs]
@dataclass
class DecoratorConfig:
"""Default configurations for all decorator types"""
cardinality: Cardinality = field(
default_factory=lambda: Cardinality(
cardinality=CardinalityType.BIDIRECTIONAL,
size=DEF_CARD_SIZE,
nudge=None,
)
)
"""Configuration for cardinality decorators"""
active_low: ActiveLow = field(
default_factory=lambda: ActiveLow(
diameter=DEF_ACTIVE_LOW_DIAM,
nudge=DEF_ACTIVE_LOW_NUDGE,
)
)
"""Configuration for active-low decorators"""
open_collector: OpenCollector = field(
default_factory=lambda: OpenCollector(
width=DEF_OC_DEC_SIZE,
line_width=DEF_LINE_WIDTH,
oc_type=DEF_OC_TYPE,
pullup_down=DEF_OC_PULL,
)
)
"""Configuration for open-collector decorators"""
clock: Clock = field(
default_factory=lambda: Clock(
size=DEF_CLOCK_SIZE,
line_width=DEF_LINE_WIDTH,
)
)
"""Configuration for clock decorators"""
[docs]
@dataclass
class PinDecorator(Property):
"""
Pin decorator property. This should be assigned to a component port for
pin-specific decoration.
"""
spec: DecoratorSpec
"""The decorator specification that this property represents"""
[docs]
def shapes(self) -> tuple[Shape, ...]:
return self.spec.shapes()
[docs]
@dataclass
class ActiveLow(DecoratorSpec):
"""
Active-low (bubble) pin decorator specification
Creates a circle/bubble symbol to indicate active-low signals.
"""
diameter: float = DEF_ACTIVE_LOW_DIAM
"""Diameter of the bubble in symbol grid units"""
nudge: tuple[float, float] = DEF_ACTIVE_LOW_NUDGE
"""Offset to position the bubble aesthetically"""
[docs]
def shapes(self) -> tuple[Shape, ...]:
return (
Transform((-self.diameter / 2.0, 0))
* Transform(self.nudge)
* Circle(diameter=self.diameter),
)
[docs]
def placement(self) -> DecoratorPlacement:
return DecoratorPlacement.OUTSIDE
[docs]
@dataclass
class OpenCollector(DecoratorSpec):
"""
Open collector pin decorator specification
Creates a diamond symbol with optional hat and pullup/pulldown indicator.
"""
oc_type: OpenCollectorType = OpenCollectorType.SINK
"""Type of open collector (sink or source)"""
width: float = DEF_OC_DEC_SIZE
"""Size of the diamond symbol"""
line_width: float = DEF_LINE_WIDTH
"""Stroke width for the symbol lines"""
pullup_down: bool = DEF_OC_PULL
"""Whether to show internal pullup/pulldown resistor indicator"""
nudge: tuple[float, float] | None = None
"""Position offset for the symbol"""
def __post_init__(self):
if self.nudge is None:
# Default nudge places the symbol inside the component body
self.nudge = (self.width, 0.0)
[docs]
def shapes(self) -> tuple[Shape, ...]:
w2 = self.width / 2.0
shapes = []
# Create diamond shape
diamond_points = [
(0.0, w2),
(w2, 0.0),
(0.0, -w2),
(-w2, 0.0),
(0.0, w2), # Close the shape
]
diamond = Polyline(self.line_width, diamond_points)
shapes.append(diamond)
# Add hat based on type
hat_points = [(-w2, 0.0), (w2, 0.0)]
if self.oc_type == OpenCollectorType.SOURCE:
hat_y = w2
else: # SINK
hat_y = -w2
hat = Polyline(self.line_width, hat_points)
hat_transform = Transform((0.0, hat_y))
shapes.append(hat_transform * hat)
# Add pullup/pulldown indicator if requested
if self.pullup_down:
pullup_line = Polyline(self.line_width, [(-w2, 0.0), (w2, 0.0)])
shapes.append(pullup_line)
# Apply nudge to all shapes
assert self.nudge is not None # __post_init__ ensures this is never None
return tuple(Transform(self.nudge) * shape for shape in shapes)
[docs]
def placement(self) -> DecoratorPlacement:
return DecoratorPlacement.INSIDE
[docs]
@dataclass
class Clock(DecoratorSpec):
"""
Clock pin decorator specification
Creates a ">" shaped symbol to indicate clock inputs.
"""
size: float | tuple[float, float] = DEF_CLOCK_SIZE
"""Size of the clock symbol"""
line_width: float = DEF_LINE_WIDTH
"""Stroke width for the symbol lines"""
[docs]
def shapes(self) -> tuple[Shape, ...]:
if isinstance(self.size, tuple):
w, h = self.size
else:
w = h = self.size
h2 = h / 2.0
points = [(0.0, h2), (w / 2.0, 0.0), (0.0, -h2)]
clock_shape = Polyline(self.line_width, points)
return (clock_shape,)
[docs]
def placement(self) -> DecoratorPlacement:
return DecoratorPlacement.INSIDE
[docs]
@dataclass
class Cardinality(DecoratorSpec):
"""
Pin cardinality specification
This is a data structure that specifies the direction of signal flow for a pin
(input, output, or bidirectional).
"""
cardinality: CardinalityType
"""The direction of signal flow for this pin"""
size: float | tuple[float, float] = DEF_CARD_SIZE
"""The size of the arrow symbols"""
nudge: tuple[float, float] | None = None
"""The nudge offset for the arrow symbols"""
def _default_nudge(self) -> tuple[float, float]:
if isinstance(self.size, tuple):
w = self.size[0]
else:
w = self.size
# Default nudge places the arrow outside the symbol body (negative for outside)
return (-w / 2.0, 0.0)
def __post_init__(self):
"""Calculate default nudge if not provided"""
if self.nudge is None:
self.nudge = self._default_nudge()
[docs]
def shapes(self) -> tuple[Shape, ...]:
if isinstance(self.size, tuple):
w, h = self.size
h2 = h / 2.0
else:
w = self.size
h2 = w / 2.0
shapes = []
if self.cardinality == CardinalityType.INPUT:
# Input arrow points inward (right)
shapes.append(Polygon([(0.0, h2), (w / 2.0, 0.0), (0.0, -h2)]))
elif self.cardinality == CardinalityType.OUTPUT:
# Output arrow points outward (left)
shapes.append(Polygon([(0.0, h2), (-w / 2.0, 0.0), (0.0, -h2)]))
elif self.cardinality == CardinalityType.BIDIRECTIONAL:
# Bidirectional has both arrows
shapes.append(Polygon([(0.0, h2), (w / 2.0, 0.0), (0.0, -h2)]))
shapes.append(Polygon([(0.0, h2), (-w / 2.0, 0.0), (0.0, -h2)]))
assert self.nudge is not None # __post_init__ ensures this is never None
return tuple(Transform(self.nudge) * shape for shape in shapes)
[docs]
def placement(self) -> DecoratorPlacement:
return DecoratorPlacement.OUTSIDE
[docs]
def placement(spec: DecoratorSpec) -> DecoratorPlacement:
"""Get the placement type for a decorator spec"""
return spec.placement()
[docs]
def decorate(port: Port, spec: DecoratorSpec) -> None:
"""Decorate a port with a pin decorator spec"""
PinDecorator(spec).assign(port)
[docs]
def draw(
decorator: DecoratorSpec, direction: Direction, at: Point
) -> tuple[Shape, ...]:
if direction == Direction.Left:
transform = Transform(at, 0, (1.0, 1.0))
elif direction == Direction.Down:
transform = Transform(at, 90, (1.0, 1.0))
elif direction == Direction.Right:
transform = Transform(at, 0, (-1.0, 1.0))
elif direction == Direction.Up:
transform = Transform(at, 90, (-1.0, 1.0))
else:
raise ValueError(f"Invalid direction: {direction}")
return tuple(transform * shape for shape in decorator.shapes())
# Helpers for supplying decorator types
[docs]
def Output(
size: float | tuple[float, float] = DEF_CARD_SIZE,
nudge: tuple[float, float] | None = None,
):
return Cardinality(CardinalityType.OUTPUT, size, nudge)
[docs]
def Bidirectional(
size: float | tuple[float, float] = DEF_CARD_SIZE,
nudge: tuple[float, float] | None = None,
):
return Cardinality(CardinalityType.BIDIRECTIONAL, size, nudge)
[docs]
def OpenCollectorSource(
width: float = DEF_OC_DEC_SIZE,
line_width: float = DEF_LINE_WIDTH,
pullup_down: bool = DEF_OC_PULL,
nudge: tuple[float, float] | None = None,
):
return OpenCollector(
OpenCollectorType.SOURCE, width, line_width, pullup_down, nudge
)
[docs]
def OpenCollectorSink(
width: float = DEF_OC_DEC_SIZE,
line_width: float = DEF_LINE_WIDTH,
pullup_down: bool = DEF_OC_PULL,
nudge: tuple[float, float] | None = None,
):
return OpenCollector(OpenCollectorType.SINK, width, line_width, pullup_down, nudge)