implement block placing, better layout, resizing, begin tooltips

This commit is contained in:
CodedSakura 2026-07-07 16:55:25 +03:00 committed by CodedSakura
parent 8dcb131ff2
commit 5781faaa47
13 changed files with 456 additions and 101 deletions

20
.editorconfig Normal file
View File

@ -0,0 +1,20 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 120
tab_width = 4
[{*.cjs,*.es6,*.js,*.mjs}]
indent_size = 2
ij_continuation_indent_size = 4
ij_javascript_spaces_within_imports = true
ij_javascript_spaces_within_array_initializer_brackets = true
ij_javascript_spaces_within_object_literal_braces = true
[*.html]
indent_size = 2

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -14,10 +14,11 @@
<button>View</button>
</nav>
<nav id="left-menu" data-state="planning">
<span data-resize="left-menu"></span>
<section id="block-picker">
<label>
Filter:
<input data-input="filter">
<input data-input="block-filter">
</label>
<div class="list" id="block-list">
<template>
@ -29,7 +30,7 @@
<main>
<div data-state="initialization">
Select configuration file:
<button data-button="loadConfiguration" data-configuration="default">Default (2026-07-06)</button>
<button data-button="load-configuration" data-configuration="default">Default (2026-07-06)</button>
<input type="file" data-input="configuration" accept=".ncpf.json" />
</div>
<div data-state="planning">
@ -44,7 +45,11 @@
</div>
</div>
</main>
<nav id="right-menu"></nav>
<nav id="bottom-menu"></nav>
<nav id="right-menu">
<span data-resize="right-menu"></span>
</nav>
<nav id="bottom-menu">
<span data-resize="bottom-menu"></span>
</nav>
</body>
</html>

View File

@ -103,6 +103,7 @@ export function makeAtlasImage(index) {
const y = atlasIndexY * size[1];
const div = document.createElement("div");
div.draggable = false;
div.classList.add("atlas-texture");
div.style.backgroundImage = `url(${atlasBlobURL})`;
div.style.setProperty("--atlas-resolution", `${atlasResolution}px`);

View File

@ -19,3 +19,13 @@ export function addEventToInput(dataValue, event, callback, props = {}) {
export function addEventToButton(dataValue, event, callback, props = {}) {
addEventToElement("button", dataValue, event, callback, props);
}
export function debounce(fn, milliseconds = 200) {
let timeout = null;
return (e) => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => fn(e), milliseconds);
}
}

View File

@ -1,5 +1,9 @@
export function emitEvent(name) {
window.dispatchEvent(new Event(`ncp:${name}`));
export function emitEvent(name, data = null) {
if (data !== null && data !== undefined) {
window.dispatchEvent(new CustomEvent(`ncp:${name}`, { detail: data }));
} else {
window.dispatchEvent(new Event(`ncp:${name}`));
}
}
export function listenTo(name, callback) {

53
scripts/filter.js Normal file
View File

@ -0,0 +1,53 @@
export class Filter {
match = [];
negate = [];
constructor(query) {
query ??= "";
query = query.trim();
// https://stackoverflow.com/a/16261693
for (let match of (query.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [])) {
const negate = match[0] === '-';
if (negate) {
match = match.slice(1);
}
if (match[0] === '"' && match[match.length - 1] === '"') {
match = match.slice(1, match.length - 1);
}
match = match.trim().toLocaleLowerCase();
if (!match) continue;
if (negate) {
this.negate.push(match);
} else {
this.match.push(match);
}
}
}
static fromInput(dataValue) {
const elem = document.querySelector(`[data-input='${dataValue}']`);
return new Filter(elem.value);
}
test(...data) {
for (const datum of data) {
const normalized = datum.trim().toLocaleLowerCase().trim();
for (const n of this.negate) {
if (normalized.includes(n)) {
return false;
}
}
for (const m of this.match) {
if (normalized.includes(m)) {
return true;
}
}
}
return this.match.length === 0;
}
}

View File

@ -1,54 +1,15 @@
import { atlasMetadata, makeAtlasImage } from "./atlas.js";
import { DomList } from "./dom-list.js";
import { addEventToButton, addEventToInput, getElements } from "./dom-utils.js";
import { addEventToButton, addEventToInput, debounce, getElements } from "./dom-utils.js";
import { emitEvent, listenTo } from "./events.js";
import { loadConfiguration } from "./configuration.js";
import { getState, setState, STATE } from "./state.js";
let multiblockSize = [ 7, 7, 5 ]; // width (screen x), depth (screen y), height (xy grids)
let suppressHover = false;
let activeItem = -1;
function cellHoverEvent(e) {
if (suppressHover) return;
const cells = document.querySelectorAll("#planner-list [data-x]");
cells.forEach(c => delete c.dataset.highlight);
if (e.type === "mouseenter") {
cells.forEach(c => {
if (c.dataset.y === e.target.dataset.y && c.dataset.z === e.target.dataset.z) {
c.dataset.highlight = "x";
}
if (c.dataset.x === e.target.dataset.x && c.dataset.z === e.target.dataset.z) {
c.dataset.highlight = "y";
}
if (c.dataset.x === e.target.dataset.x && c.dataset.y === e.target.dataset.y) {
c.dataset.highlight = "z";
}
if (c === e.target) {
delete c.dataset.highlight;
}
});
}
}
function listClickEvent(e) {
const items = document.querySelectorAll("#block-list [data-index]");
items.forEach(i => delete i.dataset.active);
activeItem = Number(e.currentTarget.dataset.index);
e.currentTarget.dataset.active = undefined;
}
import { initializePlanner } from "./planner.js";
import { initializeMenuResizing } from "./resize-menus.js";
window.addEventListener("load", () => {
emitEvent("setup-start");
window.addEventListener("keypress", e => {
console.log(e);
if (e.key === "H") {
suppressHover = !suppressHover;
}
});
getElements("state", "*").forEach(e => {
e.classList.add("hidden");
if (e.dataset.state === getState()) {
@ -57,7 +18,10 @@ window.addEventListener("load", () => {
});
addEventToInput("configuration", "input", loadConfiguration);
addEventToButton("loadConfiguration", "click", loadConfiguration)
addEventToButton("load-configuration", "click", loadConfiguration);
addEventToInput("block-filter", "input", debounce(e => {
emitEvent("block-picker:filter", e.target.value);
}));
listenTo("state", () => {
console.log("State changed", getState());
@ -71,44 +35,8 @@ window.addEventListener("load", () => {
listenTo("config:end", () => setState(STATE.planning));
listenTo("config:end", () => {
console.log(atlasMetadata);
const grid = new DomList(document.querySelector("#block-list"));
grid.clear();
atlasMetadata.forEach((m, i) => {
const item = grid.makeItem();
item.dataset.index = i.toString(10);
item.appendChild(makeAtlasImage(i));
item.addEventListener("click", listClickEvent);
grid.addItem(item);
});
});
listenTo("config:end", () => {
const list = new DomList(document.querySelector("#planner-list"));
list.clear();
for (let z = 0; z < multiblockSize[2]; z++) {
const item = list.makeItem();
item.style.setProperty("--width", multiblockSize[1]);
const grid = new DomList(item);
grid.clear();
for (let y = 0; y < multiblockSize[1]; y++) {
for (let x = 0; x < multiblockSize[0]; x++) {
const cell = grid.makeItem();
cell.dataset.x = x.toString(10);
cell.dataset.y = y.toString(10);
cell.dataset.z = z.toString(10);
cell.appendChild(makeAtlasImage(-1));
cell.addEventListener("mouseenter", cellHoverEvent);
cell.addEventListener("mouseleave", cellHoverEvent);
grid.addItem(cell);
}
}
list.addItem(item);
}
});
initializePlanner();
initializeMenuResizing();
emitEvent("setup-end");

168
scripts/planner.js Normal file
View File

@ -0,0 +1,168 @@
import {atlasMetadata, makeAtlasImage} from "./atlas.js";
import {DomList} from "./dom-list.js";
import { emitEvent, listenTo } from "./events.js";
import { Filter } from "./filter.js";
let suppressHover = false;
let multiblockSize = [7, 7, 5]; // width (screen x), depth (screen y), height (xy grids)
let activeItem = -1;
function cellHoverEvent(e) {
if (suppressHover) return;
const cells = document.querySelectorAll("#planner-list [data-x]");
cells.forEach(c => {
delete c.dataset.highlight;
delete c.dataset.highlightDistance;
});
if (e.type === "mouseenter") {
cells.forEach(c => {
if (c.dataset.y === e.target.dataset.y && c.dataset.z === e.target.dataset.z) {
c.dataset.highlight = "x";
c.dataset.highlightDistance = Math.abs(c.dataset.x - e.target.dataset.x).toString(10);
}
if (c.dataset.x === e.target.dataset.x && c.dataset.z === e.target.dataset.z) {
c.dataset.highlight = "y";
c.dataset.highlightDistance = Math.abs(c.dataset.y - e.target.dataset.y).toString(10);
}
if (c.dataset.x === e.target.dataset.x && c.dataset.y === e.target.dataset.y) {
c.dataset.highlight = "z";
c.dataset.highlightDistance = Math.abs(c.dataset.z - e.target.dataset.z).toString(10);
}
if (c === e.target) {
delete c.dataset.highlight;
}
});
}
}
function cellClickEvent(e) {
if (e.type === "contextmenu") {
e.preventDefault();
return;
}
if (e.buttons !== 1 && e.buttons !== 2 && e.type !== "mouseup") {
return;
}
if (e.buttons === 1 && activeItem === -1) return;
if (e.type === "mousedown") {
window.addEventListener("mouseup", cellClickEvent);
emitEvent("planner:drag:start");
}
if (e.type === "mouseup") {
window.removeEventListener("mouseup", cellClickEvent);
emitEvent("planner:drag:end");
const cells = document.querySelectorAll("#planner-list [data-x]");
cells.forEach(c => {
if (c.dataset.preview) {
c.dataset.tile = c.dataset.preview;
emitEvent("planner:cell:change", c);
delete c.dataset.preview;
}
if (c.dataset.previewRemove) {
delete c.dataset.previewRemove;
c.replaceChildren(makeAtlasImage(-1));
c.dataset.tile = (-1).toString(10);
emitEvent("planner:cell:change", c);
}
});
return;
}
if (e.buttons === 2) {
// remove
if (e.target.tile === "-1") return;
e.target.dataset.previewRemove = "yes";
return;
} else if (e.buttons === 1) {
// add
if (e.target.dataset.preview === activeItem.toString(10)) return;
e.target.dataset.preview = activeItem.toString(10);
e.target.replaceChildren(makeAtlasImage(activeItem));
}
if (e.target.dataset.preview === activeItem.toString(10)) return;
e.target.dataset.preview = activeItem.toString(10);
e.target.replaceChildren(makeAtlasImage(activeItem));
}
function listClickEvent(e) {
const items = document.querySelectorAll("#block-list [data-index]");
items.forEach(i => delete i.dataset.active);
activeItem = Number(e.currentTarget.dataset.index);
e.currentTarget.dataset.active = undefined;
}
export function initializePlanner() {
listenTo("block-picker:filter", e => {
buildBlockPicker();
});
listenTo("config:end", buildBlockPicker);
listenTo("config:end", buildPlanner);
}
function buildBlockPicker() {
const filter = Filter.fromInput("block-filter");
console.log(filter);
const grid = new DomList(document.querySelector("#block-list"));
grid.clear();
atlasMetadata.forEach((m, i) => {
const item = grid.makeItem();
item.dataset.index = i.toString(10);
if (!filter.test(m.modules["plannerator:display_name"].display_name)) {
item.classList.add("hidden");
}
item.appendChild(makeAtlasImage(i));
item.addEventListener("click", listClickEvent);
grid.addItem(item);
});
}
function buildPlanner() {
document.addEventListener("keydown", e => {
console.log(e);
if (e.key === "H") {
suppressHover = !suppressHover;
}
});
const list = new DomList(document.querySelector("#planner-list"));
list.clear();
for (let z = 0; z < multiblockSize[2]; z++) {
const item = list.makeItem();
item.style.setProperty("--width", multiblockSize[1]);
const grid = new DomList(item);
grid.clear();
for (let y = 0; y < multiblockSize[1]; y++) {
for (let x = 0; x < multiblockSize[0]; x++) {
const cell = grid.makeItem();
cell.dataset.x = x.toString(10);
cell.dataset.y = y.toString(10);
cell.dataset.z = z.toString(10);
cell.dataset.tile = (-1).toString(10);
cell.appendChild(makeAtlasImage(-1));
cell.addEventListener("mouseenter", cellHoverEvent);
cell.addEventListener("mouseleave", cellHoverEvent);
cell.addEventListener("mousedown", cellClickEvent);
cell.addEventListener("mouseenter", cellClickEvent);
cell.addEventListener("mouseleave", cellClickEvent);
cell.addEventListener("contextmenu", cellClickEvent);
grid.addItem(cell);
}
}
list.addItem(item);
}
}

43
scripts/resize-menus.js Normal file
View File

@ -0,0 +1,43 @@
import { recall, save } from "./storage.js";
export function initializeMenuResizing() {
const handles = document.querySelectorAll('[data-resize]');
handles.forEach(handle => {
handle.addEventListener("mousedown", startResize);
const stored = recall(`${handle.dataset.resize}-size`);
if (stored) {
document.body.style.setProperty(`--${handle.dataset.resize}-size`, stored);
}
});
}
function startResize(e) {
window.addEventListener("mouseup", endResize);
window.addEventListener("mousemove", resize);
document.body.dataset.resizing = e.target.dataset.resize;
}
function resize(e) {
const resizing = document.body.dataset.resizing;
let size = 0.25;
switch (resizing) {
case "left-menu":
size = e.pageX / window.innerWidth;
break;
case "right-menu":
size = 1 - e.pageX / window.innerWidth;
break;
case "bottom-menu":
size = 1 - e.pageY / window.innerHeight;
break;
}
size = Math.max(0.05, Math.min(size, 0.5));
document.body.style.setProperty(`--${resizing}-size`, `${size*100}%`);
save(`${resizing}-size`, `${size*100}%`);
}
function endResize() {
window.removeEventListener("mouseup", endResize);
window.removeEventListener("mousemove", resize);
delete document.body.dataset.resizing;
}

11
scripts/storage.js Normal file
View File

@ -0,0 +1,11 @@
export function save(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
export function recall(key, defaultValue = null) {
const item = localStorage.getItem(key);
if (item) {
return JSON.parse(item);
}
return defaultValue;
}

0
scripts/tooltip.js Normal file
View File

124
style.css
View File

@ -1,6 +1,22 @@
* {
box-sizing: border-box;
}
:root {
font-family: sans-serif;
max-height: 100svh;
height: 100%;
overflow: hidden;
}
:root {
--scale-factor: 2;
--default-size: 16px;
--dark-bg: #990099;
--menu-border-color: #666;
--resize-handle-color: #666;
--resize-handle-width: 3px;
}
[data-state].hidden {
@ -11,6 +27,9 @@
display: flex;
flex-wrap: wrap;
}
.list .hidden {
display: none;
}
.grid {
display: grid;
grid-template-columns: repeat(var(--width), calc(var(--scale-factor) * var(--default-size)));
@ -25,34 +44,107 @@
body {
display: grid;
grid-template-columns: var(--left-nav, 400px) 1fr var(--right-nav, 300px);
grid-template-rows: 40px 1fr var(--bottom-nav, 200px);
grid-template-columns: var(--left-menu-size, 15%) 1fr var(--right-menu-size, 10%);
grid-template-rows: 28px 1fr var(--bottom-menu-size, 15%);
height: 100%;
margin: 0;
}
#top-menu {
grid-column: 1 / -1;
grid-row: 1;
background-color: var(--dark-bg);
}
#left-menu {
grid-column: 1;
grid-row: 2;
border-right: 1px solid var(--menu-border-color)
}
main {
grid-column: 2;
grid-row: 2;
overflow: auto;
}
#right-menu {
grid-column: 3;
grid-row: 2;
border-left: 1px solid var(--menu-border-color)
}
#bottom-menu {
grid-column: 1 / -1;
grid-row: 3;
border-top: 1px solid var(--menu-border-color)
}
#left-menu, #right-menu, #bottom-menu {
position: relative;
display: flex;
flex-direction: column;
min-height: 0;
}
[data-resize] {
position: absolute;
z-index: 1;
content: "";
}
#left-menu [data-resize], #right-menu [data-resize] {
border-left: 1px solid var(--resize-handle-color);
border-right: 1px solid var(--resize-handle-color);
margin: auto;
top: 0;
bottom: 0;
width: calc(var(--resize-handle-width) * 2 - 1px);
height: 1em;
}
#left-menu [data-resize] {
cursor: w-resize;
right: calc(var(--resize-handle-width) * -1);
}
#right-menu [data-resize] {
cursor: e-resize;
left: calc(var(--resize-handle-width) * -1);
}
#bottom-menu [data-resize] {
border-top: 1px solid var(--resize-handle-color);
border-bottom: 1px solid var(--resize-handle-color);
cursor: s-resize;
margin: auto;
left: 0;
right: 0;
top: calc(var(--resize-handle-width) * -1);
width: 1em;
height: calc(var(--resize-handle-width) * 2 - 1px);
}
body[data-resizing] {
user-select: none;
}
body[data-resizing="left-menu"] {
cursor: w-resize;
}
body[data-resizing="right-menu"] {
cursor: e-resize;
}
body[data-resizing="bottom-menu"] {
cursor: s-resize;
}
section {
overflow: auto;
padding: 0.5em;
}
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) {
width: 100%;
}
.atlas-texture {
image-rendering: -moz-crisp-edges; /* Firefox fallback */
image-rendering: pixelated; /* Modern browsers */
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
background-size: calc(var(--scale-factor) * var(--atlas-resolution))
calc(var(--scale-factor) * var(--atlas-resolution));
@ -61,15 +153,26 @@ main {
background-position-x: calc(var(--scale-factor) * var(--raw-x) * -1);
background-position-y: calc(var(--scale-factor) * var(--raw-y) * -1);
opacity: var(--atlas-opacity, 1);
}
.atlas-air {
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
width: calc(var(--scale-factor) * var(--size-x, var(--default-size)));
height: calc(var(--scale-factor) * var(--size-y, var(--default-size)));
outline: calc(var(--scale-factor) * 0.5px) solid var(--cell-empty-outline-color, #ccc);
opacity: var(--atlas-opacity, 1);
}
.atlas-air:hover, .atlas-texture:hover {
outline: calc(var(--scale-factor) * 1px) solid var(--cell-hover-outline-color, #8888);
}
[data-preview], [data-preview-remove] {
--atlas-opacity: var(--atlas-preview-opacity, 0.5);
}
[data-highlight] {
position: relative;
}
@ -77,29 +180,32 @@ main {
content: '';
position: absolute;
}
[data-highlight-distance="1"] {
--highlight-color: var(--highlight-near-color, #4848);
}
[data-highlight='x'] .atlas-texture:after, [data-highlight='x'] .atlas-air:after {
inset:
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4)
0
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4)
0;
background-color: var(--higlight-color, #f808);
background-color: var(--highlight-color, #f808);
}
[data-highlight='y'] .atlas-texture:after, [data-highlight='y'] .atlas-air:after {
inset:
0
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4)
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4)
0
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4);
background-color: var(--higlight-color, #f808);
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:
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-y, var(--default-size)) * 0.4)
calc(var(--scale-factor) * var(--size-y, var(--default-size)) * 0.4);
background-color: var(--higlight-color, #f808);
calc(var(--scale-factor) * var(--size-x, var(--default-size)) * 0.4);
background-color: var(--highlight-color, #f808);
}
#block-list [data-active] {