Files
md_to_gost/scripts/_gen_bpmn_sprites.py
T
Igor20264 b38661f588
Python application / build (push) Has been cancelled
Update 0.4.0
- Add\Rework UI
- Add Split Table and Listing
- Add Support Customazeble schems
2026-09-04 22:28:39 +03:00

462 lines
15 KiB
Python

"""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()