diff --git a/index.html b/index.html index 061914c..60d9a8d 100644 --- a/index.html +++ b/index.html @@ -97,5 +97,22 @@ + diff --git a/scripts/dialogs.js b/scripts/dialogs.js index 4d5717d..0204a16 100644 --- a/scripts/dialogs.js +++ b/scripts/dialogs.js @@ -1,30 +1,50 @@ import { DomList } from "./dom-list.js"; import { addEventToElement, setText } from "./dom-utils.js"; -import { emitEvent, listenTo } from "./events.js"; +import { awaitEvent, emitEvent, listenTo } from "./events.js"; import { registerLoadCallback } from "./main.js"; registerLoadCallback(() => { listenTo("fileLoad:chooseDesign", designChoiceDialog); -}) + listenTo("fileSave:nameDesign", designNameDialog); +}); -function designChoiceDialog(e) { - const dialog = document.querySelector('[data-dialog="fileLoad:chooseDesign"]'); +function initializeDialog(dialogName, endEvent, { emptyValue, onDismiss } = {}) { + const dialog = document.querySelector(`[data-dialog="${dialogName}"]`); const dismiss = (data) => { dialog.classList.add("hidden"); - emitEvent("fileLoad:chooseDesign:choice", data); + console.log("dimiss > emit", data); + emitEvent(endEvent, data); + if (onDismiss) { + onDismiss(); + } } addEventToElement( "dialog-action", "cancel", "click", - () => dismiss(-1), + () => dismiss(emptyValue), { element: dialog }, ); dialog.addEventListener("click", e => { if (e.target !== e.currentTarget) return; - dismiss(-1); + dismiss(emptyValue); }); + queueMicrotask(() => { + dialog.classList.remove("hidden"); + }); + + return { + dialog, + dismiss, + }; +} + +function designChoiceDialog(e) { + const { + dialog, dismiss + } = initializeDialog("fileLoad:chooseDesign", "fileLoad:chooseDesign:choice", -1); + const list = new DomList(dialog.querySelector(":scope > section > main")); list.clear(); @@ -36,6 +56,20 @@ function designChoiceDialog(e) { button.addEventListener("click", () => dismiss(designIndex)); list.addItem(button); } - - dialog.classList.remove("hidden"); +} + +function designNameDialog() { + const { + dismiss + } = initializeDialog( + "fileSave:nameDesign", + "fileSave:nameDesign:named", + { + emptyValue: null + } + ); + + awaitEvent("fileSave:nameDesign:submit").then(e => { + dismiss(new FormData(e.detail.target).get("name")); + }); } diff --git a/scripts/dom-list.js b/scripts/dom-list.js index 72f6b0e..2d52ee4 100644 --- a/scripts/dom-list.js +++ b/scripts/dom-list.js @@ -1,4 +1,5 @@ import { deleteTooltip } from "./tooltip.js"; +import { deleteSolverMetadata } from "./solver.js"; export class DomList { list; @@ -10,7 +11,10 @@ export class DomList { } clear() { - this.list.querySelectorAll("[data-tooltip]").forEach(e => deleteTooltip(Number(e.dataset.tooltip))) + this.list.querySelectorAll("[data-tooltip]") + .forEach(e => deleteTooltip(Number(e.dataset.tooltip))); + this.list.querySelectorAll("[data-solver-metadata]") + .forEach(e => deleteSolverMetadata(Number(e.dataset.solverMetadata))); this.list.replaceChildren(...this.list.querySelectorAll(":scope > template, :scope > [data-keep]")); } diff --git a/scripts/files.js b/scripts/files.js index e6a7dce..21d243c 100644 --- a/scripts/files.js +++ b/scripts/files.js @@ -1,14 +1,20 @@ import { awaitEvent, emitEvent, listenTo } from "./events.js"; -import { findFullEntry, stripEntryToMinimums } from "./ncpf-utils.js"; -import { getPlannerMultiblockSize, getPlannerMultiblockType, loadPlanner, readPlannerTiles } from "./planner.js"; +import { checkEquivalence, findFullEntry, stripEntryToMinimums } from "./ncpf-utils.js"; +import { + getPlannerDom, + getPlannerMultiblockSize, + getPlannerMultiblockType, + loadPlanner, + readPlannerCellData +} from "./planner.js"; import { getAtlasIndexFromMetadata, getAtlasMetadata } from "./atlas.js"; -import { resetSolver } from "./solver.js"; +import { getSolverMetadata, resetSolver } from "./solver.js"; import { registerLoadCallback } from "./main.js"; registerLoadCallback(() => { listenTo("file:load", () => showOpenFileDialog()); listenTo("file:new", () => {}); - listenTo("file:save", () => saveFile("test")); + listenTo("file:save", () => saveFile()); }); export function showOpenFileDialog() { @@ -16,6 +22,8 @@ export function showOpenFileDialog() { input.type = "file"; input.accept = ".ncpf.json"; input.addEventListener("change", async () => { + const basename = input.files[0].name.split(/[/\\]/).pop(); + const filename = basename.replace(/\.json$/, "").replace(/\.ncpf$/, ""); const data = await new Promise(resolve => { const reader = new FileReader(); reader.addEventListener("load", () => { @@ -23,7 +31,7 @@ export function showOpenFileDialog() { }); reader.readAsText(input.files[0]); }); - void loadFile(data); + void loadFile(data, filename); }); input.click(); } @@ -32,10 +40,10 @@ export async function loadExampleFile(filename) { const data = await fetch(`./examples/${filename}.ncpf.json`) .then(data => data.json()); return loadFile(data); + // return loadFile(data, filename); } -export async function loadFile(data) { - console.log(data); +export async function loadFile(data, name) { if (!data.designs) throw new Error("invalid file"); if (data.designs.length === 0) throw new Error("no designs"); @@ -55,13 +63,36 @@ export async function loadFile(data) { const designWithAtlas = design.design.map(a => a.map(b => b.map(c => c === -1 ? -1 : blocks[c]))); loadPlanner(design.dimensions, design.type, designWithAtlas); - resetSolver(design); + resetSolver(design, data.configuration[design.type]); + + if (name) { + getPlannerDom().dataset.name = name; + } + + console.log("load file", data); } -export async function saveFile(name) { +export async function saveFile(name = undefined) { + if (!name) { + name = getPlannerDom().dataset.name; + } + + if (!name) { + emitEvent("fileSave:nameDesign"); + const nameEvent = await awaitEvent("fileSave:nameDesign:named"); + name = nameEvent.detail; + console.log(nameEvent) + } + + if (!name) { + name = "unnamed"; + } + const multiblockType = getPlannerMultiblockType(); - const designAsAtlas = readPlannerTiles(); + const designAsCellData = readPlannerCellData(); + const designAsAtlas = designAsCellData + .map(xPlane => xPlane.map(yRow => yRow.map(c => Number(c.tile)))); const designBlocksAtlasMap = Object.fromEntries( [...new Set(designAsAtlas.flat(3))] .filter(v => v !== -1) @@ -75,44 +106,93 @@ export async function saveFile(name) { const designAsBlockIndices = designAsBlocks .map(xPlane => xPlane.map(yRow => yRow.map(b => b?.designIndex ?? -1))); - const designBlocksWithRecipes = designAsBlocks - .map(xPlane => xPlane - .map(yRow => yRow - .map(block => { - if (!block) return; - return findFullEntry("blocks", block, { configurationHint: multiblockType }); - }) - .filter(b => b && b.modules && "ncpf:block_recipes" in b.modules) - ) - .filter(r => r.length > 0) - ) - .filter(p => p.length > 0); - console.log("designBlocksWithRecipes", designBlocksWithRecipes) - const minimizedBlocks = Object.entries(designBlocksAtlasMap) .sort(([a], [b]) => a - b) .map(([, m]) => stripEntryToMinimums(m)); - const data = { - addons: [], - configurations: { - [multiblockType]: { - blocks: minimizedBlocks, - coolant_recipes: [], // FIXME - }, - }, - designs: [ - { - block_recipes: [], // FIXME - coolant_recipe: 0, // FIXME - design: designAsBlockIndices, - dimensions: getPlannerMultiblockSize(), - type: multiblockType, - }, - ], - version: 1, - }; + const blockRecipes = designAsBlocks + .map((xPlane, x) => xPlane + .map((yRow, y) => yRow + .map((block, z) => { + if (!block) return; + const fullBlock = findFullEntry( + "blocks", + block, + { configurationHint: multiblockType }, + ); + if (!fullBlock || !fullBlock.modules || !("ncpf:block_recipes" in fullBlock.modules)) return; - console.log(designBlocksAtlasMap); - console.log(data); + const designIndex = designBlocksAtlasMap[Number(designAsCellData[x][y][z].tile)].designIndex; + const solverMetadata = getSolverMetadata(designAsCellData[x][y][z].solverMetadata); + const minimizedBlock = minimizedBlocks[designIndex]; + let recipeIndex = -1; + + if (multiblockType === "nuclearcraft:overhaul_sfr") { + if (!minimizedBlock.modules) { + minimizedBlock.modules = {}; + } + if (!minimizedBlock.modules["ncpf:block_recipes"]) { + minimizedBlock.modules["ncpf:block_recipes"] = { + recipes: [], + }; + } + const recipes = minimizedBlock.modules["ncpf:block_recipes"].recipes; + const recipe = solverMetadata["block_recipe"]; + recipeIndex = recipes.findIndex(r => checkEquivalence(r, recipe)); + if (recipeIndex === -1) { + recipes.push(stripEntryToMinimums(recipe)); + recipeIndex = recipes.length - 1; + } + } + + return recipeIndex; + }) + .filter(b => b != null) + ) + .filter(r => r.length > 0) + ) + .filter(p => p.length > 0); + + let fileData = null; + + if (multiblockType === "nuclearcraft:overhaul_sfr") { + const coolantRecipes = []; + let coolantRecipeIndex = -1; + const plannerMetadata = getSolverMetadata(getPlannerDom()); + + coolantRecipes.push(stripEntryToMinimums(plannerMetadata["coolant_recipe"])); + coolantRecipeIndex = 0; + + fileData = { + addons: [], + configuration: { + [multiblockType]: { + blocks: minimizedBlocks, + coolant_recipes: coolantRecipes, + }, + }, + designs: [ + { + block_recipes: blockRecipes, + coolant_recipe: coolantRecipeIndex, + design: designAsBlockIndices, + dimensions: getPlannerMultiblockSize(), + type: multiblockType, + }, + ], + version: 1, + }; + } + + console.log(`save file (${name})`, fileData); + + if (fileData) { + const blob = new Blob([ JSON.stringify(fileData) ], { + type: "application/json", + }); + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = `${name}.ncpf.json`; + a.click(); + } } diff --git a/scripts/main.js b/scripts/main.js index 12e9430..b58fc74 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -3,6 +3,8 @@ import { addEventToButton, addEventToInput, debounce, getElements } from "./dom- import { awaitEvent, emitEvent, listenTo } from "./events.js"; import { loadExampleFile } from "./files.js"; import { getState, setState, STATE } from "./state.js"; +import "./menu-bar.js"; +import "./dialogs.js"; let loadCallbacks = []; export function registerLoadCallback(cb) { @@ -46,6 +48,12 @@ window.addEventListener("load", () => { emitEvent(elem.dataset.input + ":change", e); }); }); + getElements("form", "*").forEach(elem => { + elem.addEventListener("submit", e => { + e.preventDefault(); + emitEvent(elem.dataset.form + ":submit", e); + }); + }); addEventToInput("configuration", "input", loadConfiguration); addEventToButton("load-configuration", "click", loadConfiguration); @@ -70,5 +78,4 @@ window.addEventListener("load", () => { awaitEvent("config:end").then(() => loadExampleFile("tiny reactor")); // awaitEvent("config:end").then(() => loadExampleFile("multi-reactor")); - // awaitEvent("solver:reset:done").then(() => emitEvent("file:save")); }); diff --git a/scripts/menu-bar.js b/scripts/menu-bar.js index fc9f98e..7211ae6 100644 --- a/scripts/menu-bar.js +++ b/scripts/menu-bar.js @@ -4,6 +4,7 @@ import { registerLoadCallback } from "./main.js"; let activeMenuBar = null; registerLoadCallback(() => { + console.log("menubar-load"); document.querySelectorAll(".menu-bar").forEach(menuBar => { menuBar.querySelectorAll("button").forEach(button => { button.setAttribute("tabindex", "-1"); diff --git a/scripts/ncpf-utils.js b/scripts/ncpf-utils.js index 7499b22..23700ec 100644 --- a/scripts/ncpf-utils.js +++ b/scripts/ncpf-utils.js @@ -9,9 +9,17 @@ export function checkEquivalence(a, b) { && a.metadata === b.metadata && (a.blockstate === undefined && b.blockstate === undefined || JSON.stringify(a.blockstate) === JSON.stringify(b.blockstate)); + + case "legacy_fluid": + return a.name === b.name; + case "oredict": return a.oredict === b.oredict; + case "list": + return a.elements.length === b.elements.length && + a.elements.every((aE, i) => checkEquivalence(aE, b.elements[i])); + default: console.warn("unknown type", a.type, a, b); } @@ -24,8 +32,17 @@ export function stripEntryToMinimums(entry) { case "legacy_block": return objectKeepKeys(entry, "type", "name", "metadata", "blockstate"); + case "legacy_fluid": + return objectKeepKeys(entry, "type", "name"); + case "oredict": return objectKeepKeys(entry, "type", "oredict"); + + case "list": + return { + type: entry.type, + elements: entry.elements.map(stripEntryToMinimums), + }; } throw new Error(`unknown entry type ${entry.type}`); } diff --git a/scripts/planner.js b/scripts/planner.js index 5753a8c..f57a667 100644 --- a/scripts/planner.js +++ b/scripts/planner.js @@ -307,7 +307,7 @@ export function loadPlanner(size, type, textures) { buildPlanner(textures); } -export function readPlannerTiles() { +export function readPlannerCellData() { const planner = document.getElementById("planner"); const tiles = initialize3dArray(...multiblockSize, -1); const grids = [...planner.children].filter(e => e.dataset.y); @@ -316,12 +316,21 @@ export function readPlannerTiles() { const gridTiles = [...grid.children].filter(e => e.dataset.tile); for (let z = 0; z < multiblockSize[2]; z++) { for (let x = 0; x < multiblockSize[0]; x++) { - tiles[x][y][z] = Number(gridTiles[x + z * multiblockSize[0]].dataset.tile); + tiles[x][y][z] = { ...gridTiles[x + z * multiblockSize[0]].dataset }; } } } return tiles; } +export function getPlannerDom() { + return document.getElementById("planner"); +} +export function getPlannerCellsDom() { + return document.querySelectorAll("#planner [data-tile]"); +} +export function getPlannerCellDom(x, y, z) { + return document.querySelector(`#planner [data-x="${x}"][data-y="${y}"][data-z="${z}"]`); +} export const getPlannerMultiblockType = () => multiblockType; export const getPlannerMultiblockSize = () => multiblockSize; diff --git a/scripts/solver.js b/scripts/solver.js index 3ae0ab6..e3a9ab8 100644 --- a/scripts/solver.js +++ b/scripts/solver.js @@ -1,11 +1,72 @@ import { configuration } from "./configuration.js"; import { emitEvent } from "./events.js"; import { registerLoadCallback } from "./main.js"; +import { getPlannerCellDom, getPlannerDom } from "./planner.js"; +import { findFullEntry } from "./ncpf-utils.js"; registerLoadCallback(() => { }); -export function resetSolver(design = undefined) { - console.log("solver", configuration, design); +const solverMetadata = {}; +let solverMetadataCounter = 0; +function createSolverMetadata(data) { + const id = solverMetadataCounter; + solverMetadata[id] = data; + solverMetadataCounter++; + return id; +} +export function setSolverMetadata(domNode, data) { + if (domNode.dataset.solverMetadata) { + solverMetadata[Number(domNode.dataset.solverMetadata)] = data; + return; + } + domNode.dataset.solverMetadata = createSolverMetadata(data).toString(10); +} +export function getSolverMetadata(id) { + if (typeof id === "object") { + // assume DOM element + if (id.dataset && id.dataset.solverMetadata) { + return getSolverMetadata(id.dataset.solverMetadata); + } + return null; + } + if (typeof id === "string") { + return getSolverMetadata(Number(id)); + } + return solverMetadata[id]; +} +export function deleteSolverMetadata(id) { + delete solverMetadata[id]; +} + +export function resetSolver(design = undefined, designConfiguration) { + const planner = getPlannerDom(); + if (design && designConfiguration) { + planner.dataset.type = design.type; + if (design.type === "nuclearcraft:overhaul_sfr") { + // design has coolant_recipe and block_recipes + const designCoolantRecipe = designConfiguration.coolant_recipes[design.coolant_recipe]; + const fullCoolantRecipe = findFullEntry( + "coolant_recipes", + designCoolantRecipe, + { configurationHint: design.type }, + ); + setSolverMetadata(planner, { coolant_recipe: fullCoolantRecipe }); + + const designBlockRecipes = design.block_recipes.flat(3); + for (let x = 0; x < design.dimensions[0]; x++) { + for (let y = 0; y < design.dimensions[1]; y++) { + for (let z = 0; z < design.dimensions[2]; z++) { + const block = designConfiguration.blocks[design.design[x][y][z]]; + if (!block || !block.modules || !block.modules["ncpf:block_recipes"]) continue; + const blockRecipeIndex = designBlockRecipes.shift(); + const blockRecipe = block.modules["ncpf:block_recipes"].recipes[blockRecipeIndex]; + setSolverMetadata(getPlannerCellDom(x, y, z), { block_recipe: blockRecipe }); + } + } + } + } + } + emitEvent("solver:reset:done"); } diff --git a/style.css b/style.css index d57ed6b..825839a 100644 --- a/style.css +++ b/style.css @@ -352,7 +352,9 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) { } .dialog > section > footer { display: flex; - flex-direction: row-reverse; + gap: 0.5em; + flex-direction: row; + justify-content: end; } .dialog > section > .list-choice { display: flex;