refactor: centralize world and imagery requests in Axios services
This commit is contained in:
Generated
+4315
-4
File diff suppressed because it is too large
Load Diff
+20
-1
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user