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
This commit is contained in:
@@ -12,6 +12,10 @@ 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
|
||||
|
||||
@@ -19,15 +23,95 @@ 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)
|
||||
w = CW * len(s)
|
||||
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 - min_px, ty + 3 + min_px)
|
||||
return s, (x0 - min_px, x0 + w + min_px, ty - 9 * k - min_px, ty + 3 * k + min_px)
|
||||
|
||||
|
||||
def _segmente(svg):
|
||||
@@ -60,6 +144,7 @@ def eticheta_libera(svg, name, etichete, min_px=5.0, rects=False, segmente=True)
|
||||
`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:
|
||||
@@ -99,6 +184,35 @@ def demo():
|
||||
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")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user