"""One-shot generator for BPMN.puml geometric sprites. Run from repo root.""" from __future__ import annotations import math from pathlib import Path N = 48 HEX = "0123456789ABCDEF" SS = 2 def clamp01(v: float) -> float: return 0.0 if v < 0 else 1.0 if v > 1 else v def mix(grid: list[list[float]], x: int, y: int, v: float) -> None: if 0 <= x < N and 0 <= y < N: grid[y][x] = max(grid[y][x], clamp01(v)) def ring(d: float, r: float, hw: float) -> float: return clamp01(1.0 - abs(d - r) / hw) def disk(d: float, r: float, hw: float = 0.65) -> float: if d <= r - hw: return 1.0 if d >= r + hw: return 0.0 return clamp01((r + hw - d) / (2 * hw)) def line_cov(px: float, py: float, x0: float, y0: float, x1: float, y1: float, hw: float) -> float: dx, dy = x1 - x0, y1 - y0 len2 = dx * dx + dy * dy if len2 < 1e-6: return disk(math.hypot(px - x0, py - y0), 0, hw) t = clamp01(((px - x0) * dx + (py - y0) * dy) / len2) qx, qy = x0 + t * dx, y0 + t * dy return disk(math.hypot(px - qx, py - qy), 0, hw) def diamond_m(px: float, py: float, cx: float, cy: float) -> float: return abs(px - cx) + abs(py - cy) def sample_event( px: float, py: float, *, kind: str, dashed: bool, ) -> float: cx = cy = (N - 1) / 2 d = math.hypot(px - cx, py - cy) v = 0.0 if kind == "start": v = max(v, ring(d, 18.2, 1.15)) elif kind == "inter": v = max(v, ring(d, 18.4, 1.05)) v = max(v, ring(d, 14.6, 1.05)) elif kind == "end": v = max(v, ring(d, 17.6, 2.55)) elif kind == "endfill": v = max(v, ring(d, 17.6, 2.55)) v = max(v, disk(d, 7.2, 0.7)) if dashed and v > 0: ang = (math.atan2(py - cy, px - cx) + math.pi) / (2 * math.pi) if int(ang * 18) % 2 == 1: v *= 0.08 return v def icon_msg(px: float, py: float, cx: float, cy: float, filled: bool) -> float: # envelope 16x10 around center x0, y0, x1, y1 = cx - 8, cy - 5, cx + 8, cy + 5 v = 0.0 # outer rect inside = (x0 + 0.8 <= px <= x1 - 0.8) and (y0 + 0.8 <= py <= y1 - 0.8) on_border = ( (abs(px - x0) < 1.1 or abs(px - x1) < 1.1) and (y0 - 0.4 <= py <= y1 + 0.4) ) or ( (abs(py - y0) < 1.1 or abs(py - y1) < 1.1) and (x0 - 0.4 <= px <= x1 + 0.4) ) if filled: if inside or on_border: v = 1.0 # inverted flap (white cut) approximated by not drawing center of flap v = max(v, line_cov(px, py, x0, y0, cx, cy + 1.5, 0.9)) v = max(v, line_cov(px, py, x1, y0, cx, cy + 1.5, 0.9)) return v v = max(v, 1.0 if on_border else 0.0) v = max(v, line_cov(px, py, x0, y0, cx, cy + 1.8, 0.95)) v = max(v, line_cov(px, py, x1, y0, cx, cy + 1.8, 0.95)) return v def icon_timer(px: float, py: float, cx: float, cy: float) -> float: d = math.hypot(px - cx, py - cy) v = ring(d, 7.2, 1.0) v = max(v, line_cov(px, py, cx, cy, cx, cy - 5.2, 0.9)) v = max(v, line_cov(px, py, cx, cy, cx + 3.6, cy, 0.9)) return v def icon_error(px: float, py: float, cx: float, cy: float) -> float: pts = [ (cx + 1, cy - 8), (cx - 2, cy - 1), (cx + 3, cy - 1), (cx - 1, cy + 8), (cx + 0.5, cy + 1.5), (cx - 3.5, cy + 1.5), ] v = 0.0 for a, b in zip(pts, pts[1:] + pts[:1]): v = max(v, line_cov(px, py, a[0], a[1], b[0], b[1], 1.0)) return v def icon_signal(px: float, py: float, cx: float, cy: float, filled: bool) -> float: a, b, c = (cx, cy - 7.5), (cx - 7.2, cy + 6), (cx + 7.2, cy + 6) v = 0.0 v = max(v, line_cov(px, py, *a, *b, 1.05)) v = max(v, line_cov(px, py, *b, *c, 1.05)) v = max(v, line_cov(px, py, *c, *a, 1.05)) if filled: # barycentric fill def area(p, q, r): return (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]) p = (px, py) a0 = area(a, b, c) if abs(a0) > 0.1: w1 = area(p, b, c) / a0 w2 = area(a, p, c) / a0 w3 = area(a, b, p) / a0 if min(w1, w2, w3) >= -0.02: v = max(v, 1.0) return v def icon_esc(px: float, py: float, cx: float, cy: float) -> float: v = 0.0 v = max(v, line_cov(px, py, cx, cy + 7, cx, cy - 2, 1.1)) v = max(v, line_cov(px, py, cx, cy - 2, cx - 6, cy + 4, 1.05)) v = max(v, line_cov(px, py, cx, cy - 2, cx + 6, cy + 4, 1.05)) v = max(v, line_cov(px, py, cx - 6, cy + 4, cx + 6, cy + 4, 1.0)) return v def icon_link(px: float, py: float, cx: float, cy: float) -> float: v = line_cov(px, py, cx - 8, cy, cx + 5, cy, 1.1) v = max(v, line_cov(px, py, cx + 5, cy, cx + 1, cy - 4, 1.05)) v = max(v, line_cov(px, py, cx + 5, cy, cx + 1, cy + 4, 1.05)) return v def icon_comp(px: float, py: float, cx: float, cy: float) -> float: v = 0.0 for ox in (-4.2, 3.2): a, b, c = (cx + ox + 4, cy), (cx + ox - 3.5, cy - 6), (cx + ox - 3.5, cy + 6) v = max(v, line_cov(px, py, *a, *b, 1.0)) v = max(v, line_cov(px, py, *a, *c, 1.0)) v = max(v, line_cov(px, py, *b, *c, 1.0)) return v def icon_cond(px: float, py: float, cx: float, cy: float) -> float: x0, y0, x1, y1 = cx - 6, cy - 8, cx + 6, cy + 8 v = 0.0 on = ( (abs(px - x0) < 1.0 or abs(px - x1) < 1.0) and (y0 <= py <= y1) ) or ( (abs(py - y0) < 1.0 or abs(py - y1) < 1.0) and (x0 <= px <= x1) ) if on: v = 1.0 for yy in (cy - 3, cy, cy + 3): v = max(v, line_cov(px, py, cx - 3.5, yy, cx + 3.5, yy, 0.8)) return v def icon_multi(px: float, py: float, cx: float, cy: float) -> float: r = 7.4 pts = [] for i in range(5): a = -math.pi / 2 + i * 2 * math.pi / 5 pts.append((cx + r * math.cos(a), cy + r * math.sin(a))) v = 0.0 for a, b in zip(pts, pts[1:] + pts[:1]): v = max(v, line_cov(px, py, *a, *b, 1.0)) return v def icon_par_multi(px: float, py: float, cx: float, cy: float) -> float: v = ring(math.hypot(px - cx, py - cy), 7.4, 1.0) v = max(v, line_cov(px, py, cx, cy - 5, cx, cy + 5, 1.05)) v = max(v, line_cov(px, py, cx - 5, cy, cx + 5, cy, 1.05)) return v def icon_cancel(px: float, py: float, cx: float, cy: float) -> float: v = line_cov(px, py, cx - 6, cy - 6, cx + 6, cy + 6, 1.15) v = max(v, line_cov(px, py, cx + 6, cy - 6, cx - 6, cy + 6, 1.15)) return v def icon_user(px: float, py: float, cx: float, cy: float) -> float: v = disk(math.hypot(px - cx, py - (cy - 3.2)), 3.1, 0.7) v = max(v, ring(math.hypot(px - cx, py - (cy - 3.2)), 3.1, 0.9)) # shoulders v = max(v, line_cov(px, py, cx - 6, cy + 7, cx + 6, cy + 7, 1.1)) v = max(v, line_cov(px, py, cx - 6, cy + 7, cx - 4, cy + 1.5, 1.0)) v = max(v, line_cov(px, py, cx + 6, cy + 7, cx + 4, cy + 1.5, 1.0)) return v def icon_service(px: float, py: float, cx: float, cy: float) -> float: v = ring(math.hypot(px - cx, py - cy), 4.2, 1.15) for i in range(8): a = i * math.pi / 4 v = max( v, line_cov( px, py, cx + 3.2 * math.cos(a), cy + 3.2 * math.sin(a), cx + 7.4 * math.cos(a), cy + 7.4 * math.sin(a), 1.05, ), ) return v def icon_script(px: float, py: float, cx: float, cy: float) -> float: return icon_cond(px, py, cx, cy) def icon_manual(px: float, py: float, cx: float, cy: float) -> float: v = line_cov(px, py, cx - 7, cy + 2, cx + 7, cy + 2, 1.2) v = max(v, line_cov(px, py, cx - 7, cy + 2, cx - 5, cy - 4, 1.0)) v = max(v, line_cov(px, py, cx - 2, cy + 2, cx - 1, cy - 6, 1.0)) v = max(v, line_cov(px, py, cx + 2, cy + 2, cx + 3, cy - 5.5, 1.0)) v = max(v, line_cov(px, py, cx + 6, cy + 2, cx + 7, cy - 3, 1.0)) return v def icon_rule(px: float, py: float, cx: float, cy: float) -> float: v = 0.0 for yy in (cy - 6, cy, cy + 6): v = max(v, line_cov(px, py, cx - 7, yy, cx + 7, yy, 0.95)) for xx in (cx - 7, cx, cx + 7): v = max(v, line_cov(px, py, xx, cy - 6, xx, cy + 6, 0.95)) return v def icon_plusbox(px: float, py: float, cx: float, cy: float) -> float: x0, y0, x1, y1 = cx - 7, cy - 7, cx + 7, cy + 7 v = 0.0 on = ( (abs(px - x0) < 1.05 or abs(px - x1) < 1.05) and (y0 <= py <= y1) ) or ( (abs(py - y0) < 1.05 or abs(py - y1) < 1.05) and (x0 <= px <= x1) ) if on: v = 1.0 v = max(v, line_cov(px, py, cx, cy - 4.5, cx, cy + 4.5, 1.05)) v = max(v, line_cov(px, py, cx - 4.5, cy, cx + 4.5, cy, 1.05)) return v ICONS = { "": None, "msg": lambda px, py, cx, cy: icon_msg(px, py, cx, cy, False), "msgf": lambda px, py, cx, cy: icon_msg(px, py, cx, cy, True), "timer": icon_timer, "error": icon_error, "signal": lambda px, py, cx, cy: icon_signal(px, py, cx, cy, False), "signalf": lambda px, py, cx, cy: icon_signal(px, py, cx, cy, True), "esc": icon_esc, "link": icon_link, "comp": icon_comp, "cond": icon_cond, "multi": icon_multi, "par": icon_par_multi, "cancel": icon_cancel, "user": icon_user, "service": icon_service, "script": icon_script, "manual": icon_manual, "rule": icon_rule, "plusbox": icon_plusbox, } def raster(draw) -> list[list[float]]: acc = [[0.0] * N for _ in range(N)] big = N * SS for y in range(big): for x in range(big): v = draw((x + 0.5) / SS, (y + 0.5) / SS) acc[y // SS][x // SS] += v s = float(SS * SS) return [[acc[y][x] / s for x in range(N)] for y in range(N)] def to_sprite(name: str, grid: list[list[float]]) -> str: lines = [f"sprite ${name} [{N}x{N}/16] {{"] for row in grid: lines.append("".join(HEX[min(15, int(round(v * 15)))] for v in row)) lines.append("}") return "\n".join(lines) def event_grid(kind: str, icon: str, dashed: bool = False) -> list[list[float]]: cx = cy = (N - 1) / 2.0 fn = ICONS[icon] def draw(px, py): v = sample_event(px, py, kind=kind, dashed=dashed) if fn is not None: v = max(v, fn(px, py, cx, cy)) return v return raster(draw) def gateway_grid(mark: str) -> list[list[float]]: cx = cy = (N - 1) / 2.0 r = 21.0 def draw(px, py): m = diamond_m(px, py, cx, cy) v = ring(m, r, 1.25) if mark == "x": v = max(v, line_cov(px, py, cx - 8, cy - 8, cx + 8, cy + 8, 1.25)) v = max(v, line_cov(px, py, cx + 8, cy - 8, cx - 8, cy + 8, 1.25)) elif mark == "+": v = max(v, line_cov(px, py, cx, cy - 9, cx, cy + 9, 1.3)) v = max(v, line_cov(px, py, cx - 9, cy, cx + 9, cy, 1.3)) elif mark == "o": v = max(v, ring(math.hypot(px - cx, py - cy), 7.0, 1.15)) elif mark == "*": for i in range(4): a = i * math.pi / 4 v = max( v, line_cov( px, py, cx - 7 * math.cos(a), cy - 7 * math.sin(a), cx + 7 * math.cos(a), cy + 7 * math.sin(a), 1.05, ), ) elif mark == "ev": v = max(v, icon_multi(px, py, cx, cy)) v = max(v, ring(math.hypot(px - cx, py - cy), 10.5, 0.95)) elif mark == "pev": v = max(v, icon_par_multi(px, py, cx, cy)) return v return raster(draw) EVENTS = [ ("evStart", "start", "", False), ("evStartMsg", "start", "msg", False), ("evStartTimer", "start", "timer", False), ("evStartSignal", "start", "signal", False), ("evStartCond", "start", "cond", False), ("evStartError", "start", "error", False), ("evStartEsc", "start", "esc", False), ("evStartMulti", "start", "multi", False), ("evStartPar", "start", "par", False), ("evStartNI", "start", "", True), ("evStartMsgNI", "start", "msg", True), ("evStartTimerNI", "start", "timer", True), ("evStartSignalNI", "start", "signal", True), ("evInter", "inter", "", False), ("evCatchMsg", "inter", "msg", False), ("evCatchTimer", "inter", "timer", False), ("evCatchSignal", "inter", "signal", False), ("evCatchError", "inter", "error", False), ("evCatchEsc", "inter", "esc", False), ("evCatchCond", "inter", "cond", False), ("evCatchLink", "inter", "link", False), ("evCatchComp", "inter", "comp", False), ("evCatchCancel", "inter", "cancel", False), ("evCatchMulti", "inter", "multi", False), ("evCatchPar", "inter", "par", False), ("evThrowMsg", "inter", "msgf", False), ("evThrowSignal", "inter", "signalf", False), ("evThrowEsc", "inter", "esc", False), ("evThrowLink", "inter", "link", False), ("evThrowComp", "inter", "comp", False), ("evThrowMulti", "inter", "multi", False), ("evEnd", "end", "", False), ("evEndMsg", "end", "msgf", False), ("evEndError", "end", "error", False), ("evEndEsc", "end", "esc", False), ("evEndCancel", "end", "cancel", False), ("evEndComp", "end", "comp", False), ("evEndSignal", "end", "signalf", False), ("evEndMulti", "end", "multi", False), ("evTerminate", "endfill", "", False), ("evBoundMsg", "inter", "msg", False), ("evBoundTimer", "inter", "timer", False), ("evBoundError", "inter", "error", False), ("evBoundEsc", "inter", "esc", False), ("evBoundSignal", "inter", "signal", False), ("evBoundCancel", "inter", "cancel", False), ("evBoundComp", "inter", "comp", False), ("evBoundCond", "inter", "cond", False), ("evBoundMsgNI", "inter", "msg", True), ("evBoundTimerNI", "inter", "timer", True), ("evBoundEscNI", "inter", "esc", True), ("evBoundSignalNI", "inter", "signal", True), ("evBoundCondNI", "inter", "cond", True), ] GATES = [ ("gwXor", "x"), ("gwAnd", "+"), ("gwOr", "o"), ("gwComplex", "*"), ("gwEvent", "ev"), ("gwParEvent", "pev"), ] TASK_ICONS = ["user", "service", "script", "manual", "rule", "plusbox", "msg", "msgf"] def main() -> None: chunks = ["' auto-generated geometric sprites (BPMN 2.0 notation)"] for name, kind, icon, dashed in EVENTS: chunks.append(to_sprite(name, event_grid(kind, icon, dashed))) chunks.append("") for name, mark in GATES: chunks.append(to_sprite(name, gateway_grid(mark))) chunks.append("") cx = cy = (N - 1) / 2.0 for icon in TASK_ICONS: fn = ICONS[icon] grid = raster(lambda px, py, fn=fn: fn(px, py, cx, cy)) chunks.append(to_sprite("ic" + icon.capitalize() if icon != "msgf" else "icMsgf", grid)) chunks.append("") # plusbox already as icPlusbox; loop/adhoc/mi as simple extra def loop_draw(px, py): v = ring(math.hypot(px - cx, py - (cy - 1)), 8.5, 1.1) v = max(v, line_cov(px, py, cx + 6, cy + 2, cx + 10, cy + 6, 1.05)) v = max(v, line_cov(px, py, cx + 6, cy + 2, cx + 2, cy + 6, 1.05)) return v chunks.append(to_sprite("icLoop", raster(loop_draw))) chunks.append("") Path("_bpmn_sprites.puml").write_text("\n".join(chunks), encoding="utf-8") print("wrote", len(chunks), "chunks") if __name__ == "__main__": main()