Commit 1d05e405 authored by vertighel's avatar vertighel
Browse files

fits-viewer.js: sequence-guard sul canvas primario + fix panner

_refreshPrimary() (REST) e il listener WebSocket "fits-preview"
dipingevano entrambi cv-main/cv-pano senza guardia d'ordine: se la
catena REST finiva dopo un push WS piu' recente, sovrascriveva con un
frame vecchio (flicker su scicam1).

Aggiunti _nextPrimarySeq()/_isPrimaryStale(), stesso pattern gia' in
uso per le tile (_tileSeq/_pendingTileSeq): ogni produttore mina un
numero di sequenza e lo ricontrolla subito prima di disegnare, scartando
risposte superate da un aggiornamento piu' recente.

Corretto anche il pannello panoramico: il push WS dipingeva cv-pano e
poi _drawPanoViewport() lo cancellava subito ridisegnando dal vecchio
this._panImage (mai aggiornato dal push) — la scrittura WS non aveva
mai alcun effetto visibile. Ora _panImage viene aggiornato con
l'immagine appena ricevuta prima di richiamare _drawPanoViewport().
parent e9ee88c7
Loading
Loading
Loading
Loading
+89 −9
Original line number Diff line number Diff line
@@ -139,6 +139,14 @@ export class FitsViewer {
            };
        }

        // ── Primary canvas sequence guard ───────────────────────────────────
        // REST refresh (_refreshPrimary) and the WebSocket "fits-preview" push
        // both paint cv-main/cv-pano independently; without this guard a slower
        // REST chain can finish after a newer WS push and overwrite it with a
        // stale frame. See _nextPrimarySeq/_isPrimaryStale.
        this._primarySeq        = 0;
        this._primaryPendingSeq = 0;

        // ── Right-drag range-adjustment state ──────────────────────────────
        this._rdrag = null;  // set to {startX, startY, currentMin, currentMax} while dragging

@@ -388,6 +396,42 @@ export class FitsViewer {
    // Primary camera rendering
    // =========================================================================

    /**
     * Mint a new sequence number for a primary-canvas update and mark it as
     * the one currently in flight.
     *
     * REST refresh (_refreshPrimary/_renderPng/_renderFits/_refreshPanoramic)
     * and the WebSocket "fits-preview" push race to paint cv-main/cv-pano.
     * Every producer calls this once before starting its async work, then
     * checks _isPrimaryStale(seq) right before actually drawing — a producer
     * superseded by a later one discards its result instead of overwriting
     * a newer frame with an older one.
     *
     * Returns
     * -------
     * number
     */
    _nextPrimarySeq() {
        const seq = ++this._primarySeq;
        this._primaryPendingSeq = seq;
        return seq;
    }

    /**
     * True if `seq` was superseded by a later primary-canvas update.
     *
     * Parameters
     * ----------
     * seq : number
     *
     * Returns
     * -------
     * bool
     */
    _isPrimaryStale(seq) {
        return seq !== this._primaryPendingSeq;
    }

    /**
     * Re-fetch /info and repaint the primary camera.
     *
@@ -398,9 +442,12 @@ export class FitsViewer {
    async _refreshPrimary() {
        const state = this._primary;
        const cam   = state.cam;
        const seq   = this._nextPrimarySeq();

        try {
            const info = await this._getJson(`/viewer/${cam}/info`);
            if (this._isPrimaryStale(seq)) return;

            state.width  = info.shape[1];
            state.height = info.shape[0];
            state.dtype  = info.dtype;
@@ -426,12 +473,12 @@ export class FitsViewer {
        }

        if (state.mode === 'fits') {
            await this._renderFits();
            await this._renderFits(seq);
        } else {
            await this._renderPng();
            await this._renderPng(seq);
        }

        await this._refreshPanoramic();
        await this._refreshPanoramic(seq);
        this._redrawOverlay();
    }

@@ -442,11 +489,17 @@ export class FitsViewer {
     * the longest side, which is small enough to transfer quickly over localhost
     * and large enough for the 800-px canvas.
     *
     * Parameters
     * ----------
     * seq : number, optional
     *     Sequence number from _nextPrimarySeq(); mints its own if omitted.
     *     A response that arrives after a newer update was issued is discarded.
     *
     * Returns
     * -------
     * Promise<void>
     */
    async _renderPng() {
    async _renderPng(seq = this._nextPrimarySeq()) {
        const state  = this._primary;
        const canvas = this._el.canvasMain;
        const url    = this._url(
@@ -455,6 +508,8 @@ export class FitsViewer {
        );

        const img = await this._loadImage(url);
        if (this._isPrimaryStale(seq)) return;

        const ctx = canvas.getContext('2d');
        const lb  = state.letterbox;
        ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -467,11 +522,16 @@ export class FitsViewer {
     * Fetches the full image as a single tile, uploads to GPU as an R32F texture,
     * and applies the Viridis colormap entirely on the GPU via a fragment shader.
     *
     * Parameters
     * ----------
     * seq : number, optional
     *     Sequence number from _nextPrimarySeq(); mints its own if omitted.
     *
     * Returns
     * -------
     * Promise<void>
     */
    async _renderFits() {
    async _renderFits(seq = this._nextPrimarySeq()) {
        const state  = this._primary;
        const canvas = this._el.canvasMain;
        const cx     = Math.round(state.width  / 2);
@@ -486,6 +546,8 @@ export class FitsViewer {

        try {
            const { data, w, h } = await this._fetchTileRest(url);
            if (this._isPrimaryStale(seq)) return;

            if (!state.renderer) {
                state.renderer = new GLRenderer(canvas);
                state.renderer.setColormap(
@@ -506,11 +568,16 @@ export class FitsViewer {
     * The panoramic canvas always shows the full image scaled to fit 256 px,
     * with a yellow rectangle indicating the current viewport.
     *
     * Parameters
     * ----------
     * seq : number, optional
     *     Sequence number from _nextPrimarySeq(); mints its own if omitted.
     *
     * Returns
     * -------
     * Promise<void>
     */
    async _refreshPanoramic() {
    async _refreshPanoramic(seq = this._nextPrimarySeq()) {
        const state = this._primary;
        const url   = this._url(
            `/viewer/${state.cam}/panoramic`,
@@ -518,7 +585,10 @@ export class FitsViewer {
        );

        try {
            this._panImage = await this._loadImage(url);
            const img = await this._loadImage(url);
            if (this._isPrimaryStale(seq)) return;

            this._panImage = img;
            this._drawPanoViewport();
        } catch (err) {
            console.warn('Panoramic refresh failed:', err);
@@ -604,9 +674,19 @@ export class FitsViewer {

            if (cam_id === this._primaryCam) {
                if (!this._primary.autoUpdate) return;
                await this._paintBase64(png, this._el.canvasMain);
                const seq = this._nextPrimarySeq();
                const img = await this._loadImage(`data:image/png;base64,${png}`);
                if (this._isPrimaryStale(seq)) return;

                const cvMain = this._el.canvasMain;
                const ctx    = cvMain.getContext('2d');
                const lb     = this._letterboxRect(
                    img.naturalWidth, img.naturalHeight, cvMain.width, cvMain.height);
                ctx.clearRect(0, 0, cvMain.width, cvMain.height);
                ctx.drawImage(img, lb.dx, lb.dy, lb.dw, lb.dh);
                this._redrawOverlay();
                await this._paintBase64(png, this._el.canvasPano);

                this._panImage = img;
                this._drawPanoViewport();
            } else if (this._secondary[cam_id]) {
                if (!this._secondary[cam_id].autoUpdate) return;