227 lines
8.7 KiB
JavaScript
227 lines
8.7 KiB
JavaScript
// packs_preview.js — opens a model in the vendored Blockbench app inside a
|
|
// full-screen overlay. The geo JSON is loaded via the gated pack_asset
|
|
// endpoint and the texture images are resolved from afw_bone_textures (or the
|
|
// vanilla entity skin fallback) and handed to Blockbench; the bundled
|
|
// GeckoLib + Multi Actor Animator plugins handle applying them.
|
|
(function () {
|
|
let overlay = null;
|
|
let iframe = null;
|
|
let busy = false;
|
|
|
|
const BB_URL = '/static/vendor/blockbench/index.html';
|
|
|
|
function ensureOverlay() {
|
|
if (overlay) return overlay;
|
|
overlay = document.createElement('div');
|
|
overlay.className = 'bb-overlay';
|
|
overlay.innerHTML =
|
|
'<div class="bb-overlay-header">' +
|
|
'<span class="bb-overlay-title" id="bb-overlay-title"></span>' +
|
|
'<button type="button" class="bb-overlay-close" id="bb-overlay-close" aria-label="Close"><i class="fas fa-times"></i></button>' +
|
|
'</div>' +
|
|
'<div class="bb-overlay-status" id="bb-overlay-status"></div>';
|
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
|
overlay.querySelector('#bb-overlay-close').addEventListener('click', close);
|
|
document.body.appendChild(overlay);
|
|
return overlay;
|
|
}
|
|
|
|
function setStatus(text) {
|
|
const el = document.querySelector('#bb-overlay-status');
|
|
if (el) el.textContent = text || '';
|
|
}
|
|
|
|
function close() {
|
|
if (!overlay) return;
|
|
overlay.hidden = true;
|
|
if (iframe) { iframe.remove(); iframe = null; }
|
|
}
|
|
|
|
function assetUrl(baseUrl, member) {
|
|
return baseUrl.replace('ASSET', member);
|
|
}
|
|
|
|
function entityNameFromMember(member) {
|
|
let base = member.slice(member.lastIndexOf('/') + 1);
|
|
if (base.endsWith('.geo.json')) base = base.slice(0, -'.geo.json'.length);
|
|
for (const s of ['.mf', '.fm', '.m', '.f', '.g']) {
|
|
if (base.endsWith(s)) { base = base.slice(0, -s.length); break; }
|
|
}
|
|
return base;
|
|
}
|
|
|
|
function resourceToMember(resource) {
|
|
resource = String(resource);
|
|
if (resource.indexOf(':') !== -1) {
|
|
const p = resource.split(':');
|
|
return 'assets/' + p.shift() + '/' + p.join(':');
|
|
}
|
|
if (resource.startsWith('assets/')) return resource;
|
|
return 'assets/' + resource;
|
|
}
|
|
|
|
function loadImage(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
img.onload = () => resolve(img);
|
|
img.onerror = () => reject(new Error('Could not load ' + url));
|
|
img.src = url;
|
|
});
|
|
}
|
|
|
|
function imageDataUrl(img) {
|
|
const c = document.createElement('canvas');
|
|
c.width = img.naturalWidth;
|
|
c.height = img.naturalHeight;
|
|
c.getContext('2d').drawImage(img, 0, 0);
|
|
return c.toDataURL('image/png');
|
|
}
|
|
|
|
async function fetchGeo(member, baseUrl) {
|
|
const r = await fetch(assetUrl(baseUrl, member), { headers: { 'Accept': 'application/json' } });
|
|
if (!r.ok) throw new Error('Could not load the model file');
|
|
return r.json();
|
|
}
|
|
|
|
async function fetchVanillaIndex(vanillaBaseUrl) {
|
|
try {
|
|
const r = await fetch(assetUrl(vanillaBaseUrl, 'entity_index.json'));
|
|
return r.ok ? await r.json() : {};
|
|
} catch (e) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Resolve the textures to hand to Blockbench: the pack's <entity>_features
|
|
// overlay(s) plus the vanilla entity skin (a random fur/colour variant when
|
|
// the mob has several). Blockbench + the bundled plugins UV-wrap and apply
|
|
// them, so we don't worry about per-bone assignment here.
|
|
async function resolveTextures(geo, entity, opts, index) {
|
|
const textures = [];
|
|
const afw = geo['afw_bone_textures'] || {};
|
|
|
|
// 1. Feature overlays — from afw_bone_textures, or the conventional
|
|
// assets/<ns>/textures/entity/<entity>/<entity>_features.png path.
|
|
const members = [];
|
|
let namespace = 'needsofnature';
|
|
for (const k in afw) {
|
|
const m = resourceToMember(afw[k]);
|
|
if (members.indexOf(m) === -1) members.push(m);
|
|
const colon = String(afw[k]).indexOf(':');
|
|
if (colon > 0) namespace = String(afw[k]).slice(0, colon);
|
|
}
|
|
if (!members.length && entity) {
|
|
members.push('assets/' + namespace + '/textures/entity/' + entity + '/' + entity + '_features.png');
|
|
}
|
|
for (const m of members) {
|
|
try {
|
|
const img = await loadImage(assetUrl(opts.baseUrl, m));
|
|
textures.push({ name: m.split('/').pop(), dataUrl: imageDataUrl(img) });
|
|
} catch (e) {
|
|
console.warn('packs: skip feature texture', m, e);
|
|
}
|
|
}
|
|
|
|
// 2. Vanilla skin — a random variant when several exist, else the
|
|
// server-matched pack skin, else none.
|
|
let rel = null;
|
|
const ent = index && index[entity];
|
|
if (ent) {
|
|
const variants = ent.variants || (typeof ent === 'string' ? [ent] : [ent.default]);
|
|
if (variants && variants.length) rel = variants[Math.floor(Math.random() * variants.length)];
|
|
}
|
|
if (rel) {
|
|
try {
|
|
const img = await loadImage(assetUrl(opts.vanillaBaseUrl, 'entity/' + rel));
|
|
textures.push({ name: rel.split('/').pop(), dataUrl: imageDataUrl(img) });
|
|
} catch (e) {
|
|
console.warn('packs: skip vanilla skin', e);
|
|
}
|
|
} else if (opts.defaultTexture) {
|
|
try {
|
|
const img = await loadImage(assetUrl(opts.baseUrl, opts.defaultTexture));
|
|
textures.push({ name: opts.defaultTexture.split('/').pop(), dataUrl: imageDataUrl(img) });
|
|
} catch (e) {
|
|
console.warn('packs: skip default texture', e);
|
|
}
|
|
}
|
|
return textures;
|
|
}
|
|
|
|
async function open(btn) {
|
|
const member = btn.dataset.member;
|
|
const name = btn.dataset.name || member;
|
|
const opts = { baseUrl: btn.dataset.baseUrl, vanillaBaseUrl: btn.dataset.vanillaBaseUrl, defaultTexture: btn.dataset.defaultTexture || null };
|
|
if (!member || !opts.baseUrl || busy) return;
|
|
busy = true;
|
|
|
|
const overlayEl = ensureOverlay();
|
|
overlayEl.hidden = false;
|
|
document.querySelector('#bb-overlay-title').textContent = name;
|
|
setStatus('Starting Blockbench…');
|
|
|
|
if (iframe) iframe.remove();
|
|
iframe = document.createElement('iframe');
|
|
iframe.className = 'bb-frame';
|
|
iframe.src = BB_URL;
|
|
overlayEl.appendChild(iframe);
|
|
|
|
const ready = await new Promise((resolve) => {
|
|
let done = false;
|
|
const finish = (ok) => {
|
|
if (done) return;
|
|
done = true;
|
|
window.removeEventListener('message', onMsg);
|
|
clearTimeout(timer);
|
|
resolve(ok);
|
|
};
|
|
const timer = setTimeout(() => finish(false), 45000);
|
|
const onMsg = (e) => {
|
|
if (e.origin !== window.location.origin) return;
|
|
if (e.data && e.data.type === 'packs-bb-ready') finish(true);
|
|
};
|
|
window.addEventListener('message', onMsg);
|
|
});
|
|
|
|
if (!ready) {
|
|
setStatus('Blockbench failed to start.');
|
|
busy = false;
|
|
return;
|
|
}
|
|
|
|
setStatus('Loading model…');
|
|
try {
|
|
const [geo, index] = await Promise.all([
|
|
fetchGeo(member, opts.baseUrl),
|
|
fetchVanillaIndex(opts.vanillaBaseUrl),
|
|
]);
|
|
const entity = entityNameFromMember(member);
|
|
const textures = await resolveTextures(geo, entity, opts, index);
|
|
iframe.contentWindow.postMessage({
|
|
type: 'packs-open-model',
|
|
geo: geo,
|
|
name: name,
|
|
textures: textures,
|
|
}, window.location.origin);
|
|
setStatus('Applying textures…');
|
|
} catch (e) {
|
|
setStatus('Could not load model: ' + e.message);
|
|
}
|
|
busy = false;
|
|
}
|
|
|
|
window.addEventListener('message', (e) => {
|
|
if (e.origin !== window.location.origin) return;
|
|
if (!e.data) return;
|
|
if (e.data.type === 'packs-model-open') {
|
|
setStatus('Ready — drag to orbit, scroll to zoom.');
|
|
} else if (e.data.type === 'packs-model-error') {
|
|
setStatus('Model error: ' + (e.data.error || ''));
|
|
} else if (e.data.type === 'packs-plugin-error') {
|
|
setStatus('Plugin error: ' + (e.data.error || ''));
|
|
}
|
|
});
|
|
|
|
window.PacksPreview = { open: open, close: close };
|
|
})();
|