From 3467da728456a713a4f3eeaaa32e695586c2204d Mon Sep 17 00:00:00 2001 From: Shaad Alaka Date: Thu, 25 May 2023 00:48:36 +0200 Subject: [PATCH] New job queueing, automatic multi-tool, icons for UI, canvas overlay for highlighting --- public/dot-m.svg | 49 +++ public/dot-s.svg | 49 +++ public/dot-xs.svg | 49 +++ public/dot.svg | 49 +++ public/icons/dot.svg | 49 +++ quasar.config.js | 1 + src/assets/data/0_paper_tileset.json | 4 +- src/assets/js/HSWFC.js | 559 +++++++++++++++++++++------ src/assets/js/worker.js | 49 +-- src/css/app.scss | 19 +- src/layouts/MainLayout.vue | 368 +++++++++++++----- yarn.lock | 2 +- 12 files changed, 997 insertions(+), 250 deletions(-) create mode 100644 public/dot-m.svg create mode 100644 public/dot-s.svg create mode 100644 public/dot-xs.svg create mode 100644 public/dot.svg create mode 100644 public/icons/dot.svg diff --git a/public/dot-m.svg b/public/dot-m.svg new file mode 100644 index 0000000..7fd4bd3 --- /dev/null +++ b/public/dot-m.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/public/dot-s.svg b/public/dot-s.svg new file mode 100644 index 0000000..35589d0 --- /dev/null +++ b/public/dot-s.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/public/dot-xs.svg b/public/dot-xs.svg new file mode 100644 index 0000000..45b08c9 --- /dev/null +++ b/public/dot-xs.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/public/dot.svg b/public/dot.svg new file mode 100644 index 0000000..7fd5aa1 --- /dev/null +++ b/public/dot.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/public/icons/dot.svg b/public/icons/dot.svg new file mode 100644 index 0000000..7fd5aa1 --- /dev/null +++ b/public/icons/dot.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/quasar.config.js b/quasar.config.js index 1bfe774..33837ad 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -44,6 +44,7 @@ module.exports = configure(function (/* ctx */) { "roboto-font", // optional, you are not bound to it "material-icons", // optional, you are not bound to it + "fontawesome-v6", ], // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build diff --git a/src/assets/data/0_paper_tileset.json b/src/assets/data/0_paper_tileset.json index f0a7404..5ea9581 100644 --- a/src/assets/data/0_paper_tileset.json +++ b/src/assets/data/0_paper_tileset.json @@ -4,7 +4,7 @@ "paintable": true, "generate": true, "color": [0, 0, 0], - "children": { "water": 1, "land": 1, "village": 1 }, + "children": { "water": 0.1, "land": 1, "village": 1 }, "adjacencies": { "U": ["root"], "D": ["root"], @@ -88,7 +88,7 @@ "paintable": true, "generate": true, "color": [190, 0, 190], - "children": { "water": 1, "sand": 1, "grass": 1, "house": 10 }, + "children": { "water": 1, "grass": 1, "house": 10 }, "adjacencies": { "U": ["village"], "D": ["village"], diff --git a/src/assets/js/HSWFC.js b/src/assets/js/HSWFC.js index 46844f4..830bdf0 100644 --- a/src/assets/js/HSWFC.js +++ b/src/assets/js/HSWFC.js @@ -42,6 +42,9 @@ import { createPalette } from "hue-map"; // import BTree from "sorted-btree"; // import { SoMap, SoSet } from "somap"; // import * as ProperSkipList from "proper-skip-list"; + +import { MinPriorityQueue } from "@datastructures-js/priority-queue"; + import Z from "redis-sorted-set"; const SortedSet = Z; // class QTNode { @@ -221,13 +224,26 @@ const SortedSet = Z; // } class Target { - constructor(x, y) { + constructor(x, y, tile_index = -1, mode = 0) { this.x = x; this.y = y; + this.tile_index = tile_index; + this.mode = mode; } equals(other) { - return this.x === other.x && this.y === other.y; + return ( + this.x === other.x && + this.y === other.y && + this.tile_index === other.tile_index + ); + } +} + +class Job { + constructor(type, targets) { + this.type = type; + this.targets = targets; } } @@ -285,11 +301,19 @@ class Node { return this; } - getDepth(d) { + ancestors() { + let s = new Set([this]); for (const parent of this.incoming.keys()) { - return parent.getDepth(d + 1); + s = s.add(parent).union(parent.ancestors()); } - return d; + return s; + } + + commonAncestor(node) { + const common = this.ancestors().intersect(node.ancestors()); + const max = common.maxBy((n) => n.depth); + const maxn = common.filter((n) => n.depth === max.depth); + return maxn.max((n) => n.leaves().size); } leaves() { @@ -326,17 +350,22 @@ GridState.i = 0; export class Grid { constructor(gridSize, tileset, adjacencies, invertedIndex, bufferSize) { this.entropyBufferSize = bufferSize; + this.paintBuffer = []; this.snapshots = {}; this.undoStack = []; this.redoStack = []; this.current = null; this.counter = 0; - this.collapseQueue = new Deque(); this.gridChoices = Object.keys(tileset).length; + this.lock = false; this.gridSize = gridSize; this.tileset = tileset; + + this.jobQueue = new MinPriorityQueue((job) => job.type); + this.ALL = range(0, this.gridChoices); this.COLOR = range(0, 4); + this.entropyColors = createPalette({ map: "autumn", steps: this.gridChoices, @@ -361,11 +390,13 @@ export class Grid { }) ); + // TODO: Seems U/D and R/L are flipped this.eigenOffsets = { D: 2, U: 0, L: 3, R: 1 }; this.colorMap = new Array(this.gridChoices); this.allowed = new Array(this.gridChoices); this.nameIndex = new Array(this.gridChoices); + this.invertedIndex = invertedIndex; for (const name in tileset) { const tile = tileset[name]; tile.color.push(255); @@ -395,15 +426,11 @@ export class Grid { } this.root = this.nodeIndex[this.nameIndex[0]].root(); - // Set the depths - for (const n in tileset) { - this.nodeIndex[n].depth = this.nodeIndex[n].getDepth(0); - } - // Precompute all paths // Access method: .get(leaf).get(meta) this.pathMap = new Map(); - for (const node of this.root.leaves()) { + for (const n in this.nodeIndex) { + const node = this.nodeIndex[n]; console.log("LEAF: ", this.nameIndex[node.index]); let tempMap = new Map(); let q = [node]; @@ -438,6 +465,14 @@ export class Grid { } } + // Set the depths based on shortest path to root + for (const n in this.nodeIndex) { + const node = this.nodeIndex[n]; + node.depth = + new Set(this.pathMap.get(node.index).get(0)).minBy((a) => a.size) + .length - 1; + } + // NOTE: If the adjacencies are bad, a giant propagation wave can be triggered right at the start, explains the lag-spike this.adj = new Map(); this.adjaug = new Map(); @@ -489,6 +524,18 @@ export class Grid { } } }); + + // for (const node of this.root.leaves()) { + // console.log( + // node.name, + // node + // .ancestors() + // .toArray() + // .map((c) => c.name) + // ); + // } + + // console.log(this.nodeIndex["sand"].commonAncestor(this.nodeIndex["grass"])); } getState() { @@ -496,6 +543,8 @@ export class Grid { image: this.image, entropyImage: this.entropyImage, nameIndex: this.nameIndex, + invertedIndex: this.invertedIndex, + chosen: this.chosen, choices: this.choices, entropy: this.entropy, }; @@ -536,12 +585,13 @@ export class Grid { // Init eigen await eig.ready; - console.log(this.nodeIndex); + // console.log(this.nodeIndex); - console.log(this.indices([2, 23, 1, 2, 3, 4, 52, 2], 2)); + // console.log(this.indices([2, 23, 1, 2, 3, 4, 52, 2], 2)); // Build masks this.SM = {}; + this.eigSM = new eig.BoolMatrix(this.gridChoices, this.gridChoices); this.CM = {}; this.eigCM = new eig.BoolMatrix(this.gridChoices, this.gridChoices); this.LM = new eig.BoolMatrix(this.gridChoices, 1); //zeros(this.gridChoices); @@ -559,6 +609,7 @@ export class Grid { while (children.length > 0) { var child = children.pop(); subMask.set([child.index], 1); + this.eigCM.set(node.index, child.index, true); children.push(...child.outgoing.keys()); } this.SM[node.index] = subMask; @@ -578,6 +629,7 @@ export class Grid { new eig.BoolMatrix(this.adjaug.get("D")._data), new eig.BoolMatrix(this.adjaug.get("L")._data), this.eigCM, + this.eigSM, this.LM ); @@ -610,7 +662,7 @@ export class Grid { this.applyEntropyModifiers(this.entropy.get([rX, rY])) ); - this.clearQueue(); + // this.clearQueue(); } // Undo system @@ -665,7 +717,7 @@ export class Grid { loadGridState(gridState) { console.log("Loaded gridstate: ", gridState); this.current = gridState; - this.clearQueue(); + // this.clearQueue(); this.choices = new Uint8Array(gridState.choices); this.eigenGrid.setChoices(this.choices); this.chosen = clone(gridState.chosen); @@ -689,7 +741,7 @@ export class Grid { } }); - if (this.minEntropy.length === 0) { + while (this.minEntropy.length === 0) { // Choose random point const rX = floor(random(this.gridSize)); const rY = floor(random(this.gridSize)); @@ -766,40 +818,44 @@ export class Grid { return arr.map((v, u) => (v === val ? u : -1)).filter((v) => v !== -1); } - isEnqueued(collapseTarget) { - for (const e of this.collapseQueue.entries()) { - if ( - e[0] === collapseTarget[0] && - e[1] === collapseTarget[1] && - e[2] === collapseTarget[2] - ) { - return true; + // isEnqueued(collapseTarget) { + // for (const e of this.collapseQueue.entries()) { + // if ( + // e[0] === collapseTarget[0] && + // e[1] === collapseTarget[1] && + // e[2] === collapseTarget[2] + // ) { + // return true; + // } + // } + // return false; + // } + + autoCollapse(targets) { + for (const t of targets) { + if (this.minEntropy.length > 0) { + const le = this.leastEntropyQT(); + if (le !== undefined) { + t.x = le.x; + t.y = le.y; + // / console.log(t); + this.collapse([t]); + this.propagate([t]); + } } } - return false; - } - - autoCollapse(amount = 1) { - this.collapseQueue.extendLeft(new Array(amount).fill([-1, -1, -1])); } update() { - // TODO: Investigate whether it is OK for responsiveness to go from an "if" to a "while" - while (this.collapseQueue.size > 0) { - const entry = this.collapseQueue.popLeft(); - let target = entry; - // Automated collapse; we only fetch the entropy on popping the queue - if (entry[2] === -1) { - if (this.minEntropy.length > 0) { - const le = this.leastEntropyQT(); - if (le !== undefined) { - target[0] = le.x; - target[1] = le.y; - this.collapse(le.x, le.y, -1); - } - } - } else { - this.collapse(target[0], target[1], target[2]); + while (!this.jobQueue.isEmpty()) { + const job = this.jobQueue.dequeue(); + switch (job.type) { + case 0: + this.paint(job.targets); + break; + case 1: + this.autoCollapse(job.targets); + break; } } } @@ -813,107 +869,343 @@ export class Grid { return -entropy + random(0.0001); } - clearQueue() { - this.collapseQueue.clear(); + // clearQueue() { + // this.collapseQueue.clear(); + // } + + paintEnqueue(coordinates, tileIndex) { + const targets = coordinates.map((c) => new Target(c[0], c[1], tileIndex)); + this.jobQueue.enqueue({ type: 0, targets: targets }); + // for ( + // let i = Math.floor(x - markerSize / 2) + 1; + // i < Math.floor(x + markerSize / 2) + 1; + // i++ + // ) { + // for ( + // let j = Math.floor(y - markerSize / 2) + 1; + // j < Math.floor(y + markerSize / 2) + 1; + // j++ + // ) { + // if (i >= 0 && i < this.gridSize && j >= 0 && j < this.gridSize) { + // // stuff here + // } + // } + // } } - manualCollapse(x, y, tile_index) { - if (!this.isEnqueued([x, y, tile_index])) { - this.collapseQueue.pushLeft([x, y, tile_index]); + autoEnqueue(amount = 1) { + const targets = []; + for (let i = 0; i < amount; i++) { + targets.push(new Target(-1, -1)); } + this.jobQueue.enqueue({ type: 1, targets: targets }); } - collapse(x, y, tile_index = -1) { - const choices = this.getChoices(x, y); - const currentTile = this.chosen._data[x][y]; + // TODO: bonus feature, to illustrate the idea of job enqueueing + growEnqueue() {} + + manualCollapse(jobId, x, y, markerSize, tileIndex) {} + + paint(targets) { + // bins + const uncollapseBin = []; + const collapseBin = []; + const replaceBinUp = []; + const replaceBinDown = []; + for (let t of targets) { + const x = t.x; + const y = t.y; + const targetTileIndex = t.tile_index; + const targetNode = this.nodeIndex[this.nameIndex[targetTileIndex]]; + const targetSubtreeMask = this.SM[targetTileIndex]; + const currentTileIndex = this.chosen._data[x][y]; + const currentNode = this.nodeIndex[this.nameIndex[currentTileIndex]]; + const currentSubtreeMask = this.SM[currentTileIndex]; + const ancestor = currentNode.commonAncestor(targetNode); // This is actually problematic... + + if (currentTileIndex === targetTileIndex) { + // console.log("NO-OP"); + continue; + } else if ( + !targetSubtreeMask.get([currentTileIndex]) && + !currentSubtreeMask.get([targetTileIndex]) + ) { + // Uncollapse to common ancestor first + // console.log("UP-DOWN"); + replaceBinUp.push(new Target(t.x, t.y, this.root.index)); + replaceBinDown.push(t); + } else { + if (ancestor === currentNode) { + // this.collapse([t]); + collapseBin.push(t); + } else { + uncollapseBin.push(t); + // this.uncollapse([t]); + } + } + } - const childMask = this.CM[currentTile]; + // Order from most flexible to most restrictive - // Apply weights - for (const cidx of this.indices(childMask._data)) { - const edge = this.nodeIndex[this.nameIndex[currentTile]].outgoing.get( - this.nodeIndex[this.nameIndex[cidx]] - ); - childMask._data[cidx] = edge.weight; //.set([cidx], edge.weight); + // Replace + if (replaceBinUp && replaceBinDown) { + this.uncollapse(replaceBinUp); + this.depropagate(replaceBinUp); + this.collapse(replaceBinDown); + this.propagate(replaceBinDown); } - - let choice = pickRandom(this.ALL, dotMultiply(choices, childMask)); - // console.log(choices, childMask._data); - // console.log("Choice: ", choice); - if (isUndefined(choice)) { - // console.log("Warning, uncollapsable cell:", x, y, choices); - return false; + // Uncollapse + if (uncollapseBin) { + this.uncollapse(uncollapseBin); + this.depropagate(uncollapseBin); } + // Regular collapse + if (collapseBin) { + this.collapse(collapseBin); + this.propagate(collapseBin); + } + } + + collapse(targets) { + for (let t of targets) { + const x = t.x; + const y = t.y; + const tile_index = t.tile_index; + const choices = this.getChoices(x, y); + const currentTile = this.chosen._data[x][y]; + + const childMask = this.CM[currentTile]; + + // Apply weights + for (const cidx of this.indices(childMask._data)) { + const edge = this.nodeIndex[this.nameIndex[currentTile]].outgoing.get( + this.nodeIndex[this.nameIndex[cidx]] + ); + childMask._data[cidx] = edge.weight; //.set([cidx], edge.weight); + } + + let choice = pickRandom(this.ALL, dotMultiply(choices, childMask)); + // console.log(choices, childMask._data); + // console.log("Choice: ", choice); + if (isUndefined(choice)) { + // console.log("Warning, uncollapsable cell:", x, y, choices); + // console.log("HUH1"); + const s = this.minEntropy.del(this.invIndex._data[x][y]); // Should be removed + + // console.log(this.namesFromChoices(choices)); + // console.log(this.namesFromChoices(childMask)); + continue; + } + + if (tile_index >= 0) { + if (choices[tile_index]) { + choice = tile_index; + } else { + // Tile is not allowed + // console.log("HUH2"); + // No need to remove from buffer --> tile_index >= 0 means painted + continue; + } + } + + const newChoices = dotMultiply(this.SM[choice], choices); + newChoices._data[choice] = 1; //.set([choice], 1); + this.setChoices(x, y, newChoices._data); + // console.log("new array: ", newChoices); + + // Sync with eigen + var v = new eig.Vector(); + newChoices._data.forEach((val) => v.push_back(val)); + this.eigenGrid.setCol(x, y, v); + + // this.chosen.subset(index(x, y), choice); + this.chosen._data[x][y] = choice; + + // this.entropy.subset(index(x, y), this.getCellEntropy(x, y)); + if (tile_index >= 0) { + this.painted._data[x][y] = 1; + } + const entropyValue = this.getCellEntropy(x, y); + this.entropy._data[x][y] = entropyValue; - if (tile_index >= 0) { - if (choices[tile_index]) { - choice = tile_index; + if (entropyValue > 1) { + this.minEntropy.add( + this.invIndex._data[x][y], + this.applyEntropyModifiers(entropyValue, { + painted: this.painted._data[x][y], + }) + ); } else { - return false; + // console.log("DEL:", x, y, "|", entropyValue); + this.painted._data[x][y] = 0; + const s = this.minEntropy.del(this.invIndex._data[x][y]); + // console.log(s, this.minEntropy.min()); } + // console.log(this.minEntropy); + // console.log("STATE:", this.entropyBuffer.toArray()); + // this.image.subset(index(x, y, this.COLOR), this.colorMap[choice]); + this.image._data[x][y][0] = this.colorMap[choice][0]; + this.image._data[x][y][1] = this.colorMap[choice][1]; + this.image._data[x][y][2] = this.colorMap[choice][2]; + + // For debugging purposes + // const entr = this.entropy.get([x, y]); + this.entropyImage.subset( + index(x, y, this.COLOR), + entropyValue <= this.entropyColors.length && entropyValue >= 0 + ? this.entropyColors[floor(entropyValue - 1)] + : [0, 0, 0, 255] + ); } + return true; + } - const newChoices = dotMultiply(this.SM[choice], choices); - newChoices._data[choice] = 1; //.set([choice], 1); - this.setChoices(x, y, newChoices._data); - // console.log("new array: ", newChoices); + uncollapse(targets) { + for (let t of targets) { + const x = t.x; + const y = t.y; + const tile_index = t.tile_index; + const choices = this.getChoices(x, y); + const currentTile = this.chosen._data[x][y]; + const subtreeMask = this.SM[tile_index]; + + // if the current tile is not in the subtree of the new tile: abort + if (!subtreeMask.get([currentTile])) { + return false; + } - // Sync with eigen - var v = new eig.Vector(); - newChoices._data.forEach((val) => v.push_back(val)); - this.eigenGrid.setCol(x, y, v); + const newChoices = subtreeMask; + newChoices._data[tile_index] = 1; //.set([choice], 1); + this.setChoices(x, y, newChoices._data); + // console.log("new array: ", newChoices); - // this.chosen.subset(index(x, y), choice); - this.chosen._data[x][y] = choice; + // Sync with eigen + // TODO: can move this into "setChoices" + var v = new eig.Vector(); + newChoices._data.forEach((val) => v.push_back(val)); + this.eigenGrid.setCol(x, y, v); - // this.entropy.subset(index(x, y), this.getCellEntropy(x, y)); - if (tile_index >= 0) { - this.painted._data[x][y] = 1; - } - const entropyValue = this.getCellEntropy(x, y); - this.entropy._data[x][y] = entropyValue; + // this.chosen.subset(index(x, y), choice); + this.chosen._data[x][y] = tile_index; - if (entropyValue > 1) { - this.minEntropy.add( - this.invIndex._data[x][y], - this.applyEntropyModifiers(entropyValue, { - painted: this.painted._data[x][y], - }) - ); - } else { - // console.log("DEL:", x, y, "|", entropyValue); - this.painted._data[x][y] = 0; - const s = this.minEntropy.del(this.invIndex._data[x][y]); - // console.log(s, this.minEntropy.min()); - } - // console.log(this.minEntropy); - // console.log("STATE:", this.entropyBuffer.toArray()); - // this.image.subset(index(x, y, this.COLOR), this.colorMap[choice]); - this.image._data[x][y][0] = this.colorMap[choice][0]; - this.image._data[x][y][1] = this.colorMap[choice][1]; - this.image._data[x][y][2] = this.colorMap[choice][2]; - - // For debugging purposes - // const entr = this.entropy.get([x, y]); - this.entropyImage.subset( - index(x, y, this.COLOR), - entropyValue <= this.entropyColors.length && entropyValue >= 0 - ? this.entropyColors[floor(entropyValue - 1)] - : [0, 0, 0, 255] - ); + // this.entropy.subset(index(x, y), this.getCellEntropy(x, y)); + if (tile_index >= 0) { + this.painted._data[x][y] = 1; + } - this.propagate(x, y, tile_index >= 0); - // console.log("========================"); + // We have to calculate the entropy; it will be updated anyway if some of the children are not allowed + const entropyValue = this.getCellEntropy(x, y); + this.entropy._data[x][y] = entropyValue; + if (entropyValue > 1) { + this.minEntropy.add( + this.invIndex._data[x][y], + this.applyEntropyModifiers(entropyValue, { + painted: this.painted._data[x][y], + }) + ); + } else { + // console.log("DEL:", x, y, "|", entropyValue); + this.painted._data[x][y] = 0; + const s = this.minEntropy.del(this.invIndex._data[x][y]); + // console.log(s, this.minEntropy.min()); + } + // // console.log(this.minEntropy); + // // console.log("STATE:", this.entropyBuffer.toArray()); + // // this.image.subset(index(x, y, this.COLOR), this.colorMap[choice]); + this.image._data[x][y][0] = this.colorMap[tile_index][0]; + this.image._data[x][y][1] = this.colorMap[tile_index][1]; + this.image._data[x][y][2] = this.colorMap[tile_index][2]; + + // // For debugging purposes + // // const entr = this.entropy.get([x, y]); + this.entropyImage.subset( + index(x, y, this.COLOR), + entropyValue <= this.entropyColors.length && entropyValue >= 0 + ? this.entropyColors[floor(entropyValue - 1)] + : [0, 0, 0, 255] + ); + } return true; } - // TODO: add function to set tile, that combines all the different things happening + namesFromChoices(choices) { + return this.nameIndex.filter((n) => choices[this.invertedIndex[n]]); + } - propagate(cx, cy, painted) { - const q = new Deque(); + depropagate(targets) { + const Q = new Deque(targets); const postQ = new Deque(); - q.pushLeft(new Target(cx, cy)); + const P = targets; + while (Q.size > 0) { + const p = Q.pop(); + // console.log("Depropagating", p, "..."); + this.offsets.forEach((o, k) => { + const nx = p.x + o.x; + const ny = p.y + o.y; + if (nx >= 0 && nx < this.gridSize && ny >= 0 && ny < this.gridSize) { + var diff = this.eigenGrid.depropagate( + p.x, + p.y, + nx, + ny, + this.eigenOffsets[k], + this.chosen.get([nx, ny]) + ); + + var neighbour = new Target(nx, ny); + if (diff) { + postQ.push(neighbour); + Q.push(neighbour); + } else { + P.push(neighbour); + } + } + }); + } + + // Copy choices + this.choices = this.eigenGrid.getChoices(); + // console.time("postQ"); + while (postQ.size > 0) { + let n = postQ.pop(); + // console.log("Propagating caused change", n, "..."); + // console.log( + // " AFTER: ", + // this.namesFromChoices(this.getChoices(n.x, n.y)) + // ); + + const entropyValue = this.getCellEntropy(n.x, n.y); + this.entropy._data[n.x][n.y] = entropyValue; + if (entropyValue > 1) { + this.minEntropy.add( + this.invIndex._data[n.x][n.y], + this.applyEntropyModifiers(entropyValue, { + painted: this.painted._data[n.x][n.y], + }) + ); + } else { + this.painted._data[n.x][n.y] = 0; + this.minEntropy.del(this.invIndex._data[n.x][n.y]); + } + try { + this.entropyImage.subset( + index(n.x, n.y, this.COLOR), + entropyValue <= this.entropyColors.length + ? this.entropyColors[round(entropyValue) - 1] + : [0, 0, 0, 255] + ); + } catch { + this.entropyImage.subset(index(n.x, n.y, this.COLOR), [255, 0, 0, 255]); + } + } + this.propagate(P, false); + } + + propagate(targets, painted = false) { + const q = new Deque(targets); + const postQ = new Deque(); + while (q.size > 0) { const p = q.pop(); @@ -931,6 +1223,11 @@ export class Grid { ); if (diff) { + // console.log("Propagating caused change", p, "..."); + // console.log( + // " BEFORE: ", + // this.namesFromChoices(this.getChoices(nx, ny)) + // ); var neighbour = new Target(nx, ny); postQ.push(neighbour); q.push(neighbour); @@ -945,6 +1242,12 @@ export class Grid { // console.time("postQ"); while (postQ.size > 0) { let n = postQ.pop(); + // console.log("Propagating caused change", n, "..."); + // console.log( + // " AFTER: ", + // this.namesFromChoices(this.getChoices(n.x, n.y)) + // ); + const entropyValue = this.getCellEntropy(n.x, n.y); this.entropy._data[n.x][n.y] = entropyValue; if (entropyValue > 1) { @@ -966,9 +1269,21 @@ export class Grid { : [0, 0, 0, 255] ); } catch { + // CONTRADICTION + console.log("Contradiction! Undoing..."); + this.undo(); + return; this.entropyImage.subset(index(n.x, n.y, this.COLOR), [255, 0, 0, 255]); } } // console.timeEnd("postQ"); } + + // waitForUnlock() { + // const poll = (resolve) => { + // if (!this.lock) resolve(); + // else setTimeout((_) => poll(resolve), 400); + // }; + // return new Promise(poll); + // } } diff --git a/src/assets/js/worker.js b/src/assets/js/worker.js index 0a375de..b5130cc 100644 --- a/src/assets/js/worker.js +++ b/src/assets/js/worker.js @@ -1,11 +1,9 @@ import { Grid } from "./HSWFC.js"; let grid; -const batchSize = 256; let autoStepSize = 1; -let lock = false; -self.onmessage = ({ data: { question, value } }) => { +self.onmessage = ({ data: { question, value, cells } }) => { if (question === "init") { const width = value[0]; const tileset = value[1]; @@ -21,9 +19,7 @@ self.onmessage = ({ data: { question, value } }) => { }); }); } else if (question === "update") { - for (let i = 0; i < batchSize; i++) { - grid.update(); - } + grid.update(); self.postMessage({ grid: { ...grid.getState(), @@ -32,35 +28,22 @@ self.onmessage = ({ data: { question, value } }) => { }, message: "doneUpdate", }); - } else if (question === "manual") { - const x = value[1]; - const y = value[0]; - const tileIndex = value[2]; - const markerSize = value[3]; + } else if (question === "paint") { + const tileIndex = value[0]; + const coordinates = cells.flatMap((_, i, a) => + i % 2 ? [] : [a.slice(i, i + 2)] + ); + grid.paintEnqueue(coordinates, tileIndex); - for ( - let i = Math.floor(x - markerSize / 2) + 1; - i < Math.floor(x + markerSize / 2) + 1; - i++ - ) { - for ( - let j = Math.floor(y - markerSize / 2) + 1; - j < Math.floor(y + markerSize / 2) + 1; - j++ - ) { - if (i >= 0 && i < grid.gridSize && j >= 0 && j < grid.gridSize) { - grid.manualCollapse(i, j, tileIndex); - } - } - } + // grid.manualCollapse(x, y, markerSize, tileIndex); } else if (question === "auto") { - if (!lock) { - grid.autoCollapse(autoStepSize); + if (!grid.lock) { + grid.autoEnqueue(autoStepSize); } self.postMessage({ grid: grid.getState(), message: "doneAuto" }); } else if (question === "clear") { // console.log("Clearing"); - grid.clearQueue(); + // grid.clearQueue(); } else if (question === "reset") { grid.initialize(); } else if (question === "undo") { @@ -73,16 +56,16 @@ self.onmessage = ({ data: { question, value } }) => { autoStepSize = value; } else if (question === "onestep") { if (value) { - grid.autoCollapse(value); + grid.autoEnqueue(value); } else { - grid.autoCollapse(autoStepSize); + grid.autoEnqueue(autoStepSize * autoStepSize); } grid.update(); self.postMessage({ grid: grid.getState(), message: "doneStep" }); } else if (question === "lock") { - lock = true; + grid.lock = true; } else if (question === "unlock") { - lock = false; + grid.lock = false; } else if (question === "snapshot") { grid.snapshot(value); } else if (question === "load snapshot") { diff --git a/src/css/app.scss b/src/css/app.scss index c2e5070..51d0b9d 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -1,11 +1,20 @@ // app global css in SCSS form -#wfc { - width: 40%; - height: 40%; - image-rendering: pixelated; +.canvasHolder { + min-width: 70vh; + min-height: 70vh; + width: 70vh; + height: 70vh; + position: relative; + box-sizing: border-box; border: 4px solid lightgray; - // cursor: crosshair; +} + +canvas { + width: 100%; + height: 100%; + image-rendering: pixelated; + position: absolute; } .q-tree__node-header:has(div > .selectedtree) { diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index c8d7b80..8bf478c 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -36,7 +36,15 @@ - +
+ + +
+
- State - - - Tiles + + + + Tiles
- Tool + Toggles @@ -171,10 +163,11 @@
- + - Brush Size + Brush Size + - + + + + - Speed + Solve Speed - + +
- - + --> + + + @@ -395,21 +475,37 @@ choices: {{ + // Formula: c + C * y + C * Y * x this.mx >= 0 && this.mx < this.width && this.my >= 0 && this.my < this.height ? this.grid?.nameIndex?.filter( - (_, i) => + (n) => this.grid?.choices?.[ - i + - this.tiles.length * this.my + - this.tiles.length * this.height * this.mx + this.grid?.invertedIndex[n] + + this.grid?.nameIndex.length * this.mx + // TODO: Investigate why this is flipped compared to the algorithm... + this.grid?.nameIndex.length * this.height * this.my ] ) : [] }} + + + current: + {{ + // Formula: c + C * y + C * Y * x + this.mx >= 0 && + this.mx < this.width && + this.my >= 0 && + this.my < this.height + ? this.grid?.nameIndex?.[ + this.grid?.chosen._data?.[this.my]?.[this.mx] + ] + : "n/a" + }} + entropy: {{ @@ -438,6 +534,7 @@ import { ones, range, floor, + sin, } from "mathjs"; export default defineComponent({ @@ -450,15 +547,19 @@ export default defineComponent({ // 3: [70, 70, 255, 255], // W data() { return { + time: 0, width: 64, height: 64, mx: 0, my: 0, - size: 1, - stepSize: 1, + mxp: 0, + myp: 0, + size: 5, + stepSize: 5, intervalId: undefined, grid: undefined, context: undefined, + highlightContext: undefined, leftMouseDown: false, rightMouseDown: false, autoCollapse: false, @@ -472,12 +573,15 @@ export default defineComponent({ chosenTileset: 0, importedTilesets: [], ctrlDown: false, - expanded: [0], + expanded: [], canRedo: false, canUndo: false, snapshotCount: 4, imageUrl: [], imagePopup: [], + paintBuffer: [], + processBuffer: [], + processFlush: 0, }; }, computed: { @@ -529,6 +633,12 @@ export default defineComponent({ // Any operation that creates a checkpoint invalidates our redo stack (unless we start using some funny merge strategies..) // this.canRedo = false; }, + isEmpty(obj) { + for (var x in obj) { + return false; + } + return true; + }, undo() { this.worker.postMessage({ question: "undo", @@ -546,23 +656,37 @@ export default defineComponent({ }); }, setStepSize() { + console.log("SETTING STEP SIZE"); this.worker.postMessage({ question: "step", value: this.stepSize }); }, updateCanvas(w, h) { const matrix = toRaw(this.grid).image._data; + const arr = Uint8ClampedArray.from( + new Array(this.width * this.height * 4) + ); - for (const i of range(0, this.size)._data) { - for (const j of range(0, this.size)._data) { - let index = matrix[this.my + i - floor(this.size / 2)]; - if (index) { - index = index[this.mx + j - floor(this.size / 2)]; - } - if (index) { - index[0] = 255; - } - } + // TODO: loop for full marker? + arr[4 * this.mx + 4 * this.my * w] = 255; + arr[4 * this.mx + 4 * this.my * w + 3] = 255; + + for (let p of this.paintBuffer) { + arr[4 * p[1] + 4 * p[0] * w] = 255; + arr[4 * p[1] + 4 * p[0] * w + 1] = 255; + + arr[4 * p[1] + 4 * p[0] * w + 3] = 128; + } + + for (let p of this.processBuffer) { + arr[4 * p[1] + 4 * p[0] * w + 1] = + 100 + (155 * (1 + sin(this.time / 5))) / 2; + arr[4 * p[1] + 4 * p[0] * w + 2] = 255; + + arr[4 * p[1] + 4 * p[0] * w + 3] = 128; } + const highlightImg = new ImageData(arr, w, h); + this.highlightContext.putImageData(highlightImg, 0, 0); + const img = this.debug ? new ImageData( Uint8ClampedArray.from( @@ -600,6 +724,27 @@ export default defineComponent({ value: 8 * this.stepSize, }); }, + markForPaint() { + const paintMat = zeros(this.width, this.height); + for ( + let i = Math.floor(this.mx - this.size / 2) + 1; + i < Math.floor(this.mx + this.size / 2) + 1; + i++ + ) { + for ( + let j = Math.floor(this.my - this.size / 2) + 1; + j < Math.floor(this.my + this.size / 2) + 1; + j++ + ) { + if (i >= 0 && i < this.width && j >= 0 && j < this.height) { + if (!paintMat._data[i][j]) { + this.paintBuffer.push([j, i]); + paintMat._data[i][j] = true; + } + } + } + } + }, selectTileset(path) { for (const c in toRaw(this.importedTilesets)) { console.log(c); @@ -638,13 +783,13 @@ export default defineComponent({ // dagNodes = []; this.tiles = []; const nodeArray = []; + for (const n in nodes) { invertedIndex[n] = i; nodeArray.push(nodes[n]); const node = nodes[n]; - if (node.paintable) { - this.tiles.push({ slot: n, color: node.color, value: i }); - } + this.tiles.push({ slot: n, color: node.color, value: i }); + // dagNodes.push(dagNode); i++; @@ -652,7 +797,7 @@ export default defineComponent({ // Build tile tree const treeNodeArray = []; - console.log(treeNodeArray); + console.log("exp", this.expanded); for (const n in nodes) { const nodeIndex = invertedIndex[n]; @@ -674,6 +819,10 @@ export default defineComponent({ } this.tileTree = [treeNodeArray[invertedIndex["root"]]]; + this.expanded.push(this.tileTree[0].key); + this.tileTree[0].children.forEach((c) => { + this.expanded.push(c.key); + }); // console.log(nodeIndex); @@ -712,8 +861,11 @@ export default defineComponent({ }, }, mounted() { + this.leftDrawerOpen = false; var canvas = document.getElementById("wfc"); + var highlightCanvas = document.getElementById("highlight"); this.context = canvas.getContext("2d"); + this.highlightContext = highlightCanvas.getContext("2d"); this.worker = new Worker( new URL("../assets/js/worker.js", import.meta.url), @@ -741,13 +893,27 @@ export default defineComponent({ } console.log(this.importedTilesets); + // Feels smoother, more regular + window.setInterval(() => { + this.updateCanvas(this.width, this.height); + }, 10); + this.worker.onmessage = ({ data: { grid, message } }) => { this.grid = grid; if (message === "doneUpdate") { this.canRedo = grid.canRedo; this.canUndo = grid.canUndo; + + // lmao h@x0r + if (this.processBuffer) { + this.processFlush++; + } + if (this.processFlush > 20) { + this.processFlush = 0; + this.processBuffer = []; + } } - this.updateCanvas(this.width, this.height); + // this.updateCanvas(this.width, this.height); if (message === "doneAuto" && this.autoCollapse) { this.worker.postMessage({ question: "auto" }); } @@ -759,24 +925,39 @@ export default defineComponent({ } }; + window.setInterval(() => { + this.time++; + }, 10); + let paintCanvas = (canvas, event) => { - this.worker.postMessage({ - question: "manual", - value: [this.mx, this.my, this.tile_index, this.size], - }); + // this.worker.postMessage({ + // question: "manual", + // value: [this.mx, this.my, this.tile_index, this.size, this.tool], + // }); }; - canvas.addEventListener("mousemove", (e) => { + + highlightCanvas.addEventListener("mousemove", (e) => { e.preventDefault(); - const rect = canvas.getBoundingClientRect(); + const rect = highlightCanvas.getBoundingClientRect(); + this.mxp = this.mx; + this.myp = this.my; this.mx = round( - ((this.width + 1) * (e.clientX - rect.left)) / canvas.offsetWidth - 1 + (this.width * (e.clientX - rect.left)) / highlightCanvas.offsetWidth - + 0.5 ); this.my = round( - ((this.height + 1) * (e.clientY - rect.top)) / canvas.offsetHeight - 1 + (this.height * (e.clientY - rect.top)) / highlightCanvas.offsetHeight - + 0.5 ); - if (this.leftMouseDown && this.tool == 0) { - paintCanvas(canvas, e); + if (this.mx !== this.mxp || this.my !== this.myp) { + if (this.leftMouseDown) { + this.markForPaint(); + } + + // if (this.leftMouseDown && [0, 2].includes(this.tool)) { + // // paintCanvas(canvas, e); + // } } }); @@ -792,25 +973,35 @@ export default defineComponent({ // canvas.addEventListener("mouseout", (e) => { // this.leftMouseDown = false; // }); - canvas.addEventListener("mousedown", (e) => { + highlightCanvas.addEventListener("mousedown", (e) => { e.preventDefault(); if (e.button == 0) { this.leftMouseDown = true; - this.worker.postMessage({ question: "clear" }); + this.markForPaint(); + + // this.worker.postMessage({ question: "clear" }); this.checkpoint(); - this.worker.postMessage({ question: "lock" }); + // this.worker.postMessage({ question: "lock" }); - if (this.tool == 0) { - paintCanvas(canvas, e); - } else if (this.tool == 1) { - this.worker.postMessage({ question: "info", value: [] }); - } + // if ([0, 2].includes(this.tool)) { + // paintCanvas(canvas, e); + // } else if (this.tool == 1) { + // this.worker.postMessage({ question: "info", value: [] }); + // } } }); document.addEventListener("mouseup", (e) => { if (e.button == 0) { this.leftMouseDown = false; - this.worker.postMessage({ question: "unlock" }); + if (this.paintBuffer.length > 0) { + this.worker.postMessage({ + question: "paint", + value: [this.tile_index], + cells: flatten(toRaw(this.paintBuffer)), + }); + } + this.processBuffer = this.paintBuffer; + this.paintBuffer = []; } }); window.addEventListener("keydown", (e) => { @@ -858,6 +1049,7 @@ export default defineComponent({ if (e.key === "s") { this.oneStep(); } + for (const i of range(1, this.snapshotCount + 1)) { if (e.key === `${i.value}`) { this.setSnapshot(i.value - 1); @@ -867,7 +1059,7 @@ export default defineComponent({ // TOUCH SUPPORT // TODO: Extract common elements to methods, so mouse/touch can use the same interface - canvas.addEventListener("touchstart", (e) => { + highlightCanvas.addEventListener("touchstart", (e) => { if (e.touches.length === 1) { this.leftMouseDown = true; this.worker.postMessage({ question: "clear" }); @@ -882,27 +1074,27 @@ export default defineComponent({ } return false; }); - canvas.addEventListener("touchend", (e) => { + highlightCanvas.addEventListener("touchend", (e) => { this.leftMouseDown = false; this.worker.postMessage({ question: "unlock" }); }); - canvas.addEventListener("touchmove", (e) => { + highlightCanvas.addEventListener("touchmove", (e) => { e.preventDefault(); if (e.touches) { const touch = e.touches[0]; - const rect = canvas.getBoundingClientRect(); + const rect = highlightCanvas.getBoundingClientRect(); this.mx = round( ((this.width + 1) * (touch.clientX - rect.left)) / - canvas.offsetWidth - + highlightCanvas.offsetWidth - 1 ); this.my = round( ((this.height + 1) * (touch.clientY - rect.top)) / - canvas.offsetHeight - + highlightCanvas.offsetHeight - 1 ); - if (this.leftMouseDown && this.tool == 0) { + if (this.leftMouseDown && [0, 2].includes(this.tool)) { paintCanvas(canvas, e); } } @@ -922,6 +1114,8 @@ export default defineComponent({ ); } + this.setStepSize(); + console.log("MOUNTED"); }, }); diff --git a/yarn.lock b/yarn.lock index ca1c76a..2def70a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1522,7 +1522,7 @@ ee-first@1.1.1: "eigen@https://github.com/Archer6621/eigen-js": version "0.2.2" - resolved "https://github.com/Archer6621/eigen-js#02c5faa738af884577256e96f6fed30083d36b47" + resolved "https://github.com/Archer6621/eigen-js#a646f386231e59a53b9c8bef90d58279e20e1d7c" dependencies: "@types/hashmap" "^2.3.1" hashmap "^2.4.0"