feedback3 WD: a model load begun in a hidden tab and still pending 5 s after the tab is shown is started again, not joined to PIXI's pending request (GitHub #68)

This commit is contained in:
sganggs committed 2026-10-04 17:32:54 +08:00
1 parent 41739beaaf
commit 2582367cea
3 files changed
+274 -33

No files matched your search

+27 -5
View File
@@ -8,7 +8,9 @@
// (the plain 源石虫) is drawn tinted toward the slug's own colours (ALIAS_TINT). A model that failed or timed out is
// loaded again after SPINE_RETRY_MS (bounded), and `retryAssets()` re-resolves a view's picture and model when the
// asset manifest arrives after the view was built or the tab is shown again (render/app.js; public issue #8 item 5:
// after a reload whose manifest was slow or failed, every operator stayed the image-less placeholder for good). Every
// after a reload whose manifest was slow or failed, every operator stayed the image-less placeholder for good); a load
// begun while the tab was hidden and still in flight SPINE_STUCK_MS after it is shown again is started again
// (assets.js spine.restart: GitHub #68, such a load may never settle — its view stayed the placeholder). Every
// bar is a tinted Texture.WHITE sprite, so HUDs batch into few draw calls.
//
// Placement: feet anchored at world (x, y, z); scale = camera px-per-tile at the feet × UNIT.modelScale, so
@@ -107,6 +109,13 @@ const PIC_WAIT_MS = 400;
* shown again). Counted from the failure; a hidden tab runs no frames, so nothing is retried while hidden.
*/
export const SPINE_RETRY_MS = Object.freeze([2000, 6000, 15000, 30000]);
/**
* A Spine load begun while the tab was hidden and still in flight this long (ms, real time) after the tab is shown again
* is started again (`retryAssets` arms it, `update` fires it; GitHub #68: such a load — a request the hidden tab left
* hanging — may never settle, and the view stayed the placeholder). A load that finishes within it is never doubled.
* [ASSUMED] the length: a healthy load finishes within a few seconds once the tab is visible.
*/
export const SPINE_STUCK_MS = 5000;
/** Heights above this count as standing on a raised top (bench pads are the lowest raised tiles, 0.16). */
const RAISED_Z = 0.12;
@@ -365,6 +374,8 @@ export class UnitView {
this._spineBusy = false; // a Spine load of this view is in flight
this._spineTries = 0; // failed loads since the last model (SPINE_RETRY_MS)
this._retryAt = 0; // when the next retry is due (ms, performance clock; 0 = none)
this._spineHidden = false; // the load in flight began while the tab was hidden (SPINE_STUCK_MS)
this._stuckAt = 0; // when that load is started again unless it settled (ms; 0 = none)
this.baseTint = 0xffffff; // the drawn model's own tint (ALIAS_TINT), under the status tints
this.hud = new P.Container();
@@ -431,12 +442,16 @@ export class UnitView {
* Re-resolve what this view could not draw yet (public issue #8 item 5): the picture when it has none (no avatar URL —
* the asset manifest arrived after the view was built — or the image failed) and, at once, the Spine model when none
* is shown and none is loading (no manifest entry then, or a load that failed / timed out). render/app.js calls it for
* every view when a manifest arrives (assets.js onChange) and when the tab is shown again. Nothing to do otherwise.
* every view when a manifest arrives (assets.js onChange) and when the tab is shown again. A load still in flight that
* began while the tab was hidden gets SPINE_STUCK_MS from now (the tab visible) to finish, then `update` starts it again
* (GitHub #68). Nothing to do otherwise.
*/
retryAssets() {
if (this.destroyed) return;
if (!this._pic || this._pic.state === 'none') this._loadPicture();
if (!this.actor && !this._spineBusy) { this._retryAt = 0; this._loadSpine(true); }
if (this.actor) return;
if (!this._spineBusy) { this._retryAt = 0; this._loadSpine(true); return; }
if (this._spineHidden && !this._stuckAt && !globalThis.document?.hidden) this._stuckAt = nowMs() + SPINE_STUCK_MS;
}
/**
@@ -451,8 +466,10 @@ export class UnitView {
this.entry = entry;
const req = this._spineReq = (this._spineReq || 0) + 1;
this._spineBusy = true;
this._spineHidden = !!globalThis.document?.hidden;
this._stuckAt = 0;
a.spine.acquire(entry, retry ? { retry: true } : undefined).then((data) => {
if (req === this._spineReq) this._spineBusy = false;
if (req === this._spineReq) { this._spineBusy = false; this._stuckAt = 0; }
if (this.destroyed || req !== this._spineReq) { this._releaseEntry(entry); return; }
this._spineTries = 0;
this._retryAt = 0;
@@ -496,7 +513,7 @@ export class UnitView {
if (deployed != null) { this.actor.deploy(); if (deployed > 0) this.actor.update(deployed); }
}
}, () => {
if (req === this._spineReq) this._spineBusy = false;
if (req === this._spineReq) { this._spineBusy = false; this._stuckAt = 0; }
this._releaseEntry(entry);
if (this.destroyed || req !== this._spineReq) return;
if (entry.fallback) { this._acquireSpine(entry.fallback, id, retry); return; }
@@ -850,6 +867,11 @@ export class UnitView {
if (this._modelDirty) { this._modelDirty = false; this._syncModel(); }
// a failed / timed-out model load is tried again once its wait is over (SPINE_RETRY_MS; frames only: never hidden)
if (this._retryAt && nowMs() >= this._retryAt) { this._retryAt = 0; if (!this.actor && !this._spineBusy) this._loadSpine(true); }
// a load begun in a hidden tab and still in flight SPINE_STUCK_MS after it came back: started again (GitHub #68)
if (this._stuckAt && nowMs() >= this._stuckAt) {
this._stuckAt = 0;
if (!this.actor && this._spineBusy && this.entry) { this._spineHidden = false; this.ctx.assets?.spine?.restart?.(this.entry); }
}
if (this.zTarget != null && this.z !== this.zTarget) {
const d = this.zTarget - this.z;
this.z = Math.abs(d) < 1e-3 ? this.zTarget : this.z + d * Math.min(1, dt * 12);