b38661f588
Python application / build (push) Has been cancelled
- Add\Rework UI - Add Split Table and Listing - Add Support Customazeble schems
462 lines
15 KiB
Python
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()
|