import { emitEvent } from "./events.js"; import { checkEquivalence } from "./ncpf-utils.js"; import { registerLoadCallback } from "./main.js"; const defaultSize = 16; // px export let atlases = []; registerLoadCallback(() => { document.body.style.setProperty("--default-size", `${defaultSize}px`); }); async function getTextureSize(data) { const texture = data.texture; if (!texture) { data.size = [ 0, 0 ]; return; } await new Promise(resolve => { const image = new Image(); image.addEventListener("load", () => { data.size = [ image.width, image.height ]; data.image = image; resolve(); }); image.src = `data:image/png;base64,${texture}`; }); } function putTextureOnCanvas(ctx, atlasSize, data) { if (!data.duplicate) { const atlasIndexX = data.textureIndex % atlasSize; const atlasIndexY = (data.textureIndex - atlasIndexX) / atlasSize; const x = atlasIndexX * data.size[0]; const y = atlasIndexY * data.size[1]; ctx.drawImage(data.image, x, y); } // free up image data delete data.image; delete data.texture; delete data.duplicate; } export async function buildAtlas(data) { // TODO: atlas caching emitEvent("config:process:atlas:start"); const textures = [ ...Object.entries(data.configuration), ...data.addons.flatMap(a => Object.entries(a.configuration)), ] .map(([ config, data ]) => [ config, data.modules["plannerator:configuration_metadata"], data ]) // .map(v => {console.log(v); return v}) .flatMap(([ config, addon, data ]) => Object.entries(data) .map(([ type, entries ]) => [ config, type, addon, entries ])) .flatMap(([ config, type, addon, entry ]) => { if (Array.isArray(entry)) { return [[ config, type, addon, entry ]]; } if (type === "modules" && entry["plannerator:global_elements"]) { return [[ config, "plannerator:global_elements", addon, entry["plannerator:global_elements"].elements, ]]; } return []; }) .flatMap(([ config, type, addon, list ]) => list.flatMap((entry) => { if (entry.modules && entry.modules["ncpf:block_recipes"]) { return [ [ config, type, addon, entry ], ...entry.modules["ncpf:block_recipes"].recipes.map((recipe) => [ config, "ncpf:block_recipes", addon, recipe ]), ]; } return [[ config, type, addon, entry ]]; })) .filter(([,,, b]) => b && b.modules && b.modules["plannerator:texture"]) .map(([c, t, a, b]) => ({ configuration: c, configurationList: t, configurationAddon: a, texture: b.modules["plannerator:texture"].texture, ...(Object.fromEntries(Object.entries(b).filter(([k]) => k !== "modules"))) })); if (textures.length === 0) { throw new UserError("Configuration processing failed - no textured blocks found"); } await Promise.all(textures.map(getTextureSize)); atlases = []; let offset = 0; for (const t of Object.values(Object.groupBy(textures, t => t.size.toString()))) { if (t[0].size[0] !== t[0].size[1]) { throw new UserError("Configuration processing failed - textures must be square"); } const dedupTextureMap = new Map(); t.forEach((texture, i) => { texture.index = i; if (dedupTextureMap.has(texture.texture)) { texture.textureIndex = dedupTextureMap.get(texture.texture) texture.duplicate = true; } else { texture.textureIndex = dedupTextureMap.size; dedupTextureMap.set(texture.texture, texture.textureIndex); texture.duplicate = false; } }); const atlasEdgeSizeBlocks = 2 ** Math.ceil(Math.log2(Math.sqrt(t.length))); const atlasEdgeSize = atlasEdgeSizeBlocks * t[0].size[0]; const canvas = new OffscreenCanvas(atlasEdgeSize, atlasEdgeSize); const ctx = canvas.getContext("2d"); await Promise.all(t.map(b => putTextureOnCanvas(ctx, atlasEdgeSizeBlocks, b))); const blob = await canvas.convertToBlob({ type: "image/png" }); atlases.push({ offset, url: URL.createObjectURL(blob), metadata: t, size: atlasEdgeSizeBlocks, resolution: atlasEdgeSize, length: t.length, }); offset += t.length; } console.log(atlases); emitEvent("config:process:atlas:end"); } export function makeAtlasImage(index) { if (index < -1) { return null; } if (index === -1) { const div = document.createElement("div"); div.classList.add("atlas-air"); return div; } const originalIndex = index; for (const atlas of atlases) { if (index < atlas.length) { const meta = atlas.metadata[index]; const atlasIndexX = meta.textureIndex % atlas.size; const atlasIndexY = (meta.textureIndex - atlasIndexX) / atlas.size; const size = meta.size; const x = atlasIndexX * size[0]; const y = atlasIndexY * size[1]; const div = document.createElement("div"); div.draggable = false; div.dataset.originalIndex = originalIndex; div.classList.add("atlas-texture"); div.style.backgroundImage = `url(${atlas.url})`; div.style.setProperty("--atlas-resolution", `${atlas.resolution}px`); div.style.setProperty("--atlas-scale", `${defaultSize / size[0]}`); div.style.setProperty("--raw-x", `${x}px`); div.style.setProperty("--raw-y", `${y}px`); div.style.setProperty("--size-x", `${size[0]}px`); div.style.setProperty("--size-y", `${size[1]}px`); return div; } index -= atlas.length; } return null; } export function getAtlasMetadata(index) { if (index < -1) return null; if (index === -1) return {}; for (const atlas of atlases) { if (index < atlas.length - 1) { return atlas.metadata[index]; } index -= atlas.length; } return null; } export function getAtlasIndexFromMetadata(metadata, configuration, configurationList = null) { let index = 0; for (const atlas of atlases) { for (const atlasItemMeta of atlas.metadata) { if (checkEquivalence(metadata, atlasItemMeta) && atlasItemMeta.configuration === configuration && (!configurationList || atlasItemMeta.configurationList === configurationList)) { return index; } index++; } } return -1; }