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", plan_code: "", }; async function serializePlan(app_state) { let rest = new Array(GRID_SIZE_X * GRID_SIZE_Y * (FLOOR_COUNT - 1)).fill(0); let concated_plans = app_state.plan.reduce((big, floor_plan) => big.concat(floor_plan), []); return compressBytes(Uint8Array.from(concated_plans)); } 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}) { 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, "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}) { return h("div", {"id": "PlanView"}, h("div", {"id": "EntryIndicator"}, "⬇️"), h(PlanGridView, {content: plan, mouse_state: mouse_state})); } // 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); function onDragAssetBegin(e) { let new_state = structuredClone(state); new_state.mouse_state = "dnd"; setState(new_state); } function onDragAssetEnd(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; if(x < 0 || y < 0 || x > b.width || y > b.height) return; 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 asset_index = parseInt(e.target.getAttribute("data-asset-index")); let new_state = structuredClone(state); new_state.plan[new_state.floor][cell_index] = asset_index; new_state.mouse_state = "normal"; 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("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, on_drag_end: onDragAssetEnd}))), h(PlanView, {plan: state.plan[state.floor], mouse_state: state.mouse_state})), 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); }); }