audit: pragul de rand derivat din font-size; si cele doua diagrame chiar stricate

Al doilea defect al uneltei: inaltimea de rand era constanta LINE_H=13,
calibrata pe 12 px, deci la etichetele de 10 px cu pas de 12 px credea ca
randurile se ating. Acum e LINE_H_RATIO x font-size-ul efectiv, mostenit
corect de pe <g>. Factorul e 1.07, nu 1.15: agentul a incercat valorile mari,
a masurat cu metrice reale de glif si a aratat ca scot cinci fantome noi.

Cele doua probleme reale pe care unealta reparata le-a scos la iveala:
atmi_ansamblu avea un caption de 792 px intr-un viewBox de 720, deci se taia
la margine; la selectia_proximitate se calcau eticheta sagetii si titlul.
Ambele reparate si privite la 1:1.

Cele 92 de diagrame: 0 probleme. Selftest-ul trece, si prinde in continuare o
suprapunere fabricata deliberat - unealta tace pentru ca e curat, nu pentru ca
a orbit.

Ramane, raportat si nereparat: CHAR_W = 5.7 e tot calibrat pe 12 px, deci
latimea textelor mici e supraestimata. Aceeasi clasa de bug, n-a produs inca
raportari gresite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
This commit is contained in:
Claude Agent
2026-09-22 20:45:50 +00:00
parent 5b157116d3
commit 43e6fbcd32
9 changed files with 488 additions and 378 deletions

View File

@@ -14,8 +14,10 @@ import re
import sys
from pathlib import Path
CHAR_W = 5.7 # latime medie per caracter la 12px sans-serif (calibrat pe randare)
LINE_H = 13 # înălțimea unei linii de text
CHAR_W = 5.7 # latime medie per caracter la 12px sans-serif (calibrat pe randare)
LINE_H_RATIO = 1.07 # înălțimea rândului = font-size efectiv * ratio (același prag ca
# LINE_H=13 la 12px, dar scade pentru etichetele de 10px)
DEFAULT_FONT = 12.0 # font-size când nu e precizat nici pe element, nici moștenit
_TRANSFORM = re.compile(r'translate\(\s*([-\d.]+)\s*[, ]\s*([-\d.]+)\s*\)')
_ELEMENT = re.compile(
@@ -25,29 +27,43 @@ _ROLE_ENTRY = re.compile(r'intrar|entry', re.I)
_ROLE_TP = re.compile(r'\bTP\d*|\bțint|\btarget', re.I)
def _font_px(css: str) -> float | None:
"""font-size în px dintr-un atribut/`style` (ex: `font-size:10px`, `font-size="10"`, `font:12px`)."""
m = re.search(r'font-size\s*[:=]\s*"?([\d.]+)', css)
if m:
return float(m.group(1))
m = re.search(r'(?<!-)font\s*:\s*([\d.]+)px', css)
return float(m.group(1)) if m else None
def _elements(svg: str):
"""Evenimentele SVG în ordine, cu translația `<g>` acumulată aplicată.
"""Evenimentele SVG în ordine, cu translația și font-size-ul moștenit aplicate.
Fiecare `<g transform="translate(dx,dy)">` deschide un cadru; elementele
dinăuntru moștenesc deplasarea, oricât de adânc ar fi imbricate.
dinăuntru moștenesc deplasarea și font-size-ul, oricât de adânc ar fi
imbricate. Font-size-ul de bază vine de pe rădăcina `<svg>`.
"""
stack = [(0.0, 0.0)]
root = re.search(r'<svg\b([^>]*)>', svg)
base_fs = (_font_px(root.group(1)) if root else None) or DEFAULT_FONT
stack = [((0.0, 0.0), base_fs)]
for m in _ELEMENT.finditer(svg):
if m.group(1) is not None: # <g ...>
t = _TRANSFORM.search(m.group(1))
dx, dy = (float(t.group(1)), float(t.group(2))) if t else (0.0, 0.0)
px, py = stack[-1]
stack.append((px + dx, py + dy))
(px, py), fs = stack[-1]
own = _font_px(m.group(1))
stack.append(((px + dx, py + dy), own if own else fs))
elif m.group(0) == "</g>":
if len(stack) > 1:
stack.pop()
else:
tx, ty = stack[-1]
(tx, ty), fs = stack[-1]
if m.group(3) is not None: # <text ...>...</text>
attrs = m.group(2)
x = float(re.search(r'\bx="([-\d.]+)"', attrs).group(1)) if re.search(r'\bx="', attrs) else 0.0
y = float(re.search(r'\by="([-\d.]+)"', attrs).group(1)) if re.search(r'\by="', attrs) else 0.0
yield "text", x + tx, y + ty, attrs, m.group(3)
own = _font_px(attrs)
yield "text", x + tx, y + ty, attrs, m.group(3), own if own else fs
else: # <line .../> sau alt tag
y = re.search(r'\by1="([-\d.]+)"', m.group(4) or "")
if y:
@@ -60,12 +76,12 @@ def texts(svg: str):
for ev in _elements(svg):
if ev[0] != "text":
continue
_, x, y, attrs, raw = ev
_, x, y, attrs, raw, fs = ev
inner = re.sub(r'<[^>]+>', '', raw)
anchor = (re.search(r'text-anchor="(\w+)"', attrs) or [None, "start"])[1]
w = len(inner) * CHAR_W
x0 = x - w if anchor == "end" else (x - w / 2 if anchor == "middle" else x)
yield x0, x0 + w, y, inner.strip()
yield x0, x0 + w, y, inner.strip(), fs
def levels(svg: str):
@@ -98,16 +114,17 @@ def _audit_one(i: int, svg: str) -> list[str]:
problems.append(f"svg {i}: linie goală înăuntru — markdown rupe blocul")
tx = list(texts(svg))
for x0, x1, y, s in tx:
for x0, x1, y, s, _ in tx:
if x1 > vw + 2:
problems.append(f'svg {i}: text depășește marginea dreaptă ({x1:.0f} > {vw:.0f}): "{s[:45]}"')
if y > vh + 2 or y < 0:
problems.append(f'svg {i}: text în afara înălțimii (y={y:.0f}, viewBox {vh:.0f}): "{s[:45]}"')
for a in range(len(tx)):
for b in range(a + 1, len(tx)):
xa0, xa1, ya, sa = tx[a]
xb0, xb1, yb, sb = tx[b]
if abs(ya - yb) < LINE_H and xa0 < xb1 - 4 and xb0 < xa1 - 4:
xa0, xa1, ya, sa, fa = tx[a]
xb0, xb1, yb, sb, fb = tx[b]
line_h = LINE_H_RATIO * max(fa, fb)
if abs(ya - yb) < line_h and xa0 < xb1 - 4 and xb0 < xa1 - 4:
problems.append(f'svg {i}: texte suprapuse — "{sa[:28]}" / "{sb[:28]}"')
# dacă diagrama are exact un triplet SL/intrare/TP, raportul distanțelor
@@ -149,6 +166,23 @@ def selftest():
real = svg('<g transform="translate(0,0)"><text x="10" y="20">unu</text>'
'<text x="14" y="20">doi</text></g>')
assert any("suprapuse" in p for p in _audit_one(0, real)), "suprapunerea reală a fost pierdută"
# pragul de înălțime urmează font-size-ul: 10px la pas de 12px NU se atinge
small = svg('<text x="10" y="100" style="font-size:10px">unu</text>'
'<text x="10" y="112" style="font-size:10px">doi</text>')
assert not any("suprapuse" in p for p in _audit_one(0, small)), "10px la pas de 12px raportat fals"
# același caz cu font-size moștenit de pe <g>
small_inherited = svg('<g style="font-size:10px"><text x="10" y="100">unu</text>'
'<text x="10" y="112">doi</text></g>')
assert not any("suprapuse" in p for p in _audit_one(0, small_inherited)), "font-size moștenit ignorat"
# font-size-ul unui text nu se scurge la frații lui (baza vine de pe <svg>)
leak = svg('<text x="10" y="20" style="font-size:30px">titlu</text>'
'<text x="10" y="120">unu</text>'
'<text x="10" y="134">doi</text>')
assert not any("suprapuse" in p for p in _audit_one(0, leak)), "font-size-ul unui frate s-a scurs"
# două rânduri de 10px care chiar se calcă rămân raportate
real_small = svg('<text x="10" y="100" style="font-size:10px">unu</text>'
'<text x="10" y="108" style="font-size:10px">doi</text>')
assert any("suprapuse" in p for p in _audit_one(0, real_small)), "suprapunerea reală la 10px a fost pierdută"
# triplet SL/intrare/TP cu scară greșită rămâne semnalat
bad = svg('<line x1="10" y1="300" x2="380" y2="300"/><text x="10" y="295">SL 90.00</text>'
'<line x1="10" y1="250" x2="380" y2="250"/><text x="10" y="245">intrare 100.00</text>'