Update 0.4.0
Python application / build (push) Has been cancelled

- Add\Rework UI
- Add Split Table and Listing
- Add Support Customazeble schems
This commit is contained in:
Igor20264
2026-09-04 22:28:39 +03:00
parent 516abe7b83
commit b38661f588
70 changed files with 69532 additions and 413 deletions
+461
View File
@@ -0,0 +1,461 @@
"""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()
+85
View File
@@ -0,0 +1,85 @@
Attribute VB_Name = "CheckPageFill"
' Heuristic check for half-empty pages inside an unfinished section (MIREA method guide, app. V).
' Run from Word: Tools → Macro → Macros → CheckPageFill_Run
' Findings may be FALSE POSITIVES (large figures, end of subsection, odd breaks).
Option Explicit
Private Const EMPTY_THRESHOLD As Double = 0.4
Public Sub CheckPageFill_Run()
Dim doc As Document
Dim i As Long, n As Long
Dim msg As String
Dim issues As Long
Dim secTitle As String, nextTitle As String
Dim emptyFrac As Double
Dim contentBottom As Double
Dim textH As Double, topM As Double, pageH As Double
Dim rng As Range
Dim vpos As Double
Set doc = ActiveDocument
doc.Repaginate
n = doc.ComputeStatistics(wdStatisticPages)
issues = 0
msg = "Проверка полупустых страниц (эвристика; возможны ложные срабатывания)" & vbCrLf
For i = 1 To n
Selection.GoTo What:=wdGoToPage, Which:=wdGoToAbsolute, Count:=i
If Selection.Sections(1).PageSetup.Orientation = wdOrientLandscape Then GoTo NextPage
pageH = Selection.Sections(1).PageSetup.PageHeight
topM = Selection.Sections(1).PageSetup.TopMargin
textH = pageH - topM - Selection.Sections(1).PageSetup.BottomMargin
If textH <= 0 Then GoTo NextPage
Set rng = doc.Bookmarks("\page").Range
On Error Resume Next
vpos = rng.Information(wdVerticalPositionRelativeToPage)
On Error GoTo 0
contentBottom = (vpos - topM) / textH
If contentBottom < 0 Then contentBottom = 0
If contentBottom > 1 Then contentBottom = 1
emptyFrac = 1# - contentBottom
If emptyFrac <= EMPTY_THRESHOLD Then GoTo NextPage
If i = n Then GoTo NextPage
secTitle = HeadingNear(doc, rng.Start)
Selection.GoTo What:=wdGoToPage, Which:=wdGoToAbsolute, Count:=i + 1
nextTitle = HeadingNear(doc, Selection.Start)
If StrComp(secTitle, nextTitle, vbTextCompare) <> 0 Then GoTo NextPage
If InStr(1, UCase$(secTitle), "СОДЕРЖАНИЕ", vbTextCompare) > 0 Then GoTo NextPage
issues = issues + 1
msg = msg & "стр. " & i & ": пустой низ ~" & Format(emptyFrac, "0%") & _
", раздел «" & secTitle & "» продолжается" & vbCrLf
NextPage:
Next i
If issues = 0 Then
MsgBox msg & vbCrLf & "Замечаний нет.", vbInformation
Else
MsgBox msg & vbCrLf & "Всего: " & issues & " (эвристика).", vbExclamation
End If
End Sub
Private Function HeadingNear(doc As Document, pos As Long) As String
Dim p As Paragraph
Dim s As String
On Error Resume Next
Set p = doc.Range(pos, pos).Paragraphs(1)
Dim k As Long
For k = 1 To 80
s = p.Style
If InStr(1, s, "Heading 1", vbTextCompare) > 0 Or InStr(1, s, "Заголовок 1", vbTextCompare) > 0 Then
HeadingNear = Replace(Trim$(p.Range.Text), vbCr, "")
Exit Function
End If
If p.Range.Start <= 1 Then Exit For
Set p = p.Previous
If p Is Nothing Then Exit For
Next k
HeadingNear = ""
End Function
+30
View File
@@ -0,0 +1,30 @@
"""Download official plantuml.jar into md2gost/vendor (for exe builds)."""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from md2gost.diagram_renderer import PLANTUML_JAR_URL, fetch_plantuml_jar, vendor_plantuml_path
def main() -> int:
dest = vendor_plantuml_path()
print(f"PlantUML: {PLANTUML_JAR_URL}")
print(f"Куда: {dest}")
if dest.is_file() and dest.stat().st_size > 1000:
print("Уже скачан.")
return 0
if fetch_plantuml_jar(dest):
print(f"Готово: {dest} ({dest.stat().st_size} байт)")
return 0
print("Не удалось скачать plantuml.jar (exe соберётся, диаграммы пойдут через kroki.io).")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+6
View File
@@ -0,0 +1,6 @@
"""PyInstaller entry point: GUI by default, CLI if a .md is passed."""
from md2gost.__main__ import main
if __name__ == "__main__":
main()
+4
View File
@@ -0,0 +1,4 @@
"""Runtime hook: matplotlib only needs Agg for font_manager on a foreign PC."""
import os
os.environ.setdefault("MPLBACKEND", "Agg")