begin file loading implementation

This commit is contained in:
CodedSakura 2026-07-09 23:24:12 +03:00
parent ba3adc63c4
commit 585335c21c
11 changed files with 215 additions and 25 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"addons":[],"configuration":{"nuclearcraft:overhaul_sfr":{"blocks":[{"name":"nuclearcraft:solid_fission_controller","type":"legacy_block"},{"name":"nuclearcraft:fission_vent","type":"legacy_block","blockstate":{"active":false}},{"name":"nuclearcraft:fission_vent","type":"legacy_block","blockstate":{"active":true}},{"name":"nuclearcraft:fission_casing","type":"legacy_block"},{"name":"nuclearcraft:fission_glass","type":"legacy_block"},{"metadata":2,"name":"nuclearcraft:fission_source","type":"legacy_block","blockstate":{"type":"californium"}},{"metadata":1,"name":"nuclearcraft:solid_fission_sink","type":"legacy_block","blockstate":{"type":"iron"}},{"metadata":7,"name":"nuclearcraft:solid_fission_sink","type":"legacy_block","blockstate":{"type":"lapis"}},{"metadata":8,"name":"nuclearcraft:solid_fission_sink","type":"legacy_block","blockstate":{"type":"gold"}},{"metadata":12,"name":"nuclearcraft:solid_fission_sink","type":"legacy_block","blockstate":{"type":"purpur"}},{"name":"nuclearcraft:solid_fission_cell","type":"legacy_block","modules":{"ncpf:block_recipes":{"recipes":[{"oredict":"ingotHEU233ZA","type":"oredict"}]}}},{"name":"nuclearcraft:fission_cell_port","type":"legacy_block","blockstate":{"active":false},"modules":{"ncpf:block_recipes":{"recipes":[{"oredict":"ingotHEU233ZA","type":"oredict"}]}}},{"name":"nuclearcraft:fission_cell_port","type":"legacy_block","blockstate":{"active":true},"modules":{"ncpf:block_recipes":{"recipes":[{"oredict":"ingotHEU233ZA","type":"oredict"}]}}},{"name":"nuclearcraft:heavy_water_moderator","type":"legacy_block"},{"metadata":0,"name":"nuclearcraft:fission_reflector","type":"legacy_block","blockstate":{"type":"beryllium_carbon"}}],"coolant_recipes":[{"elements":[{"name":"water","type":"legacy_fluid"},{"name":"condensate_water","type":"legacy_fluid"}],"type":"list"}]}},"designs":[{"design":[[[3,3,3,3,3],[3,0,1,2,3],[3,4,4,11,3],[3,3,3,3,3]],[[3,12,4,4,3],[5,10,13,14,4],[4,7,6,9,4],[3,4,4,4,3]],[[3,4,4,4,3],[4,7,6,9,4],[4,-1,8,-1,4],[3,4,4,4,3]],[[3,3,3,3,3],[3,4,4,4,3],[3,4,4,4,3],[3,3,3,3,3]]],"block_recipes":[[[0]],[[0],[0]]],"type":"nuclearcraft:overhaul_sfr","coolant_recipe":0,"dimensions":[4,4,5]}],"version":1}

View File

@ -54,7 +54,7 @@
<input type="file" data-input="configuration" accept=".ncpf.json" /> <input type="file" data-input="configuration" accept=".ncpf.json" />
</div> </div>
<div data-state="planning"> <div data-state="planning">
<div class="list" id="planner-list"> <div class="list" id="planner">
<template> <template>
<div class="grid m2"> <div class="grid m2">
<template> <template>
@ -71,5 +71,20 @@
<footer id="bottom-menu"> <footer id="bottom-menu">
<span data-resize="bottom-menu"></span> <span data-resize="bottom-menu"></span>
</footer> </footer>
<div class="dialog hidden" data-dialog="fileLoad:chooseDesign">
<section>
<header>Choose a design to open</header>
<main class="list-choice">
<template>
<button>
<span data-text="Name" class="bold"></span> by <span data-text="Author"></span>
</button>
</template>
</main>
<footer>
<button data-dialog-action="cancel">Cancel</button>
</footer>
</section>
</div>
</body> </body>
</html> </html>

40
scripts/dialogs.js Normal file
View File

@ -0,0 +1,40 @@
import { DomList } from "./dom-list.js";
import { addEventToElement, setText } from "./dom-utils.js";
import { emitEvent, listenTo } from "./events.js";
export function initializeDialogs() {
listenTo("fileLoad:chooseDesign", designChoiceDialog);
}
function designChoiceDialog(e) {
const dialog = document.querySelector('[data-dialog="fileLoad:chooseDesign"]');
const dismiss = (data) => {
dialog.classList.add("hidden");
emitEvent("fileLoad:chooseDesign:choice", data);
}
addEventToElement(
"dialog-action", "cancel", "click",
() => dismiss(-1),
{ element: dialog },
);
dialog.addEventListener("click", e => {
if (e.target !== e.currentTarget) return;
dismiss(-1);
});
const list = new DomList(dialog.querySelector(":scope > section > main"));
list.clear();
for (const designIndex in e.detail.designs) {
const design = e.detail.designs[designIndex];
const button = list.makeItem();
setText("Name", design.modules["plannerator:metadata"].Name, { element: button });
setText("Author", design.modules["plannerator:metadata"].Author, { element: button });
button.addEventListener("click", () => dismiss(designIndex));
list.addItem(button);
}
dialog.classList.remove("hidden");
}

View File

@ -1,16 +1,21 @@
export function getElements(dataType, dataValue, props = {}) { export function getElements(dataType, dataValue, {
additionalDataFilters = [],
additionalPropertyFilters = [],
element = document,
} = {}) {
// props: { additionalDataFilters: [key, value][], additionalPropertyFilters: [key, value][] } // props: { additionalDataFilters: [key, value][], additionalPropertyFilters: [key, value][] }
const queryString = [ const queryString = [
...[[dataType, dataValue], ...(props.additionalDataFilters ?? [])] ...[[dataType, dataValue], ...additionalDataFilters]
.map(([k, v]) => [`data-${k}`, v]), .map(([k, v]) => [`data-${k}`, v]),
...(props.additionalPropertyFilters ?? []), ...additionalPropertyFilters,
].map(([k, v]) => v === '*' ? `[${k}]` : `[${k}='${v}']`).join(""); ].map(([k, v]) => v === '*' ? `[${k}]` : `[${k}='${v}']`).join("");
return document.querySelectorAll(queryString); return element.querySelectorAll(queryString);
} }
export function addEventToElement(dataType, dataValue, event, callback, props = {}) { export function addEventToElement(dataType, dataValue, event, callback, props = {}) {
getElements(dataType, dataValue, props).forEach(e => e.addEventListener(event, callback)); getElements(dataType, dataValue, props)
.forEach(e => e.addEventListener(event, callback));
} }
export function addEventToInput(dataValue, event, callback, props = {}) { export function addEventToInput(dataValue, event, callback, props = {}) {
@ -20,6 +25,11 @@ export function addEventToButton(dataValue, event, callback, props = {}) {
addEventToElement("button", dataValue, event, callback, props); addEventToElement("button", dataValue, event, callback, props);
} }
export function setText(dataValue, text, props = {}) {
getElements("text", dataValue, props)
.forEach(e => e.innerText = text);
}
export function debounce(fn, milliseconds = 200) { export function debounce(fn, milliseconds = 200) {
let timeout = null; let timeout = null;
return (e) => { return (e) => {

44
scripts/files.js Normal file
View File

@ -0,0 +1,44 @@
import { awaitEvent, emitEvent } from "./events.js";
import { clearPlanner } from "./planner.js";
export function showOpenFileDialog() {
const input = document.createElement("input");
input.type = "file";
input.accept = ".ncpf.json";
input.addEventListener("change", async () => {
const data = await new Promise(resolve => {
const reader = new FileReader();
reader.addEventListener("load", () => {
resolve(JSON.parse(reader.result));
});
reader.readAsText(input.files[0]);
});
void loadFile(data);
});
input.click();
}
export async function loadExampleFile(filename) {
const data = await fetch(`./examples/${filename}.ncpf.json`)
.then(data => data.json());
return loadFile(data);
}
export async function loadFile(data) {
if (!data.designs) throw new Error("invalid file");
if (data.designs.length === 0) throw new Error("no designs");
let design = data.designs[0];
if (data.designs.length > 1) {
const prepChoice = awaitEvent("fileLoad:chooseDesign:choice");
emitEvent("fileLoad:chooseDesign", data);
const res = await prepChoice;
design = data.designs[res.detail];
}
if (!design) throw new Error("no design");
clearPlanner(design.dimensions, design.type);
console.log(data);
console.log(design);
}

View File

@ -1,6 +1,8 @@
import { loadConfiguration } from "./configuration.js"; import { loadConfiguration } from "./configuration.js";
import { initializeDialogs } from "./dialogs.js";
import { addEventToButton, addEventToInput, debounce, getElements } from "./dom-utils.js"; import { addEventToButton, addEventToInput, debounce, getElements } from "./dom-utils.js";
import { emitEvent, listenTo } from "./events.js"; import { awaitEvent, emitEvent, listenTo } from "./events.js";
import { loadExampleFile, showOpenFileDialog } from "./files.js";
import { initializePlanner } from "./planner.js"; import { initializePlanner } from "./planner.js";
import { initializeMenuResizing } from "./resize-menus.js"; import { initializeMenuResizing } from "./resize-menus.js";
import { getState, setState, STATE } from "./state.js"; import { getState, setState, STATE } from "./state.js";
@ -22,6 +24,7 @@ window.addEventListener("load", () => {
addEventToInput("block-filter", "input", debounce(e => { addEventToInput("block-filter", "input", debounce(e => {
emitEvent("block-picker:filter", e.target.value); emitEvent("block-picker:filter", e.target.value);
})); }));
addEventToButton("open-file", "click", showOpenFileDialog);
listenTo("state", () => { listenTo("state", () => {
console.log("State changed", getState()); console.log("State changed", getState());
@ -39,8 +42,12 @@ window.addEventListener("load", () => {
initializeMenuResizing(); initializeMenuResizing();
initializeTooltips(); initializeTooltips();
initializeMenuBar(); initializeMenuBar();
initializeDialogs();
emitEvent("setup-end"); emitEvent("setup-end");
void loadConfiguration("addons"); void loadConfiguration("catastrophe");
awaitEvent("config:end").then(() => loadExampleFile("tiny reactor"));
// awaitEvent("config:end").then(() => loadExampleFile("multi-reactor"));
}); });

View File

@ -6,13 +6,14 @@ import { createTooltip } from "./tooltip.js";
import { lookupFromMetadata, resolveBlockName } from "./ncpf-utils.js"; import { lookupFromMetadata, resolveBlockName } from "./ncpf-utils.js";
let suppressHover = false; let suppressHover = false;
let multiblockSize = [7, 7, 5]; // width (screen x), depth (screen y), height (xy grids) let multiblockSize = [9, 7, 9]; // width (screen x), height (xy grids), depth (screen y)
let multiblockType = "nuclearcraft:overhaul_sfr";
let activeItem = -1; let activeItem = -1;
function cellHoverEvent(e) { function cellHoverEvent(e) {
if (suppressHover) return; if (suppressHover) return;
const cells = document.querySelectorAll("#planner-list [data-x]"); const cells = document.querySelectorAll("#planner [data-x]");
cells.forEach(c => { cells.forEach(c => {
delete c.dataset.highlight; delete c.dataset.highlight;
delete c.dataset.highlightDistance; delete c.dataset.highlightDistance;
@ -58,7 +59,7 @@ function cellClickEvent(e) {
window.removeEventListener("mouseup", cellClickEvent); window.removeEventListener("mouseup", cellClickEvent);
emitEvent("planner:drag:end"); emitEvent("planner:drag:end");
const cells = document.querySelectorAll("#planner-list [data-x]"); const cells = document.querySelectorAll("#planner [data-x]");
cells.forEach(c => { cells.forEach(c => {
if (c.dataset.preview) { if (c.dataset.preview) {
c.dataset.tile = c.dataset.preview; c.dataset.tile = c.dataset.preview;
@ -118,7 +119,7 @@ function buildBlockPicker() {
grid.clear(); grid.clear();
const blocks = atlases.flatMap(a => a.metadata) const blocks = atlases.flatMap(a => a.metadata)
.filter(m => m.configuration === "nuclearcraft:overhaul_sfr" && m.configurationList === "blocks"); .filter(m => m.configuration === multiblockType && m.configurationList === "blocks");
console.log(blocks); console.log(blocks);
blocks.forEach(m => { blocks.forEach(m => {
const item = grid.makeItem(); const item = grid.makeItem();
@ -141,21 +142,32 @@ function buildPlanner() {
} }
}); });
const list = new DomList(document.querySelector("#planner-list")); const planner = document.querySelector("#planner");
planner.dataset.casing = "hide";
const list = new DomList(planner);
list.clear(); list.clear();
for (let z = 0; z < multiblockSize[2]; z++) { for (let y = 0; y < multiblockSize[1]; y++) {
const item = list.makeItem(); const item = list.makeItem();
item.style.setProperty("--width", multiblockSize[1]); item.style.setProperty("--width", multiblockSize[0]);
if (y === 0 || y === multiblockSize[1] - 1) {
item.dataset.casing = "";
}
const grid = new DomList(item); const grid = new DomList(item);
grid.clear(); grid.clear();
for (let y = 0; y < multiblockSize[1]; y++) { for (let z = 0; z < multiblockSize[2]; z++) {
for (let x = 0; x < multiblockSize[0]; x++) { for (let x = 0; x < multiblockSize[0]; x++) {
const cell = grid.makeItem(); const cell = grid.makeItem();
cell.dataset.x = x.toString(10); cell.dataset.x = x.toString(10);
cell.dataset.y = y.toString(10); cell.dataset.y = y.toString(10);
cell.dataset.z = z.toString(10); cell.dataset.z = z.toString(10);
cell.dataset.tile = (-1).toString(10); cell.dataset.tile = (-1).toString(10);
if (x === 0 || x === multiblockSize[0] - 1
|| y === 0 || y === multiblockSize[1] - 1
|| z === 0 || z === multiblockSize[2] - 1) {
cell.dataset.casing = "";
}
cell.appendChild(makeAtlasImage(-1)); cell.appendChild(makeAtlasImage(-1));
cell.addEventListener("mouseenter", cellHoverEvent); cell.addEventListener("mouseenter", cellHoverEvent);
cell.addEventListener("mouseleave", cellHoverEvent); cell.addEventListener("mouseleave", cellHoverEvent);
@ -170,6 +182,12 @@ function buildPlanner() {
} }
} }
export function clearPlanner(size, type) {
multiblockSize = size;
multiblockType = type;
buildPlanner();
}
function* getBlockTooltip(metadata) { function* getBlockTooltip(metadata) {
const data = lookupFromMetadata(metadata); const data = lookupFromMetadata(metadata);

View File

@ -24,6 +24,10 @@
display: none; display: none;
} }
.bold {
font-weight: bold;
}
.list { .list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -33,7 +37,7 @@
} }
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(var(--width), calc(var(--scale-factor) * var(--default-size))); grid-template-columns: repeat(var(--width-override, var(--width)), calc(var(--scale-factor) * var(--default-size)));
} }
.m1 { .m1 {
@ -194,6 +198,14 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
background-color: var(--highlight-color, #f808); background-color: var(--highlight-color, #f808);
} }
[data-highlight='y'] .atlas-texture:after, [data-highlight='y'] .atlas-air:after { [data-highlight='y'] .atlas-texture:after, [data-highlight='y'] .atlas-air:after {
inset:
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4)
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4)
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4)
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4);
background-color: var(--highlight-color, #f808);
}
[data-highlight='z'] .atlas-texture:after, [data-highlight='z'] .atlas-air:after {
inset: inset:
0 0
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4) calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4)
@ -201,13 +213,11 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4); calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4);
background-color: var(--highlight-color, #f808); background-color: var(--highlight-color, #f808);
} }
[data-highlight='z'] .atlas-texture:after, [data-highlight='z'] .atlas-air:after { [data-casing="hide"] > * {
inset: --width-override: calc(var(--width) - 2);
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4) }
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4) [data-casing="hide"] [data-casing] {
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4) display: none;
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4);
background-color: var(--highlight-color, #f808);
} }
#block-list [data-active] { #block-list [data-active] {
@ -249,7 +259,7 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 100; z-index: 100;
box-shadow: var(--menu-shadow, 0 2px 4px rgba(0, 0, 0, 0.2)); box-shadow: var(--menu-shadow, 0 2px 4px #0002);
} }
.menu-bar button { .menu-bar button {
all: initial; all: initial;
@ -274,3 +284,45 @@ input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
.menu-bar .alt-key { .menu-bar .alt-key {
text-decoration: underline; text-decoration: underline;
} }
.dialog.hidden {
display: none;
}
.dialog {
position: fixed;
inset: 0;
background: var(--dialog-backdrop, #0008);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
padding: var(--dialog-margin, 1em);
}
.dialog > section {
z-index: 11;
padding: var(--dialog-padding, 1em);
background: var(--dialog-background, #fff);
color: var(--dialog-foreground, #000);
border-radius: var(--dialog-border-radius, .25em);
box-shadow: var(--dialog-box-shaodw, 0 4px 8px #0002);
max-width: 100%;
max-height: 100%;
}
.dialog > section > header {
font-size: var(--dialog-header-size, 1.25em);
}
.dialog > section > main {
padding: 1em 0;
}
.dialog > section > footer {
display: flex;
flex-direction: row-reverse;
}
.dialog > section > .list-choice {
display: flex;
flex-direction: column;
gap: .25em;
}
.dialog > section > .list-choice > * {
font-size: 1em;
}