codefig 0.17.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
codefig/steps.py ADDED
@@ -0,0 +1,203 @@
1
+ """Circled / squared step numbers (①②③) drawn as shapes, not font glyphs.
2
+
3
+ * :class:`Step` — one numbered badge; a normal node (layering role **hard**) you can
4
+ put in any Row/Col, attach to a :class:`~codefig.primitives.Box` corner with
5
+ ``Box(..., step=2)`` or place on an edge path with ``Edge(..., step=2)``.
6
+ * :class:`StepCounter` — hands out consecutive :class:`Step` badges that share one
7
+ style (``steps.next()`` → ①, ②, ③ …).
8
+
9
+ The digits are ordinary text centred in a vector circle / rounded square, so they
10
+ render identically in every font and at every scale (unlike the Unicode ①
11
+ glyphs, which many fonts lack).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any, List, Optional, Tuple, Union
17
+
18
+ from .node import Node
19
+ from .styles import Theme
20
+
21
+ STEP_STYLES = ("circle", "square")
22
+
23
+ StepLike = Union[int, str, "Step"]
24
+
25
+
26
+ class Step(Node):
27
+ """A numbered step badge (①-style), drawn as a filled or outlined shape.
28
+
29
+ Layering role: **hard** (edges route around it; check_overlaps() reports
30
+ anything it covers). On an edge (``Edge(step=…)``) it is checked like an
31
+ edge label.
32
+
33
+ Args:
34
+ n: The number or short text shown inside (``1``, ``"2a"``, ``"A"``).
35
+ style: ``"circle"`` (default) or ``"square"`` (rounded square).
36
+ color: Badge colour (px-independent CSS colour). Default: ``theme.highlight``
37
+ (``"#c0392b"``).
38
+ filled: ``True`` (default) → solid ``color`` disc with white digits;
39
+ ``False`` → white disc with a ``color`` outline and ``color`` digits.
40
+ size: Diameter / side length in px. Default: ``1.5 × font_size`` (≈ 16 px).
41
+ font_size: Digit size in px. Default: ``0.82 × theme.font_size``.
42
+ text_color: Digit colour override (default white when filled, else ``color``).
43
+ stroke_width: Outline width in px for ``filled=False`` (default 1.4).
44
+ **kwargs: Common :class:`~codefig.node.Node` options (``id``, ``obstacle``,
45
+ ``legend`` …).
46
+
47
+ Example:
48
+ >>> from codefig import Figure, Row, Box, Step
49
+ >>> fig = Figure(size="auto", cols=1)
50
+ >>> with fig.panel("a") as p:
51
+ ... p += Row([Step(1), Box("load data"), Step(2, filled=False), Box("train")])
52
+ >>> svg = fig.to_svg()
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ n: Union[int, str],
58
+ *,
59
+ style: str = "circle",
60
+ color: Optional[str] = None,
61
+ filled: bool = True,
62
+ size: Optional[float] = None,
63
+ font_size: Optional[float] = None,
64
+ text_color: Optional[str] = None,
65
+ stroke_width: float = 1.4,
66
+ **kwargs: Any,
67
+ ) -> None:
68
+ super().__init__(**kwargs)
69
+ if style not in STEP_STYLES:
70
+ raise ValueError(f"Step style must be one of {STEP_STYLES}, got {style!r}")
71
+ self.n = n
72
+ self.text = str(n)
73
+ self.style = style
74
+ self.color = color
75
+ self.filled = bool(filled)
76
+ self.size = None if size is None else float(size)
77
+ self.font_size = None if font_size is None else float(font_size)
78
+ self.text_color = text_color
79
+ self.stroke_width = float(stroke_width)
80
+
81
+ # ---- metrics ---------------------------------------------------------------
82
+ def _fs(self) -> float:
83
+ return self.font_size if self.font_size is not None else round(self.theme.font_size * 0.82, 2)
84
+
85
+ def diameter(self) -> float:
86
+ """Badge diameter / side in px (grows for multi-character labels)."""
87
+ from .textutil import measure_text
88
+
89
+ d = self.size if self.size is not None else round(self._fs() * 1.75, 1)
90
+ tw, _ = measure_text(self.text, self._fs(), weight="bold")
91
+ return max(d, tw + self._fs() * 0.7)
92
+
93
+ def _measure(self) -> Tuple[float, float]:
94
+ d = self.diameter()
95
+ return d, d
96
+
97
+ def _color(self) -> str:
98
+ return self.color or self.theme.highlight
99
+
100
+ # ---- drawing ---------------------------------------------------------------
101
+ def paint(self, buf: List[str], cx: float, cy: float, d: Optional[float] = None) -> None:
102
+ """Draw the badge centred at page coordinates (``cx``, ``cy``)."""
103
+ from .render_svg import draw_circle, draw_rect, draw_text
104
+ from .textutil import measure_text
105
+
106
+ d = self.diameter() if d is None else d
107
+ col = self._color()
108
+ fill = col if self.filled else "#ffffff"
109
+ stroke = "none" if self.filled else col
110
+ sw = 0.0 if self.filled else self.stroke_width
111
+ # marked as a glyph so lint does not count the bold numeral as a bold label
112
+ buf.append('<g data-cf-glyph="step">')
113
+ if self.style == "circle":
114
+ draw_circle(buf, cx, cy, d / 2 - sw / 2, fill=fill, stroke=stroke, stroke_width=sw)
115
+ else:
116
+ s = d - sw
117
+ draw_rect(buf, cx - s / 2, cy - s / 2, s, s, fill=fill, stroke=stroke,
118
+ stroke_width=sw, radius=round(d * 0.2, 2))
119
+ fs = self._fs()
120
+ tc = self.text_color or ("#ffffff" if self.filled else col)
121
+ tw, _ = measure_text(self.text, fs, weight="bold")
122
+ draw_text(buf, self.text, cx - tw / 2, cy + fs * 0.36, font_size=fs, weight="bold",
123
+ fill=tc)
124
+ buf.append("</g>")
125
+
126
+ def draw(self, buf: List[str], ox: float = 0.0, oy: float = 0.0) -> None:
127
+ g = self.geom
128
+ self.paint(buf, g.cx + ox, g.cy + oy, min(g.w, g.h))
129
+
130
+
131
+ class StepCounter:
132
+ """Hands out consecutive :class:`Step` badges that share one style.
133
+
134
+ Args:
135
+ style: ``"circle"`` (default) or ``"square"``.
136
+ color: Badge colour (default ``theme.highlight``).
137
+ filled: Solid (default) or outlined badges.
138
+ start: First number (default 1).
139
+ size: Badge diameter in px (default ≈ 1.5 × font size).
140
+ font_size: Digit size in px.
141
+ theme: Theme for defaults.
142
+
143
+ Example:
144
+ >>> from codefig import Box, Edge, StepCounter
145
+ >>> steps = StepCounter(color="#1f6fb2")
146
+ >>> a, b, c = Box("parse"), Box("plan"), Box("run")
147
+ >>> e1 = Edge(a, b, step=steps.next()) # ①
148
+ >>> e2 = Edge(b, c, step=steps.next()) # ②
149
+ >>> steps.peek()
150
+ 3
151
+ """
152
+
153
+ def __init__(
154
+ self,
155
+ *,
156
+ style: str = "circle",
157
+ color: Optional[str] = None,
158
+ filled: bool = True,
159
+ start: int = 1,
160
+ size: Optional[float] = None,
161
+ font_size: Optional[float] = None,
162
+ theme: Optional[Theme] = None,
163
+ ) -> None:
164
+ if style not in STEP_STYLES:
165
+ raise ValueError(f"StepCounter style must be one of {STEP_STYLES}, got {style!r}")
166
+ self.style = style
167
+ self.color = color
168
+ self.filled = bool(filled)
169
+ self.size = size
170
+ self.font_size = font_size
171
+ self.theme = theme
172
+ self._next = int(start)
173
+
174
+ def next(self, **overrides: Any) -> Step:
175
+ """Return a new :class:`Step` with the next number (``overrides`` → Step kwargs)."""
176
+ kw = dict(style=self.style, color=self.color, filled=self.filled, size=self.size,
177
+ font_size=self.font_size, theme=self.theme)
178
+ kw.update(overrides)
179
+ s = Step(self._next, **kw)
180
+ self._next += 1
181
+ return s
182
+
183
+ def peek(self) -> int:
184
+ """The number the next :meth:`next` call will use."""
185
+ return self._next
186
+
187
+ def reset(self, start: int = 1) -> None:
188
+ """Restart numbering at ``start``."""
189
+ self._next = int(start)
190
+
191
+
192
+ def as_step(v: Optional[StepLike], theme: Optional[Theme] = None) -> Optional[Step]:
193
+ """Coerce ``None`` / an int / a str / a :class:`Step` to a Step (or None)."""
194
+ if v is None or v is False:
195
+ return None
196
+ if isinstance(v, Step):
197
+ return v
198
+ if isinstance(v, (int, str)):
199
+ return Step(v, theme=theme)
200
+ raise TypeError(f"step must be an int, str or Step, got {type(v).__name__}")
201
+
202
+
203
+ __all__ = ["Step", "StepCounter", "as_step", "STEP_STYLES"]