From deea8abdd48f5df74cd7ce5727abe3e9bcd27ec2 Mon Sep 17 00:00:00 2001 From: CodedSakura Date: Thu, 16 Jul 2026 16:55:47 +0300 Subject: [PATCH] rework atlas to load recipes and deduplicate, start on block recipe selector --- scripts/atlas.js | 75 ++++++++++----- scripts/recipe-selector.js | 188 ++++++++++++++++++++++--------------- 2 files changed, 161 insertions(+), 102 deletions(-) diff --git a/scripts/atlas.js b/scripts/atlas.js index 6846b65..0a2963d 100644 --- a/scripts/atlas.js +++ b/scripts/atlas.js @@ -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; diff --git a/scripts/recipe-selector.js b/scripts/recipe-selector.js index eeda082..7d8d4c3 100644 --- a/scripts/recipe-selector.js +++ b/scripts/recipe-selector.js @@ -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, + }; + } }