export class Renderer { constructor() { this.canvas = document.createElement('canvas'); this.canvas.style.display = 'none'; document.body.appendChild(this.canvas); this.gl = this.canvas.getContext('webgl2', {antialias: false, alpha: true}); } } function renderVariantCondition(key) { return key.split(',') .map(it => it.split("=")) .map(([n, v]) => `state[${JSON.stringify(n)}] === ${JSON.stringify(v)}`) .join(" && "); } function renderWhenCondition(when) { if (!when) return "true"; if (when["OR"]) { return when["OR"] .map(it => `(${renderWhenCondition(it)})`) .join(" || "); } if (when["AND"]) { return when["AND"] .map(it => `(${renderWhenCondition(it)})`) .join(" && ") } if (Object.entries(when).length === 0) return 'true'; return Object.entries(when) .map(([key, vals]) => vals.toString().split('|') .map(val => `state[${JSON.stringify(key)}] === ${JSON.stringify(val)}`) .join(' || ')) .map(it => `(${it})`) .join(' && '); } function renderApply(data) { if (Array.isArray(data)) { const parts = []; let total = 0.0; for (const {weight = 1.0} of data) total += weight; let accum = 0.0; for (const elem of data) { const {weight = 1.0} = elem; accum += weight; if (accum === total) { parts.push(JSON.stringify(elem)); } else { parts.push(`h < ${accum} ? ${JSON.stringify(elem)} : `); } } return `(h => ${parts.join('')})(Math.abs(hash) % ${total})`; } else { return JSON.stringify(data); } } /** * @param {Object} data * @returns {String} */ function renderBlockstateResolver(data) { const {variants, multipart} = data; if (variants) { let cases = []; for (const [key, apply] of Object.entries(variants)) { if (!key || key === "normal") continue; cases.push(`if (${(renderVariantCondition(key))}) return [${(renderApply(apply))}];`); } let fallback = variants[""] ?? variants["normal"] ?? {"model": ":invalid"}; cases.push(`return [${renderApply(fallback)}]`) return cases.join('else '); } else if (multipart) { const parts = []; parts.push('const parts = [];') for (const {when, apply} of multipart) { parts.push(`if (${renderWhenCondition(when)}) parts.push(${renderApply(apply)});`); } parts.push("return parts;"); return parts.join(''); } else { console.warn('invalid blockstate', data); return 'return [{"model": ":invalid"}]'; } } export class BlockstateResolver { constructor() { this.resolvers = new Map(); this.resolvers.set(':pending', (state, hash) => [{"model": ":pending"}]); this.resolvers.set(':invalid', (state, hash) => [{"model": ":invalid"}]); this.pending = new Map(); this.onResolve = undefined; } getResolver(id) { id = id.includes(':') ? id : `minecraft:${id}`; if (this.resolvers.has(id)) { return this.resolvers.get(id); } if (!this.pending.has(id)) { const [space, name] = id.split(':'); this.pending.set(id, fetch(`assets/${space}/blockstates/${name}.json`) .then(res => res.json()) .then(data => { const source = renderBlockstateResolver(data); const resolver = new Function(`return function (state, hash) { ${source} }`)(); this.resolvers.set(id, resolver); this.pending.delete(id); if (this.onResolve) { this.onResolve(id, resolver); } }) .catch(reason => { this.resolvers.set(id, this.resolvers.get(':invalid')); this.pending.delete(id); console.log(`failed to resolve blockstate ${id}: ${reason}`) }) ) } return this.resolvers.get(':pending'); } } /** * @param {String} src * @returns {Promise} */ async function parse(src) { const lines = src.replace(/#.*?(?:\n|$)/, '\n').trim().split(/(?:\n[ \t]*)+/); let count = 0; const res = new BlockstateResolver(); function ff() { for (const line of lines) { const args = line.split(/[ \t]+/); const cmd = args[0].toLowerCase(); if (cmd === 'p') { const id = args[1]; const hash = count; count += 1; console.log(id, JSON.stringify(res.getResolver(id)({}, hash))); } } } res.onResolve = (id, fn) => { console.log('resolved', id, 'to', fn) ff() } ff() } export class MCDioramaElement extends HTMLElement { connectedCallback() { const canvas = document.createElement('canvas'); this.appendChild(canvas); const ctx = canvas.getContext('2d'); setTimeout(() => { const script = this.querySelector('script[type="text/mc-world"]'); parse(script.textContent); }, 0) } } customElements.define('mc-diorama', MCDioramaElement);