import { OnlineUsers } from "rkgk/online-users.js"; export class Chunk { constructor(chunkAllocator) { this.id = chunkAllocator.alloc(); } destroy(chunkAllocator) { chunkAllocator.dealloc(this.id); } upload(chunkAllocator, source) { chunkAllocator.upload(this.id, source); } download(chunkAllocator) { return chunkAllocator.download(this.id); } } let layerIdCounter = 0; export class Layer { chunks = new Map(); id = layerIdCounter++; constructor({ name, chunkSize, chunkLimit }) { this.name = name; this.chunkSize = chunkSize; this.chunkLimit = chunkLimit; console.info("created layer", this.id, this.name); } destroy(chunkAllocator) { for (let { chunk } of this.chunks.values()) { chunk.destroy(chunkAllocator); } } getChunk(x, y) { return this.chunks.get(chunkKey(x, y))?.chunk; } getOrCreateChunk(chunkAllocator, x, y) { let key = chunkKey(x, y); if (this.chunks.has(key)) { return this.chunks.get(key)?.chunk; } else { if (this.chunkLimit != null && this.chunks.size >= this.chunkLimit) return null; let chunk = new Chunk(chunkAllocator); this.chunks.set(key, { x, y, chunk }); return chunk; } } composite(chunkAllocator, src, op) { for (let { x, y, chunk: srcChunk } of src.chunks.values()) { let dstChunk = this.getOrCreateChunk(chunkAllocator, x, y); if (dstChunk == null) continue; chunkAllocator.composite(dstChunk.id, srcChunk.id, op); } } async toEdits(chunkAllocator) { console.time("toEdits"); let edits = []; let encodeTime = 0; for (let { x, y, chunk } of this.chunks.values()) { edits.push({ chunk: { x, y }, data: chunk.download(chunkAllocator).then(async (downloaded) => { let start = performance.now(); let imageBitmap = await createImageBitmap( new ImageData( new Uint8ClampedArray(downloaded.data), downloaded.width, downloaded.height, ), ); chunkAllocator.freeDownload(downloaded.data); let canvas = new OffscreenCanvas(downloaded.width, downloaded.height); let ctx = canvas.getContext("bitmaprenderer"); ctx.transferFromImageBitmap(imageBitmap); let blob = canvas.convertToBlob({ type: "image/png" }); let end = performance.now(); console.debug("encoding image took", end - start, "ms"); encodeTime += end - start; return blob; }), }); } for (let edit of edits) { edit.data = await edit.data; } console.timeEnd("toEdits"); return edits; } } export function chunkKey(x, y) { return `${x},${y}`; } export class Wall { layers = []; // do not modify directly; only read #layersById = new Map(); constructor(wallInfo) { this.chunkSize = wallInfo.chunkSize; this.paintArea = wallInfo.paintArea; this.maxEditSize = wallInfo.maxEditSize; this.onlineUsers = new OnlineUsers(wallInfo); this.mainLayer = new Layer({ name: "main", chunkSize: this.chunkSize }); this.addLayer(this.mainLayer); } addLayer(layer) { if (!this.#layersById.get(layer.id)) { this.layers.push(layer); this.#layersById.set(layer.id, layer); } else { console.warn("attempt to add layer more than once", layer); } return layer; } removeLayer(layer) { if (this.#layersById.delete(layer.id)) { let index = this.layers.findIndex((x) => x == layer); this.layers.splice(index, 1); } else { console.warn("attempt to remove layer more than once", layer); } } getLayerById(id) { return this.#layersById.get(id); } }