<tmf-viewer>: clamp fit*8, viewer-error, etichette autonome, pagina di prova

Refs #122

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 22:05:54 +01:00
parent 20c0a47c00
commit d48a8a1076
3 changed files with 155 additions and 6 deletions
@@ -30,9 +30,9 @@
var f = Math.min(boxW / w, boxH / h);
return isFinite(f) && f > 0 ? f : 0;
},
// zoom ristretto a [fit * 0.5, max(8, fit)] (come #115)
// zoom ristretto a [fit * 0.5, max(8, fit * 8)]: il massimo segue il fit, cosi' un DXF in mm (fit ~10) si ingrandisce ancora
clampZoom: function (z, fit) {
var lo = fit * 0.5, hi = Math.max(8, fit);
var lo = fit * 0.5, hi = Math.max(8, fit * 8);
return Math.min(hi, Math.max(lo, z));
},
// nuovo scroll che tiene fermo il punto (px, py) del viewport passando dallo zoom z0 a z1
@@ -54,8 +54,10 @@
'tmf-viewer .tmfv-stage>svg:not(.tmfv-ov){display:block;width:100%;height:100%}',
'tmf-viewer .tmfv-ov{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;overflow:visible}',
'tmf-viewer .tmfv-labels{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}',
'tmf-viewer .tmfv-label{position:absolute;transform:translate(8px,-50%);white-space:nowrap;font:600 12px/1.3 system-ui,sans-serif;',
'color:#fff;background:rgba(0,0,0,.6);padding:1px 5px;border-radius:3px;pointer-events:none}',
// #122: aspetto completo dell'etichetta; le classi `cls` della pagina (es. bg-red-600 text-white) cambiano solo i colori
'tmf-viewer .tmfv-label{position:absolute;transform:translate(8px,-50%);white-space:nowrap;font:700 12px/1.3 system-ui,sans-serif;',
'padding:2px 6px;border-radius:4px;box-shadow:0 1px 3px rgba(0,0,0,.4);pointer-events:none}',
':where(tmf-viewer .tmfv-label){color:#fff;background:rgba(0,0,0,.6)}',
'tmf-viewer .tmfv-bar{position:absolute;top:6px;right:6px;z-index:3;display:flex;align-items:center;gap:2px;padding:2px;',
'background:rgba(0,0,0,.55);color:#fff;border-radius:6px;font:12px/1.4 system-ui,sans-serif}',
'tmf-viewer .tmfv-bar button{appearance:none;border:0;border-radius:4px;background:transparent;color:#fff;',
@@ -135,6 +137,11 @@
this._bFit.onclick = function () { self.fit(); };
this._bSave.onclick = function () { self._save(); };
img.addEventListener('load', function () { self._onLoad(); });
// #122: errore di caricamento -> evento viewer-error (bubbling) con l'URL che non si e' caricato
img.addEventListener('error', function () {
if (!self._hasSrc()) return; // l'errore di un src appena tolto non conta
self.dispatchEvent(new CustomEvent('viewer-error', { bubbles: true, detail: { src: self.getAttribute('src') } }));
});
// Ctrl+rotella = zoom intorno al puntatore; senza Ctrl la pagina scorre come sempre
vp.addEventListener('wheel', function (ev) { self._wheel(ev); }, { passive: false });
vp.addEventListener('pointerdown', function (ev) { self._panDown(ev); });
@@ -143,7 +150,12 @@
vp.addEventListener('pointercancel', function () { self._panEnd(); });
// resize del viewport: in stato «adatta» rifa Adatta; altrimenti aggiorna solo il fit
if (typeof ResizeObserver !== 'undefined') {
this._ro = new ResizeObserver(function () { self._onResize(); });
// #122: la callback cambia l'altezza del viewport (fit-height): rimando a un task dopo, senza «ResizeObserver loop»
this._ro = new ResizeObserver(function () {
if (self._raf) return;
var run = function () { self._raf = 0; self._onResize(); };
self._raf = setTimeout(run, 0);
});
this._ro.observe(vp);
}
// contenuto aggiunto dopo (x-html, template): lo adotto
@@ -247,6 +259,7 @@
if (this._content) this._content.style.display = this._hasSrc() ? 'none' : '';
if (!w) { this._z = 0; this._fit = 0; this._auto = true; this._apply(); return; }
if (changed || this._auto || !this._z) this.fit(); else this._apply();
if (!this._z) this._apply(true); // riquadro nascosto: Adatta rimandata, ma overlay ed etichette (in %) si disegnano gia'
}
// #122: area utile del viewport: offset meno i bordi (client* escludono le barre che spariranno dopo l'Adatta)