rework atlas to load recipes and deduplicate, start on block recipe selector

This commit is contained in:
CodedSakura 2026-07-16 16:55:47 +03:00
parent 846ed1266d
commit deea8abdd4
2 changed files with 161 additions and 102 deletions

View File

@ -20,15 +20,19 @@ async function getTextureSize(data) {
});
}
function putTextureOnCanvas(ctx, atlasSize, index, data) {
const atlasIndexX = index % atlasSize;
const atlasIndexY = (index - atlasIndexX) / atlasSize;
const x = atlasIndexX * data.size[0];
const y = atlasIndexY * data.size[1];
ctx.drawImage(data.image, x, y);
function putTextureOnCanvas(ctx, atlasSize, data) {
if (!data.duplicate) {
const atlasIndexX = data.index % atlasSize;
const atlasIndexY = (data.index - 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) {
@ -39,16 +43,34 @@ export async function buildAtlas(data) {
...Object.entries(data.configuration),
...data.addons.flatMap(a => Object.entries(a.configuration)),
]
.map(([t, d]) => [t, d.modules["plannerator:configuration_metadata"], d])
.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(([c, t, a, e]) => Array.isArray(e)
? [[c, t, a, e]]
: t === "modules" && e["plannerator:global_elements"]
? [[c, "plannerator:global_elements", a, e["plannerator:global_elements"].elements]]
: [])
.flatMap(([c, t, a, l]) => l.map((v) => [c, t, a, 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,
@ -71,18 +93,25 @@ export async function buildAtlas(data) {
throw new Error("Configuration processing failed - textures must be square");
}
const dedupTextureMap = new Map();
t.forEach(texture => {
if (dedupTextureMap.has(texture.texture)) {
texture.index = dedupTextureMap.get(texture.texture)
texture.duplicate = true;
} else {
texture.index = dedupTextureMap.size;
dedupTextureMap.set(texture.texture, texture.index);
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, i) => putTextureOnCanvas(ctx, atlasEdgeSizeBlocks, i, b)));
t.forEach((m, i) => {
m.index = offset + i;
delete m.texture;
});
await Promise.all(t.map(b => putTextureOnCanvas(ctx, atlasEdgeSizeBlocks, b)));
const blob = await canvas.convertToBlob({ type: "image/png" });
atlases.push({
@ -154,15 +183,13 @@ export function getAtlasMetadata(index) {
}
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;
return atlasItemMeta.index;
}
index++;
}
}
return -1;

View File

@ -28,6 +28,87 @@ registerLoadCallback(() => {
listenTo([ "planner:selection:removed", "planner:selection:added"], rescanSelectionRecipes);
});
function initializeRecipePicker(params) {
const { parent, title, entryListType, multiblockType, recipes, currentSelection, updateSelection } = params;
parent.querySelectorAll('[data-text="recipe:type"]')
.forEach(e => e.innerText = title);
function updateSelectedRecipe(selectedRecipe) {
if (!selectedRecipe) {
parent.querySelectorAll('[data-text="recipe:selected"]')
.forEach(e => e.innerText = "None");
parent.querySelectorAll('[data-atlas="recipe:selected"]')
.forEach(e => e.replaceChildren(makeAtlasImage(-1)));
return;
}
const selectedRecipeName = selectedRecipe.modules["plannerator:display_name"].display_name;
const selectedAtlasId = getAtlasIndexFromMetadata(selectedRecipe, multiblockType, entryListType);
const metadata = getAtlasMetadata(selectedAtlasId);
parent.querySelectorAll('[data-text="recipe:selected"]')
.forEach(e => e.innerText = selectedRecipeName);
parent.querySelectorAll('[data-atlas="recipe:selected"]')
.forEach(e => e.replaceChildren(makeAtlasImage(selectedAtlasId)));
parent.querySelectorAll('.recipe--selected')
.forEach(e => setTooltip(e, [...getRecipeTooltip(selectedRecipe, metadata)]));
}
updateSelectedRecipe(currentSelection);
function handleDismiss() {
console.log("dismiss");
parent.classList.remove("selecting");
window.removeEventListener("click", handleDismiss);
}
parent.addEventListener("click", e => e.stopPropagation());
const filterElem = parent.querySelector('[data-input="recipe:filter"]');
function updateRecipeList() {
const filter = Filter.fromInput(filterElem);
const recipeList = new DomList(parent.querySelector(":scope > .select"));
recipeList.clear();
console.log(recipes);
for (let i = 0; i < recipes.length; i++) {
const recipe = recipes[i];
const recipeItem = recipeList.makeItem();
const recipeName = recipe.modules["plannerator:display_name"].display_name;
const atlasId = getAtlasIndexFromMetadata(recipe, multiblockType, entryListType);
const metadata = getAtlasMetadata(atlasId);
recipeItem.querySelectorAll('[data-text="recipe:item"]')
.forEach(e => e.innerText = recipeName);
recipeItem.querySelectorAll('[data-atlas="recipe:item"]')
.forEach(e => e.replaceChildren(makeAtlasImage(atlasId)));
recipeItem.addEventListener("click", () => {
handleDismiss();
updateSelectedRecipe(recipe);
updateSelection(recipe);
});
const tooltipData = [...getRecipeTooltip(recipe, metadata)];
setTooltip(recipeItem, tooltipData);
if (!filter.test(recipeName, ...tooltipDataToPlain(tooltipData))) {
recipeItem.classList.add("hidden");
}
recipeList.addItem(recipeItem);
}
}
updateRecipeList();
filterElem.addEventListener("input", debounce(updateRecipeList));
parent.querySelectorAll(':scope > .recipe--selected').forEach(e => {
e.addEventListener("click", () => {
parent.classList.add("selecting");
parent.querySelector('[autofocus]').focus();
window.addEventListener("click", handleDismiss);
});
});
}
function rescanDesignRecipes() {
console.log("rescan recipes");
console.log(configuration);
@ -50,87 +131,26 @@ function rescanDesignRecipes() {
.flatMap(([, v]) => v.coolant_recipes);
const designItem = designRecipes.makeItem();
designItem.querySelectorAll('[data-text="recipe:type"]')
.forEach(e => e.innerText = "Coolant Recipe");
const plannerDom = getPlannerDom();
const plannerMetadata = getSolverMetadata(plannerDom);
let selectedRecipe = plannerMetadata?.coolant_recipe;
let currentSelection = plannerMetadata?.coolant_recipe;
if (!plannerMetadata) {
selectedRecipe = defaultRecipes["nuclearcraft:overhaul_sfr"].coolant_recipe;
currentSelection = defaultRecipes[multiblockType].coolant_recipe;
setSolverMetadata(plannerDom, {
coolant_recipe: selectedRecipe,
coolant_recipe: currentSelection,
});
}
selectedRecipe = findFullEntry("coolant_recipes", selectedRecipe, { configurationHint: multiblockType });
currentSelection = findFullEntry("coolant_recipes", currentSelection, { configurationHint: multiblockType });
function updateSelectedRecipe() {
const selectedRecipeName = selectedRecipe.modules["plannerator:display_name"].display_name;
const selectedAtlasId = getAtlasIndexFromMetadata(selectedRecipe, multiblockType, "coolant_recipes");
const metadata = getAtlasMetadata(selectedAtlasId);
designItem.querySelectorAll('[data-text="recipe:selected"]')
.forEach(e => e.innerText = selectedRecipeName);
designItem.querySelectorAll('[data-atlas="recipe:selected"]')
.forEach(e => e.replaceChildren(makeAtlasImage(selectedAtlasId)));
designItem.querySelectorAll('.recipe--selected')
.forEach(e => setTooltip(e, [...getRecipeTooltip(selectedRecipe, metadata)]));
}
updateSelectedRecipe();
function handleDismiss() {
console.log("dismiss");
designItem.classList.remove("selecting");
window.removeEventListener("click", handleDismiss);
}
designItem.addEventListener("click", e => e.stopPropagation());
const filterElem = designItem.querySelector('[data-input="recipe:filter"]');
function updateRecipeList() {
const filter = Filter.fromInput(filterElem);
const recipeList = new DomList(designItem.querySelector(":scope > .select"));
recipeList.clear();
for (let i = 0; i < coolantRecipes.length; i++) {
const recipe = coolantRecipes[i];
const recipeItem = recipeList.makeItem();
const recipeName = recipe.modules["plannerator:display_name"].display_name;
const atlasId = getAtlasIndexFromMetadata(recipe, multiblockType, "coolant_recipes");
const metadata = getAtlasMetadata(atlasId);
recipeItem.querySelectorAll('[data-text="recipe:item"]')
.forEach(e => e.innerText = recipeName);
recipeItem.querySelectorAll('[data-atlas="recipe:item"]')
.forEach(e => e.replaceChildren(makeAtlasImage(atlasId)));
recipeItem.addEventListener("click", () => {
handleDismiss();
selectedRecipe = recipe;
updateSelectedRecipe();
setSolverMetadata(plannerDom, {
coolant_recipe: selectedRecipe,
});
});
const tooltipData = [...getRecipeTooltip(recipe, metadata)];
setTooltip(recipeItem, tooltipData);
if (!filter.test(recipeName, ...tooltipDataToPlain(tooltipData))) {
recipeItem.classList.add("hidden");
}
recipeList.addItem(recipeItem);
}
}
updateRecipeList();
filterElem.addEventListener("input", debounce(updateRecipeList));
designItem.querySelectorAll(':scope > .recipe--selected').forEach(e => {
e.addEventListener("click", () => {
designItem.classList.add("selecting");
designItem.querySelector('[autofocus]').focus();
window.addEventListener("click", handleDismiss);
});
initializeRecipePicker({
parent: designItem,
title: "Coolant Recipe",
entryListType: "coolant_recipes",
recipes: coolantRecipes,
updateSelection: r => setSolverMetadata(plannerDom, { coolant_recipe: r }),
multiblockType,
currentSelection,
});
designRecipes.addItem(designItem);
@ -160,7 +180,17 @@ function rescanSelectionRecipes() {
return;
}
console.log(tile);
const designItem = designRecipes.makeItem();
initializeRecipePicker({
parent: designItem,
title: "Block Recipe",
entryType: "ncpf:block_recipes",
recipes: tile.modules["ncpf:block_recipes"].recipes,
multiblockType,
});
designRecipes.addItem(designItem);
}
function* getRecipeTooltip(recipe, metadata) {
@ -189,8 +219,10 @@ function* getRecipeTooltip(recipe, metadata) {
console.log("unknown recipe", recipe, metadata);
}
yield {
type: "italics",
text: metadata.configurationAddon.name,
};
if (metadata.configurationAddon) {
yield {
type: "italics",
text: metadata.configurationAddon.name,
};
}
}