refactor: centralize world and imagery requests in Axios services

This commit is contained in:
2026-10-28 12:14:08 +03:30
parent 08b6cd464b
commit 6bd9b17173
6 changed files with 4394 additions and 15 deletions
+4315 -4
View File
File diff suppressed because it is too large Load Diff
+20 -1
View File
@@ -18,10 +18,29 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@fontsource/vazirmatn": "^5.2.5", "@fontsource/vazirmatn": "^5.2.5",
"@reduxjs/toolkit": "^2.13.0",
"axios": "^1.21.0",
"clsx": "^2.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "^9.3.1",
"three": "^0.180.0" "three": "^0.180.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.5",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.6.2",
"eslint": "^9.39.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.7",
"globals": "^17.13.0",
"jsdom": "^26.1.0",
"postcss": "^8.5.32",
"prettier": "^3.9.10", "prettier": "^3.9.10",
"vite": "^6.4.3" "tailwindcss": "^3.4.19",
"vite": "^6.4.3",
"vitest": "^3.2.7"
} }
} }
+14 -2
View File
@@ -1,3 +1,4 @@
import { loadTileUrl } from './services/worldService.js';
import * as THREE from 'three'; import * as THREE from 'three';
import { toLocal } from './geo.js'; import { toLocal } from './geo.js';
@@ -41,13 +42,23 @@ export function createImagery(
})); }));
let loads = 0; let loads = 0;
function show(level, tile) { async function show(level, tile) {
if (tile.mesh || tile.loading || loads >= 6) return; if (tile.mesh || tile.loading || loads >= 6) return;
tile.loading = true; tile.loading = true;
loads++; loads++;
let tileUrl;
try {
tileUrl = await loadTileUrl(`${baseUrl}${level.z}/${tile.x}/${tile.y}.jpg`);
} catch {
loads--;
tile.loading = false;
tile.failed = true;
return;
}
loader.load( loader.load(
`${baseUrl}${level.z}/${tile.x}/${tile.y}.jpg`, tileUrl,
(texture) => { (texture) => {
URL.revokeObjectURL(tileUrl);
loads--; loads--;
tile.loading = false; tile.loading = false;
texture.colorSpace = THREE.SRGBColorSpace; texture.colorSpace = THREE.SRGBColorSpace;
@@ -78,6 +89,7 @@ export function createImagery(
}, },
undefined, undefined,
() => { () => {
URL.revokeObjectURL(tileUrl);
loads--; loads--;
tile.loading = false; tile.loading = false;
tile.failed = true; tile.failed = true;
+2 -8
View File
@@ -1,3 +1,4 @@
import { loadWorldFiles } from './services/worldService.js';
import * as THREE from 'three'; import * as THREE from 'three';
import '@fontsource/vazirmatn/400.css'; import '@fontsource/vazirmatn/400.css';
import '@fontsource/vazirmatn/600.css'; import '@fontsource/vazirmatn/600.css';
@@ -131,14 +132,7 @@ scene.add(sun, sun.target);
// ---------- World data ---------- // ---------- World data ----------
// Microsoft's building footprints and the satellite tile list are downloaded files, not bundled code. // Microsoft's building footprints and the satellite tile list are downloaded files, not bundled code.
// The game still runs without them (OSM buildings and drawn roads). // The game still runs without them (OSM buildings and drawn roads).
const loadFile = (path, read) => const [buildingsFile, imageryIndex] = await loadWorldFiles();
fetch(path)
.then((r) => (r.ok ? r[read]() : null))
.catch(() => null);
const [buildingsFile, imageryIndex] = await Promise.all([
loadFile('./data/buildings.bin', 'arrayBuffer'),
loadFile('./imagery/index.json', 'json'),
]);
// Footprints reaching into the factory compound are left out: the game's own buildings stand there. // Footprints reaching into the factory compound are left out: the game's own buildings stand there.
const inFactory = (x, z) => { const inFactory = (x, z) => {
const dx = x - FACTORY.center.x, const dx = x - FACTORY.center.x,
+17
View File
@@ -0,0 +1,17 @@
import axios from 'axios';
/** Shared HTTP client; data paths resolve below the deployment directory. */
export const apiClient = axios.create({
baseURL: typeof document === 'undefined' ? 'http://localhost/' : document.baseURI,
timeout: 15000,
});
apiClient.interceptors.response.use(
(response) => response,
(error) =>
Promise.reject(
Object.assign(new Error(error.message || 'بارگذاری داده ناموفق بود'), {
status: error.response?.status ?? null,
code: error.code,
}),
),
);
+26
View File
@@ -0,0 +1,26 @@
import { apiClient } from './apiClient.js';
const WORLD_FILES = { buildings: './data/buildings.bin', imagery: './imagery/index.json' };
/** Loads optional world assets without preventing the OSM fallback on network failure.
* @param {AbortSignal} signal - Cancels startup when the game unmounts.
* @returns {Promise<Array>} Binary buildings and imagery index, or null fallbacks.
*/
export async function loadWorldFiles(signal) {
const requests = [
apiClient.get(WORLD_FILES.buildings, { responseType: 'arraybuffer', signal }),
apiClient.get(WORLD_FILES.imagery, { signal }),
];
const results = await Promise.allSettled(requests);
return results.map((result) => (result.status === 'fulfilled' ? result.value.data : null));
}
/** Loads a tile through the common client; the caller owns the returned object URL.
* @param {string} path - Local imagery path.
* @param {AbortSignal} signal - Request cancellation.
* @returns {Promise<string>} Temporary URL, to revoke after image decoding.
*/
export async function loadTileUrl(path, signal) {
const response = await apiClient.get(path, { responseType: 'blob', signal });
return URL.createObjectURL(response.data);
}