119 lines
3.9 KiB
JavaScript
119 lines
3.9 KiB
JavaScript
// assets.js
|
|
const jsonCache = new Map();
|
|
|
|
/**
|
|
* Resolves a namespace:path ID to a file path.
|
|
* Defaults to the 'minecraft' namespace if none is provided.
|
|
*/
|
|
export function resolveResourceLocation(id, type, ext = 'json') {
|
|
const parts = id.split(':');
|
|
const namespace = parts.length > 1 ? parts[0] : 'minecraft';
|
|
const path = parts.length > 1 ? parts[1] : parts[0];
|
|
return `assets/${namespace}/${type}/${path}.${ext}`;
|
|
}
|
|
|
|
async function fetchJSON(url) {
|
|
if (jsonCache.has(url)) return jsonCache.get(url);
|
|
const res = await fetch(url);
|
|
if (!res.ok) throw new Error(`Failed to load asset: ${url}`);
|
|
const data = await res.json();
|
|
jsonCache.set(url, data);
|
|
return data;
|
|
}
|
|
|
|
export async function loadBlockstate(id) {
|
|
const url = resolveResourceLocation(id, 'blockstates');
|
|
return fetchJSON(url);
|
|
}
|
|
|
|
export async function loadModel(id) {
|
|
const url = resolveResourceLocation(id, 'models');
|
|
const model = await fetchJSON(url);
|
|
|
|
// Recursively resolve and merge parent models
|
|
if (model.parent) {
|
|
// Parents sometimes use block/ or item/ prefixes directly
|
|
const parentId = model.parent.includes(':') ? model.parent : `minecraft:${model.parent}`;
|
|
const parent = await loadModel(parentId);
|
|
|
|
// Merge textures
|
|
model.textures = {...parent.textures, ...model.textures};
|
|
// Inherit elements if child doesn't override them
|
|
if (!model.elements) model.elements = parent.elements;
|
|
}
|
|
return model;
|
|
}
|
|
|
|
export class TextureAtlas {
|
|
constructor(size = 512) {
|
|
this.canvas = document.createElement('canvas');
|
|
this.canvas.width = size;
|
|
this.canvas.height = size;
|
|
this.ctx = this.canvas.getContext('2d', {willReadFrequently: true});
|
|
this.ctx.imageSmoothingEnabled = false;
|
|
|
|
this.map = new Map();
|
|
this.promises = new Map(); // Track loading promises separately!
|
|
|
|
this.cellSize = 16;
|
|
this.padding = 1;
|
|
this.x = 0;
|
|
this.y = 0;
|
|
this.rowHeight = 0;
|
|
}
|
|
|
|
async fill(id, currentX, currentY) {
|
|
const url = resolveResourceLocation(id, 'textures', 'png');
|
|
|
|
try {
|
|
const img = await new Promise((resolve, reject) => {
|
|
const i = new Image();
|
|
i.crossOrigin = 'anonymous';
|
|
i.onload = () => resolve(i);
|
|
i.onerror = () => reject(new Error(url));
|
|
i.src = url;
|
|
});
|
|
|
|
this.ctx.drawImage(img, currentX, currentY, this.cellSize, this.cellSize);
|
|
} catch (e) {
|
|
console.warn(`Texture missing: ${id}`);
|
|
this.ctx.fillStyle = '#ff00ff';
|
|
this.ctx.fillRect(currentX, currentY, this.cellSize, this.cellSize);
|
|
}
|
|
}
|
|
|
|
load(id) {
|
|
// If already loading/loaded, just return the tracking promise
|
|
if (this.promises.has(id)) return this.promises.get(id);
|
|
|
|
if (this.x + this.cellSize > this.canvas.width) {
|
|
this.x = 0;
|
|
this.y += this.rowHeight + this.padding;
|
|
this.rowHeight = 0;
|
|
}
|
|
|
|
const currentX = this.x;
|
|
const currentY = this.y;
|
|
|
|
this.x += this.cellSize + this.padding;
|
|
this.rowHeight = Math.max(this.rowHeight, this.cellSize);
|
|
const epsX = 0.1 / this.canvas.width;
|
|
const epsY = 0.1 / this.canvas.height;
|
|
|
|
// 1. SYNCHRONOUSLY allocate the UV coordinates for buildGeometry
|
|
const uvData = {
|
|
u: currentX / this.canvas.width + epsX,
|
|
v: currentY / this.canvas.height + epsY,
|
|
du: this.cellSize / this.canvas.width - 2 * epsX,
|
|
dv: this.cellSize / this.canvas.height - 2 * epsY,
|
|
};
|
|
this.map.set(id, uvData);
|
|
|
|
// 2. ASYNCHRONOUSLY kick off the image fetch
|
|
const loadTask = this.fill(id, currentX, currentY);
|
|
this.promises.set(id, loadTask);
|
|
|
|
return loadTask;
|
|
}
|
|
}
|