Persist versioned models and validated network metadata in IndexedDB. Show scene download progress and recoverable errors with the existing frosted material. Connect existing skeletons to pending navigation and route boundaries while preserving the map. Previously only dynamic imports showed fallbacks, and rejected model promises prevented retries. Verified: 32 focused unit tests; 4 navigation and scene browser checks; 7 cache browser checks including invalid-metadata recovery; isolated production build and type checks.
61 lines
2.4 KiB
JavaScript
61 lines
2.4 KiB
JavaScript
const boot = document.getElementById('boot');
|
||
const title = document.getElementById('boot-title');
|
||
const detail = document.getElementById('boot-detail');
|
||
const progress = document.getElementById('boot-progress');
|
||
const retry = document.getElementById('boot-retry');
|
||
const formatBytes = bytes => (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||
let entries = new Map();
|
||
let failed = false;
|
||
let lastPaint = 0;
|
||
|
||
export function beginLoading(files, message = '正在载入三维场景') {
|
||
entries = new Map(files.map(file => [file, {stage:'waiting', loaded:0, total:0}]));
|
||
failed = false;
|
||
boot.hidden = false;
|
||
boot.dataset.state = 'loading';
|
||
boot.setAttribute('aria-busy', 'true');
|
||
title.textContent = message;
|
||
detail.textContent = '正在检查本地资源缓存';
|
||
progress.hidden = false;
|
||
progress.removeAttribute('value');
|
||
retry.hidden = true;
|
||
lastPaint = 0;
|
||
}
|
||
|
||
export function reportProgress(update) {
|
||
if (failed || !entries.has(update.file)) return;
|
||
entries.set(update.file, update);
|
||
// Large streamed files can emit hundreds of chunks in a frame.
|
||
if (update.stage === 'download' && performance.now() - lastPaint < 100) return;
|
||
lastPaint = performance.now();
|
||
const values = [...entries.values()];
|
||
const done = values.filter(item => item.stage === 'ready').length;
|
||
const loaded = values.reduce((sum, item) => sum + item.loaded, 0);
|
||
const cached = values.filter(item => item.stage === 'cache').length;
|
||
const parsing = values.some(item => item.stage === 'parse');
|
||
const downloading = values.some(item => item.stage === 'download');
|
||
progress.max = Math.max(1, values.length);
|
||
progress.value = done;
|
||
title.textContent = `正在载入场景资源 ${done} / ${values.length}`;
|
||
detail.textContent = `${parsing ? '正在解析模型' : downloading ? '正在下载资源' : cached ? '正在读取本地缓存' : '正在准备场景'} · 已读取 ${formatBytes(loaded)}`;
|
||
}
|
||
|
||
export function finishLoading() {
|
||
if (failed) return;
|
||
boot.setAttribute('aria-busy', 'false');
|
||
boot.hidden = true;
|
||
}
|
||
|
||
export function failLoading(error) {
|
||
failed = true;
|
||
boot.hidden = false;
|
||
boot.dataset.state = 'error';
|
||
boot.setAttribute('aria-busy', 'false');
|
||
title.textContent = '场景资源加载失败';
|
||
detail.textContent = '部分资源未能载入,请检查网络后重试。' + (error?.message ? '(' + error.message + ')' : '');
|
||
progress.hidden = true;
|
||
retry.hidden = false;
|
||
}
|
||
|
||
retry.addEventListener('click', () => window.location.reload());
|