const DEFAULT_APP_STATE = {
floor: 0, // This is 0-based.
plan: new Array(FLOOR_COUNT).fill(null).map(() =>
new Array(GRID_SIZE_X * GRID_SIZE_Y).fill(0)),
// mouse_state can be “normal”, or “dnd”.
mouse_state: "normal",
dragged_asset_index: null,
plan_code: "",
};
async function serializePlan(app_state)
{
return compressBytes(Uint8Array.from(app_state.plan.flat()));
}
function genGrid(x_count, y_count, cell_size)
{
let lines = [];
for(let x = 0; x < x_count+1; x++)
{
lines.push(h("line", {
"x1": x * cell_size + 0.5, "x2": x * cell_size + 0.5, "y1": 0.5,
"y2": y_count * cell_size + 0.5,
}));
}
for(let y = 0; y < y_count+1; y++)
{
lines.push(h("line", {
"y1": y * cell_size + 0.5, "y2": y * cell_size + 0.5, "x1": 0.5,
"x2": x_count * cell_size + 0.5,
}));
}
return h("g", {"stroke": "black", "stroke-width": 1}, lines);
}
function AssetView({asset_index, on_drag_begin, on_drag_end})
{
let bg = ASSET_WHITE_BG;
if(TILES[asset_index].bg === null)
{
bg = null;
}
return h("div", {"class": "Asset", "draggable": true,
"data-asset-index": asset_index,
"ondragstart": on_drag_begin,
"ondragend": on_drag_end,
"title": TILES[asset_index].name,},
h("svg", ASSET_SVG_PROPS, bg, TILES[asset_index].inner_svg));
}
function AssetsView({on_drag_begin, on_drag_end})
{
let views = TILES.map((t, i) =>
h(AssetView, {asset_index: i, on_drag_begin: on_drag_begin,
on_drag_end: on_drag_end}));
return h("div", {}, views);
}
function PlanGridView({content, mouse_state, on_drop_asset})
{
const [mouse_coord, setMouseCoord] = preactHooks.useState(null);
function onDragOver(e)
{
e.preventDefault();
let b = e.target.getBoundingClientRect();
let x = e.clientX - b.left;
let y = e.clientY - b.top;
setMouseCoord([x, y]);
}
function onDragLeave(e)
{
setMouseCoord(null);
}
let hilight_box = null;
if(mouse_coord !== null && mouse_state != "dnd")
{
setMouseCoord(null);
}
if(mouse_coord !== null && mouse_state == "dnd")
{
let cell_x = Math.floor(mouse_coord[0] / (CELL_SIZE + 1));
let cell_y = Math.floor(mouse_coord[1] / (CELL_SIZE + 1));
hilight_box = h("rect", {
"x": cell_x * (CELL_SIZE + 1) + 0.5, "y": cell_y * (CELL_SIZE + 1) + 0.5,
"width": CELL_SIZE + 1, "height": CELL_SIZE + 1,
"stroke": "#2ed573", "stroke-width": 3, "fill": "transparent"});
}
let grid_content = content.map((idx, i) => {
if(idx == 0) return null;
let cell_x = i % GRID_SIZE_X;
let cell_y = Math.floor(i / GRID_SIZE_Y);
let bg = ASSET_WHITE_BG;
if(TILES[idx].bg === null)
{
bg = null;
}
return h("g", {"transform": `translate(${cell_x * (CELL_SIZE + 1) + 1} \
${cell_y * (CELL_SIZE + 1) + 1})`}, bg, TILES[idx].inner_svg);
});
let img_width = GRID_SIZE_X * (CELL_SIZE + 1) + 1;
let img_height = GRID_SIZE_Y * (CELL_SIZE + 1) + 1;
return h("svg", {"width": img_width, "height": img_height,
"version": "1.1", "ondragover": onDragOver,
"ondrop": on_drop_asset,
"id": "Grid", "ondragleave": onDragLeave,},
h("rect", {x: 0, y: 0, width: img_width, height: img_height,
fill: "#c0c0c0", "stroke-width": 0}),
grid_content, genGrid(GRID_SIZE_X, GRID_SIZE_Y, CELL_SIZE + 1),
hilight_box);
}
function PlanView({plan, mouse_state, on_drop_asset})
{
return h("div", {"id": "PlanView"},
h("div", {"id": "EntryIndicator"}, "⬇️"),
h(PlanGridView, {content: plan, mouse_state: mouse_state,
on_drop_asset: on_drop_asset}));
}
// on_floor_change takes one argument, which is the 0-based floor number.
function FloorSelector({floor, on_floor_change})
{
return h("div", {"id": "FloorSelectorWrapper", "class": "ButtonRowLeft"},
h("label", {}, "Floor"),
h("input", {"id": "InputFloor", "type": "number", "step": 1, "min": 1,
"max": 15, "value": floor,
onchange: e => on_floor_change(e.target.value - 1),},));
}
function App({initial_state})
{
const [state, setState] = preactHooks.useState(initial_state);
// This event is targeted at the dragged asset.
function onDragAssetBegin(e)
{
let new_state = structuredClone(state);
new_state.mouse_state = "dnd";
new_state.dragged_asset_index =
parseInt(e.target.getAttribute("data-asset-index"));
setState(new_state);
}
// This event is targeted at the grid.
function onDropAssetOnGrid(e)
{
if(e.dragEffect === "none") return;
let grid = document.getElementById("Grid");
let b = grid.getBoundingClientRect();
let x = e.clientX - b.left;
let y = e.clientY - b.top;
let cell_x = Math.floor(x / (CELL_SIZE + 1));
let cell_y = Math.floor(y / (CELL_SIZE + 1));
let cell_index = cell_y * GRID_SIZE_X + cell_x;
let new_state = structuredClone(state);
new_state.plan[new_state.floor][cell_index] = state.dragged_asset_index;
new_state.mouse_state = "normal";
new_state.dragged_asset_index = null;
serializePlan(new_state).then((s) => {
new_state.plan_code = s;
setState(new_state);
const url = new URL(location);
url.searchParams.set("plan", s);
window.history.pushState({}, "", url);
});
}
function onClickSaveImg()
{
let grid_width = GRID_SIZE_X * (CELL_SIZE + 1) + 1;
let grid_height = GRID_SIZE_Y * (CELL_SIZE + 1) + 1;
let canvas = document.createElement('canvas');
canvas.width = grid_width;
canvas.height = grid_height;
let context = canvas.getContext("2d");
var svg_str = new XMLSerializer().serializeToString(
document.getElementById("Grid"));
var img = new Image();
img.onload = function()
{
context.drawImage(this, 0, 0);
// Open PNG image in new tab.
var dataURL = canvas.toDataURL("image/png");
var new_tab = window.open('about:blank', 'image from canvas');
new_tab.document.write("<img src='" + dataURL + "' alt='from canvas'/>");
}
img.src = 'data:image/svg+xml; charset=utf8, ' +
encodeURIComponent(svg_str);
}
// new_floor is 0-based.
function onFloorChange(new_floor)
{
let new_state = structuredClone(state);
new_state.floor = new_floor;
setState(new_state);
}
return h("div", {},
h("div", {"class": "MainWrapper"},
h("aside", {},
h(FloorSelector, {floor: state.floor + 1,
on_floor_change: onFloorChange}),
h("div", {"class": "LabeledPanel"},
h("h2", {}, "Rooms"),
h(AssetsView, {on_drag_begin: onDragAssetBegin,}))),
h(PlanView, {plan: state.plan[state.floor],
mouse_state: state.mouse_state,
on_drop_asset: onDropAssetOnGrid,})),
h("hr", {}),
h("div", {},
h("textarea", {readonly: true}, state.plan_code)),
h("div", {"class": "ButtonRow"},
h("button", {onclick: onClickSaveImg, type: "button"},
"Open as Image!")),
);
}
function render(app_state)
{
preact.render(h(App, {initial_state: app_state}),
document.getElementById("Plan"));
}
const paramsString = window.location.search;
const searchParams = new URLSearchParams(paramsString);
const encoded_plan = searchParams.get("plan");
if(encoded_plan === null)
{
render(DEFAULT_APP_STATE);
}
else
{
decompressBytes(encoded_plan).then(a => {
let state = structuredClone(DEFAULT_APP_STATE);
let concated_plan = Array.from(a);
console.debug(concated_plan);
state.plan = [];
for(let i = 0; i < GRID_SIZE_X * GRID_SIZE_Y * FLOOR_COUNT;
i += GRID_SIZE_X * GRID_SIZE_Y)
{
let floor_slice =
concated_plan.slice(i, i + GRID_SIZE_X * GRID_SIZE_Y);
console.debug(floor_slice);
state.plan.push(floor_slice);
}
state.plan_code = encoded_plan;
render(state);
});
}