Files
atm-curs-dl/gen/eticheta_libera.py
Claude Agent 29dc93dfad etichete: cele trei care mint, si doua bug-uri de infrastructura
Cele trei, toate verificate pe ecran dupa reparatie: "minimul din care a
pornit miscarea 99.80" intra sub o candela si se citea "99.8" (m2d_directie);
"2 saptamani" era acoperita de caseta de citat si se citea "2 saptaman"
(atmi_orizont); cele doua stopuri de la buy_doji_stop stateau la 12 px cu
liniile incrucisate peste etichete, iar diagrama e tocmai despre diferenta
dintre ele - acum fiecare sta la capatul propriei linii.

Doua bug-uri gasite pe drum, mai grave decat ce reparam:

1. render_html trecea SVG-urile prin markdown. In mp2_pivoti_excel cele doua
   asteriscuri din formulele Excel (=2*E2-C2) deveneau un <em> pus peste
   </text>, iar parserul browserului arunca restul SVG-ului: jumatate de tabel
   lipsea din pagina, fara niciun semn in .md sau in audit. Acum SVG-urile sunt
   scoase din calea lui markdown si puse inapoi dupa, cu assert ca nu ramane
   niciun marcaj.
2. eticheta_libera nu citea <g transform>, deci la diagramele cu panouri
   compara coordonate din sisteme diferite si rata suprapuneri reale. Acum
   aplatizeaza translate/scale pe <g>-uri imbricate. Prima diagrama pe care a
   prins-o dupa reparatie e chiar una deja comisa, din capitolul 10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ThN7ixZrowca8aTGrK1MUJ
2026-10-02 05:59:11 +00:00

221 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""O etichetă de diagramă nu are voie să stea peste desen.
`audit_diagrams.py` prinde doar text peste text. Astea sunt celelalte trei
feluri de suprapunere, găsite vizual pe 02.10.2026 și ușor de regresat la
următoarea mutare de etichetă:
- `<line>` — niveluri, laturi de wedge, bracket-uri, cozi de săgeată;
- `<polyline>` — mediile mobile (punctul orb al primei versiuni);
- `<rect>` — corpurile candelelor și barele de volum.
Verificarea e opt-in, pe etichete numite: pe un grafic de candele aproape
orice text atinge ceva, iar o regulă globală ar bloca tot repo-ul.
eticheta_libera(svg, "wedge_desc", {"baza = 4.00"}, rects=True)
Coordonatele se aplatizează înainte de comparație: la diagramele cu panouri
wrapper-ul e `<g transform="translate(0,h)">`, deci fără asta se comparau
coordonate din sisteme diferite și suprapunerile reale treceau nevăzute.
"""
import re
CW = 6.2 # lățimea unui caracter bold de 11-12px
WICK = "#1a1a1a" # fitilele: desenul le traversează oricum, prin construcție
# --- aplatizarea transformărilor -------------------------------------------
XA = ("x", "x1", "x2", "cx")
YA = ("y", "y1", "y2", "cy")
WA = ("width", "rx")
HA = ("height", "ry")
ID = (0.0, 0.0, 1.0, 1.0)
_ATTR = re.compile(r'([a-zA-Z][\w-]*)="(-?[\d.]+)"')
_TAG = re.compile(r'<(/?)([a-zA-Z]+)((?:"[^"]*"|[^>"])*)>')
def _ctm(attrs, base):
"""Transformarea cumulată: cea de pe `<g>` compusă peste cea moștenită."""
tx, ty, sx, sy = base
t = re.search(r'transform="([^"]*)"', attrs)
for fn, args in re.findall(r"(\w+)\(([^)]*)\)", t.group(1) if t else ""):
v = [float(a) for a in re.split(r"[,\s]+", args.strip()) if a]
if fn == "translate" and v:
tx, ty = tx + sx * v[0], ty + sy * (v[1] if len(v) > 1 else 0.0)
elif fn == "scale" and v:
sx, sy = sx * v[0], sy * (v[1] if len(v) > 1 else v[0])
# rotate/matrix/skew: ignorate, ca înainte — nu apar în repo, iar
# caseta unui text rotit n-ar fi oricum un dreptunghi drept.
return tx, ty, sx, sy
def _rescrie(attrs, ctm, tag):
tx, ty, sx, sy = ctm
def unu(m):
k, v = m.group(1), float(m.group(2))
if k in XA:
v = tx + sx * v
elif k in YA:
v = ty + sy * v
elif k in WA:
v = abs(sx) * v
elif k in HA:
v = abs(sy) * v
else:
return m.group(0)
return f'{k}="{v:.4f}"'
out = _ATTR.sub(unu, attrs)
out = re.sub(r'points="([^"]+)"', lambda m: 'points="%s"' % " ".join(
"{:.4f},{:.4f}".format(tx + sx * float(p.split(",")[0]),
ty + sy * float(p.split(",")[1]))
for p in m.group(1).split()), out)
if tag == "text" and sx != 1.0:
out += f' data-sx="{sx:g}"' # lățimea casetei se scalează cu el
return out
def _aplatizeaza(svg):
"""Rescrie coordonatele în sistemul rădăcinii, ținând cont de `<g transform>`.
`<g>`-urile pot fi imbricate; `<svg>`-urile imbricate rămân sisteme
separate, ca până acum (scanarea se face oricum panou cu panou).
"""
if "transform=" not in svg:
return svg
taieturi, stiva = [], [ID]
for m in _TAG.finditer(svg):
inchis, tag, attrs = m.group(1), m.group(2).lower(), m.group(3)
gol = attrs.rstrip().endswith("/")
if tag == "g":
if inchis:
if len(stiva) > 1:
stiva.pop()
elif not gol:
stiva.append(_ctm(attrs, stiva[-1]))
continue
if inchis or stiva[-1] == ID:
continue
taieturi.append((m.start(3), m.end(3), _rescrie(attrs, stiva[-1], tag)))
for a, b, nou in reversed(taieturi):
svg = svg[:a] + nou + svg[b:]
return svg
def _bbox(m, min_px):
"""Caseta etichetei, lărgită cu marginea cerută."""
tx, ty, attrs, s = float(m.group(1)), float(m.group(2)), m.group(3), m.group(4)
k = float(re.search(r'data-sx="([\d.]+)"', attrs).group(1)) if "data-sx=" in attrs else 1.0
w = CW * len(s) * k
# fara text-anchor, SVG aliniaza la stanga (start), nu centrat. Presupunerea
# ca e centrat dadea alarme false pe etichetele de Level cu label_at="left",
# care se deseneaza fara anchor.
x0 = tx - w if 'anchor="end"' in attrs else tx if 'anchor="middle"' not in attrs else tx - w / 2
return s, (x0 - min_px, x0 + w + min_px, ty - 9 * k - min_px, ty + 3 * k + min_px)
def _segmente(svg):
"""Toate segmentele desenate, ca perechi de capete: linii + polilinii."""
for m in re.finditer(r'<line x1="(-?[\d.]+)" y1="(-?[\d.]+)" x2="(-?[\d.]+)"'
r' y2="(-?[\d.]+)"[^/]*?stroke="(#[0-9a-f]{3,6})"', svg):
if m.group(5) != WICK:
yield ("linia", *map(float, m.groups()[:4]))
for m in re.finditer(r'<polyline points="([^"]+)"', svg):
pts = [tuple(map(float, p.split(","))) for p in m.group(1).split()]
for (ax, ay), (bx, by) in zip(pts, pts[1:]):
yield ("curba", ax, ay, bx, by)
def _atinge(box, ax, ay, bx, by):
x0, x1, top, bot = box
if max(ax, bx) < x0 or min(ax, bx) > x1:
return False
if ax == bx:
ys = (ay, by)
else: # y-ul segmentului pe porțiunea comună
ys = tuple(ay + (by - ay) * (min(max(x, min(ax, bx)), max(ax, bx)) - ax) / (bx - ax)
for x in (x0, x1))
return min(ys) <= bot and max(ys) >= top
def eticheta_libera(svg, name, etichete, min_px=5.0, rects=False, segmente=True):
"""Ridică AssertionError dacă vreuna din `etichete` stă peste desen.
`segmente=False` pentru etichetele de nivel: alea stau lipite de linia lor
prin construcție (y − 6), deci doar candelele sunt o suprapunere reală.
"""
svg = _aplatizeaza(svg)
segs = list(_segmente(svg)) if segmente else []
boxes = []
if rects:
for m in re.finditer(r'<rect x="(-?[\d.]+)" y="(-?[\d.]+)" width="([\d.]+)"'
r' height="([\d.]+)"([^/]*)/>', svg):
x, y, w, h = map(float, m.groups()[:4])
if "fill-opacity" in m.group(5): # zonele colorate stau în fundal
continue
boxes.append((x, y, x + w, y + h))
for m in re.finditer(r'<text x="(-?[\d.]+)" y="(-?[\d.]+)"([^>]*)>([^<]*)</text>', svg):
if m.group(4) not in etichete:
continue
s, box = _bbox(m, min_px)
for kind, ax, ay, bx, by in segs:
if _atinge(box, ax, ay, bx, by):
raise AssertionError(f"{name}: eticheta '{s}' sta pe {kind} "
f"({ax:.0f},{ay:.0f})-({bx:.0f},{by:.0f})")
for rx0, ry0, rx1, ry1 in boxes:
if rx0 < box[1] and rx1 > box[0] and ry0 < box[3] and ry1 > box[2]:
raise AssertionError(f"{name}: eticheta '{s}' sta pe dreptunghiul "
f"({rx0:.0f},{ry0:.0f})-({rx1:.0f},{ry1:.0f})")
def demo():
svg = ('<svg><line x1="0" y1="50" x2="100" y2="50" stroke="#c00"/>'
'<polyline points="0,200 100,120"/>'
'<rect x="0" y="300" width="20" height="40" fill="#188a3e"/>'
'<text x="10" y="55">pe linie</text>'
'<text x="90" y="128">pe curba</text>'
'<text x="10" y="310">pe candela</text>'
'<text x="10" y="400">libera</text></svg>')
for et in ("pe linie", "pe curba", "pe candela"):
try:
eticheta_libera(svg, "demo", {et}, rects=True)
except AssertionError:
pass
else:
raise AssertionError(f"'{et}' trebuia prinsa")
eticheta_libera(svg, "demo", {"libera"}, rects=True)
# --- transformări: un panou mutat cu <g transform="translate(...)"> ------
# 1. etichetă și linie în ACELAȘI <g> (imbricat), suprapuse: trebuie prinsă.
# 2. etichetă și linie în <g>-uri DIFERITE, care s-ar suprapune în
# coordonate brute (ambele pe y≈50) dar nu în cele finale.
mutat = ('<svg><g transform="translate(10,0)"><g transform="translate(0,300)">'
'<line x1="0" y1="50" x2="100" y2="50" stroke="#c00"/>'
'<text x="10" y="55">in panoul mutat</text></g></g>'
'<g transform="translate(0,0)"><text x="10" y="55">sus</text></g>'
'<g transform="translate(0,200)">'
'<line x1="0" y1="50" x2="100" y2="50" stroke="#c00"/></g></svg>')
try:
eticheta_libera(mutat, "demo", {"in panoul mutat"})
except AssertionError:
pass
else:
raise AssertionError("'in panoul mutat' trebuia prinsa (translate ignorat?)")
eticheta_libera(mutat, "demo", {"sus"}) # 250px mai jos decat linia ei
# scale: o candela de 20x40 scalata x2 ajunge sub eticheta de la y=620
scalat = ('<svg><g transform="scale(2)">'
'<rect x="0" y="300" width="20" height="40" fill="#188a3e"/>'
'<text x="5" y="310">pe candela scalata</text></g></svg>')
try:
eticheta_libera(scalat, "demo", {"pe candela scalata"}, rects=True)
except AssertionError:
pass
else:
raise AssertionError("'pe candela scalata' trebuia prinsa")
print("eticheta_libera: ok")
if __name__ == "__main__":
demo()