209 lines
6.3 KiB
JavaScript
209 lines
6.3 KiB
JavaScript
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 Error("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 Error("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;
|
|
}
|