finalize file saving

This commit is contained in:
CodedSakura 2026-07-13 17:07:10 +03:00
parent 880017ddf3
commit 7948a27f39
10 changed files with 293 additions and 61 deletions

View File

@ -97,5 +97,22 @@
</footer> </footer>
</section> </section>
</div> </div>
<div class="dialog hidden" data-dialog="fileSave:nameDesign">
<section>
<header>Save your design</header>
<main class="list-choice">
<form id="fileSave:nameDesign:form" data-form="fileSave:nameDesign">
<label>
Name:
<input name="name" autofocus />
</label>
</form>
</main>
<footer>
<button data-dialog-action="cancel">Cancel</button>
<button form="fileSave:nameDesign:form">Save</button>
</footer>
</section>
</div>
</body> </body>
</html> </html>

View File

@ -1,30 +1,50 @@
import { DomList } from "./dom-list.js"; import { DomList } from "./dom-list.js";
import { addEventToElement, setText } from "./dom-utils.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"; import { registerLoadCallback } from "./main.js";
registerLoadCallback(() => { registerLoadCallback(() => {
listenTo("fileLoad:chooseDesign", designChoiceDialog); listenTo("fileLoad:chooseDesign", designChoiceDialog);
}) listenTo("fileSave:nameDesign", designNameDialog);
});
function designChoiceDialog(e) { function initializeDialog(dialogName, endEvent, { emptyValue, onDismiss } = {}) {
const dialog = document.querySelector('[data-dialog="fileLoad:chooseDesign"]'); const dialog = document.querySelector(`[data-dialog="${dialogName}"]`);
const dismiss = (data) => { const dismiss = (data) => {
dialog.classList.add("hidden"); dialog.classList.add("hidden");
emitEvent("fileLoad:chooseDesign:choice", data); console.log("dimiss > emit", data);
emitEvent(endEvent, data);
if (onDismiss) {
onDismiss();
}
} }
addEventToElement( addEventToElement(
"dialog-action", "cancel", "click", "dialog-action", "cancel", "click",
() => dismiss(-1), () => dismiss(emptyValue),
{ element: dialog }, { element: dialog },
); );
dialog.addEventListener("click", e => { dialog.addEventListener("click", e => {
if (e.target !== e.currentTarget) return; 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")); const list = new DomList(dialog.querySelector(":scope > section > main"));
list.clear(); list.clear();
@ -36,6 +56,20 @@ function designChoiceDialog(e) {
button.addEventListener("click", () => dismiss(designIndex)); button.addEventListener("click", () => dismiss(designIndex));
list.addItem(button); 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"));
});
} }

View File

@ -1,4 +1,5 @@
import { deleteTooltip } from "./tooltip.js"; import { deleteTooltip } from "./tooltip.js";
import { deleteSolverMetadata } from "./solver.js";
export class DomList { export class DomList {
list; list;
@ -10,7 +11,10 @@ export class DomList {
} }
clear() { 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]")); this.list.replaceChildren(...this.list.querySelectorAll(":scope > template, :scope > [data-keep]"));
} }

View File

@ -1,14 +1,20 @@
import { awaitEvent, emitEvent, listenTo } from "./events.js"; import { awaitEvent, emitEvent, listenTo } from "./events.js";
import { findFullEntry, stripEntryToMinimums } from "./ncpf-utils.js"; import { checkEquivalence, findFullEntry, stripEntryToMinimums } from "./ncpf-utils.js";
import { getPlannerMultiblockSize, getPlannerMultiblockType, loadPlanner, readPlannerTiles } from "./planner.js"; import {
getPlannerDom,
getPlannerMultiblockSize,
getPlannerMultiblockType,
loadPlanner,
readPlannerCellData
} from "./planner.js";
import { getAtlasIndexFromMetadata, getAtlasMetadata } from "./atlas.js"; import { getAtlasIndexFromMetadata, getAtlasMetadata } from "./atlas.js";
import { resetSolver } from "./solver.js"; import { getSolverMetadata, resetSolver } from "./solver.js";
import { registerLoadCallback } from "./main.js"; import { registerLoadCallback } from "./main.js";
registerLoadCallback(() => { registerLoadCallback(() => {
listenTo("file:load", () => showOpenFileDialog()); listenTo("file:load", () => showOpenFileDialog());
listenTo("file:new", () => {}); listenTo("file:new", () => {});
listenTo("file:save", () => saveFile("test")); listenTo("file:save", () => saveFile());
}); });
export function showOpenFileDialog() { export function showOpenFileDialog() {
@ -16,6 +22,8 @@ export function showOpenFileDialog() {
input.type = "file"; input.type = "file";
input.accept = ".ncpf.json"; input.accept = ".ncpf.json";
input.addEventListener("change", async () => { 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 data = await new Promise(resolve => {
const reader = new FileReader(); const reader = new FileReader();
reader.addEventListener("load", () => { reader.addEventListener("load", () => {
@ -23,7 +31,7 @@ export function showOpenFileDialog() {
}); });
reader.readAsText(input.files[0]); reader.readAsText(input.files[0]);
}); });
void loadFile(data); void loadFile(data, filename);
}); });
input.click(); input.click();
} }
@ -32,10 +40,10 @@ export async function loadExampleFile(filename) {
const data = await fetch(`./examples/${filename}.ncpf.json`) const data = await fetch(`./examples/${filename}.ncpf.json`)
.then(data => data.json()); .then(data => data.json());
return loadFile(data); return loadFile(data);
// return loadFile(data, filename);
} }
export async function loadFile(data) { export async function loadFile(data, name) {
console.log(data);
if (!data.designs) throw new Error("invalid file"); if (!data.designs) throw new Error("invalid file");
if (data.designs.length === 0) throw new Error("no designs"); 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]))); const designWithAtlas = design.design.map(a => a.map(b => b.map(c => c === -1 ? -1 : blocks[c])));
loadPlanner(design.dimensions, design.type, designWithAtlas); 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 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( const designBlocksAtlasMap = Object.fromEntries(
[...new Set(designAsAtlas.flat(3))] [...new Set(designAsAtlas.flat(3))]
.filter(v => v !== -1) .filter(v => v !== -1)
@ -75,36 +106,75 @@ export async function saveFile(name) {
const designAsBlockIndices = designAsBlocks const designAsBlockIndices = designAsBlocks
.map(xPlane => xPlane.map(yRow => yRow.map(b => b?.designIndex ?? -1))); .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) const minimizedBlocks = Object.entries(designBlocksAtlasMap)
.sort(([a], [b]) => a - b) .sort(([a], [b]) => a - b)
.map(([, m]) => stripEntryToMinimums(m)); .map(([, m]) => stripEntryToMinimums(m));
const data = { 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;
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: [], addons: [],
configurations: { configuration: {
[multiblockType]: { [multiblockType]: {
blocks: minimizedBlocks, blocks: minimizedBlocks,
coolant_recipes: [], // FIXME coolant_recipes: coolantRecipes,
}, },
}, },
designs: [ designs: [
{ {
block_recipes: [], // FIXME block_recipes: blockRecipes,
coolant_recipe: 0, // FIXME coolant_recipe: coolantRecipeIndex,
design: designAsBlockIndices, design: designAsBlockIndices,
dimensions: getPlannerMultiblockSize(), dimensions: getPlannerMultiblockSize(),
type: multiblockType, type: multiblockType,
@ -112,7 +182,17 @@ export async function saveFile(name) {
], ],
version: 1, version: 1,
}; };
}
console.log(designBlocksAtlasMap); console.log(`save file (${name})`, fileData);
console.log(data);
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();
}
} }

View File

@ -3,6 +3,8 @@ import { addEventToButton, addEventToInput, debounce, getElements } from "./dom-
import { awaitEvent, emitEvent, listenTo } from "./events.js"; import { awaitEvent, emitEvent, listenTo } from "./events.js";
import { loadExampleFile } from "./files.js"; import { loadExampleFile } from "./files.js";
import { getState, setState, STATE } from "./state.js"; import { getState, setState, STATE } from "./state.js";
import "./menu-bar.js";
import "./dialogs.js";
let loadCallbacks = []; let loadCallbacks = [];
export function registerLoadCallback(cb) { export function registerLoadCallback(cb) {
@ -46,6 +48,12 @@ window.addEventListener("load", () => {
emitEvent(elem.dataset.input + ":change", e); 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); addEventToInput("configuration", "input", loadConfiguration);
addEventToButton("load-configuration", "click", 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("tiny reactor"));
// awaitEvent("config:end").then(() => loadExampleFile("multi-reactor")); // awaitEvent("config:end").then(() => loadExampleFile("multi-reactor"));
// awaitEvent("solver:reset:done").then(() => emitEvent("file:save"));
}); });

View File

@ -4,6 +4,7 @@ import { registerLoadCallback } from "./main.js";
let activeMenuBar = null; let activeMenuBar = null;
registerLoadCallback(() => { registerLoadCallback(() => {
console.log("menubar-load");
document.querySelectorAll(".menu-bar").forEach(menuBar => { document.querySelectorAll(".menu-bar").forEach(menuBar => {
menuBar.querySelectorAll("button").forEach(button => { menuBar.querySelectorAll("button").forEach(button => {
button.setAttribute("tabindex", "-1"); button.setAttribute("tabindex", "-1");

View File

@ -9,9 +9,17 @@ export function checkEquivalence(a, b) {
&& a.metadata === b.metadata && a.metadata === b.metadata
&& (a.blockstate === undefined && b.blockstate === undefined && (a.blockstate === undefined && b.blockstate === undefined
|| JSON.stringify(a.blockstate) === JSON.stringify(b.blockstate)); || JSON.stringify(a.blockstate) === JSON.stringify(b.blockstate));
case "legacy_fluid":
return a.name === b.name;
case "oredict": case "oredict":
return a.oredict === b.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: default:
console.warn("unknown type", a.type, a, b); console.warn("unknown type", a.type, a, b);
} }
@ -24,8 +32,17 @@ export function stripEntryToMinimums(entry) {
case "legacy_block": case "legacy_block":
return objectKeepKeys(entry, "type", "name", "metadata", "blockstate"); return objectKeepKeys(entry, "type", "name", "metadata", "blockstate");
case "legacy_fluid":
return objectKeepKeys(entry, "type", "name");
case "oredict": case "oredict":
return objectKeepKeys(entry, "type", "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}`); throw new Error(`unknown entry type ${entry.type}`);
} }

View File

@ -307,7 +307,7 @@ export function loadPlanner(size, type, textures) {
buildPlanner(textures); buildPlanner(textures);
} }
export function readPlannerTiles() { export function readPlannerCellData() {
const planner = document.getElementById("planner"); const planner = document.getElementById("planner");
const tiles = initialize3dArray(...multiblockSize, -1); const tiles = initialize3dArray(...multiblockSize, -1);
const grids = [...planner.children].filter(e => e.dataset.y); 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); const gridTiles = [...grid.children].filter(e => e.dataset.tile);
for (let z = 0; z < multiblockSize[2]; z++) { for (let z = 0; z < multiblockSize[2]; z++) {
for (let x = 0; x < multiblockSize[0]; x++) { 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; 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 getPlannerMultiblockType = () => multiblockType;
export const getPlannerMultiblockSize = () => multiblockSize; export const getPlannerMultiblockSize = () => multiblockSize;

View File

@ -1,11 +1,72 @@
import { configuration } from "./configuration.js"; import { configuration } from "./configuration.js";
import { emitEvent } from "./events.js"; import { emitEvent } from "./events.js";
import { registerLoadCallback } from "./main.js"; import { registerLoadCallback } from "./main.js";
import { getPlannerCellDom, getPlannerDom } from "./planner.js";
import { findFullEntry } from "./ncpf-utils.js";
registerLoadCallback(() => { registerLoadCallback(() => {
}); });
export function resetSolver(design = undefined) { const solverMetadata = {};
console.log("solver", configuration, design); 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"); emitEvent("solver:reset:done");
} }

View File

@ -352,7 +352,9 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
} }
.dialog > section > footer { .dialog > section > footer {
display: flex; display: flex;
flex-direction: row-reverse; gap: 0.5em;
flex-direction: row;
justify-content: end;
} }
.dialog > section > .list-choice { .dialog > section > .list-choice {
display: flex; display: flex;