UNPKG

three-wfc

Version:

A blazing fast Wave Function Collapse engine for three.js, built for real-time 2D, 2.5D, and 3D procedural world generation at scale.

963 lines (961 loc) 28.6 kB
var F = Object.defineProperty; var H = (p, t, s) => t in p ? F(p, t, { enumerable: !0, configurable: !0, writable: !0, value: s }) : (p[t] = s); var c = (p, t, s) => H(p, typeof t != "symbol" ? t + "" : t, s); import { Vector2 as $, Color as O } from "three"; const L = 4, v = 0, D = 1, R = 2, _ = 32, z = 5, U = ["🇵🇸 TOP 🇵🇸", "🔻 BOTTOM 🔻", "LEFT", "RIGHT", "FRONT", "BACK"]; function G(p, t, s) { return function (e) { const i = (e % t) + s.x, o = ((Math.floor(e / t) + s.y) << 16) ^ i; let n = p + Math.imul(o, 2654435769); return ( (n = Math.imul(n ^ (n >>> 15), n | 1)), (n ^= n + Math.imul(n ^ (n >>> 7), n | 61)), ((n ^ (n >>> 14)) >>> 0) / 4294967296 ); }; } const w = 1; class K { /** * Creates a new optimized Min Heap with a key-to-position map. * Assumes keys will be integers in the range [0, maxElements - 1]. * @param count The maximum number of elements the heap can hold, * also defining the maximum key value + 1. */ constructor(t) { c(this, "keys"); c(this, "entropy"); c(this, "keyToPos"); c(this, "count"); (this.count = 0), (this.keys = new Uint32Array(t + w)), (this.entropy = new Float32Array(t + w)), (this.keyToPos = new Int32Array(t).fill(-1)); } /** Number of elements currently in the heap. */ get size() { return this.count; } /** Is the heap empty? */ isEmpty() { return this.count === 0; } /** * Reads the current entropy associated with a given key. * Returns Positive Infinity if the key is not in the heap or out of range. * * @param key The key to look up. * @returns The entropy (entropy) or Number.POSITIVE_INFINITY. */ read(t) { const s = this.keyToPos[t]; return s !== -1 ? this.entropy[s] : Number.POSITIVE_INFINITY; } /** * Adds a key with a given entropy to the heap or updates it if it exists. * Throws an error if the heap is full and the key is new. * * @param key The key to add or update. * @param entropy The entropy associated with the key. */ push(t, s) { const e = this.count + w; (this.keys[e] = t), (this.entropy[e] = s), (this.keyToPos[t] = e), this.count++, this.bubbleUp(e); } /** * Adds a key with a given entropy to the heap or updates it if it exists. * Throws an error if the heap is full and the key is new. * * @param key The key to add or update. * @param entropy The entropy associated with the key. */ put(t, s) { this.update(t, s) || this.push(t, s); } /** * Explicitly updates the entropy of an existing key. Does nothing if the key isn't present. * * @param key The key whose entropy to update. * @param newEntropy The new entropy value. */ update(t, s) { const e = this.keyToPos[t]; if (e === -1) return !1; const i = this.entropy[e]; return ( (this.entropy[e] = s), s < i ? this.bubbleUp(e) : this.bubbleDown(e), !0 ); } /** * Removes and returns the key with the minimum entropy. * Returns undefined if the heap is empty. * * @returns The key with the minimum entropy, or undefined. */ pop() { if (!this.count) return null; const t = this.keys[w]; if (((this.keyToPos[t] = -1), this.count > 1)) { const s = this.count + w - 1, e = this.keys[s]; (this.keys[w] = e), (this.entropy[w] = this.entropy[s]), (this.keyToPos[e] = w), this.count--, this.bubbleDown(w); } else this.count--; return t; } /** * Returns the minimum entropy without removing the element. * Returns undefined if the heap is empty. */ peek() { return this.count !== 0 && this.entropy[w]; } /** * Returns the key with the minimum entropy without removing it. * Returns undefined if the heap is empty. */ peekKey() { return this.count !== 0 && this.keys[w]; } /** * Removes an element associated with a specific key from the heap. * Returns true if the key was found and removed, false otherwise. * * @param key The key of the element to remove. * @returns True if removal was successful, false if the key was not found. */ remove(t) { const s = this.keyToPos[t]; if (s === -1) return !1; this.keyToPos[t] = -1; const e = this.count + w - 1; if (s === e) this.count--; else if (this.count > 1) { const i = this.keys[e], r = this.entropy[e]; (this.keys[s] = i), (this.entropy[s] = r), (this.keyToPos[i] = s), this.count--; const o = s >>> 1; s > w && this.entropy[s] < this.entropy[o] ? this.bubbleUp(s) : this.bubbleDown(s); } else this.count--; return !0; } clear() { (this.count = 0), this.keyToPos.fill(-1); } /** Swaps two elements in the heap arrays and updates the map */ swap(t, s) { const e = this.keys, i = this.entropy, r = this.keyToPos, o = e[t], n = e[s], h = i[t], l = i[s]; (e[t] = n), (e[s] = o), (i[t] = l), (i[s] = h), (r[o] = s), (r[n] = t); } /** Bubble an item up until its heap property is satisfied. */ bubbleUp(t) { const s = this.entropy, e = s[t]; for (; t > w; ) { const i = t >>> 1; if (s[i] <= e) break; this.swap(t, i), (t = i); } } /** Bubble an item down until its heap property is satisfied. */ bubbleDown(t) { const s = this.entropy, e = s[t], i = this.count + w; for (;;) { const r = t << 1, o = r + 1; let n = -1; if ((r < i && s[r] < e && (n = r), o < i)) { const h = s[o], l = n === -1 ? e : s[n]; h < l && (n = o); } if (n === -1) break; this.swap(t, n), (t = n); } } } const E = new Uint8Array(256); for (let p = 0; p < 256; p++) E[p] = (p & 1) + E[p >> 1]; const B = (p) => E[p & 255] + E[(p >> 8) & 255] + E[(p >> 16) & 255] + E[(p >> 24) & 255]; class W { constructor(t, s) { c(this, "array"); c(this, "count"); c(this, "stride"); c(this, "maskLength"); c(this, "isSingleChunk"); c(this, "tiles"); c(this, "unionMask"); c(this, "outputIndices"); c(this, "lastChunkMask"); this.tiles = s; const e = s.count; (this.count = s.count), (this.stride = Math.ceil(e / _)), (this.maskLength = e), (this.isSingleChunk = this.stride === 1), (this.array = new Uint32Array(t * this.stride)); const i = e % _; (this.lastChunkMask = i === 0 ? 4294967295 : (1 << i) - 1), (this.outputIndices = new Uint16Array(this.maskLength)), (this.unionMask = new Uint32Array(this.stride)); } /** * Sets a specific bit. * @param index The index of the cell * @param position The position of the bit to set/clear */ flag(t, s) { if (this.isSingleChunk) { this.array[t] |= 1 << s; return; } const e = t * this.stride, i = s >>> z, r = s & (_ - 1); this.array[e + i] |= 1 << r; } /** * Collapse the cell's option to a single * * @param index * @param position */ collapse(t, s) { if (this.isSingleChunk) { this.array[t] = 1 << s; return; } const e = t * this.stride, i = s >>> z, r = s & (_ - 1), o = e + i; for (let n = 0; n < this.stride; n++) this.array[e + n] = 0; this.array[o] = 1 << r; } /** Enables all possible states (bits) for an item. */ enableAll(t) { if (this.isSingleChunk) this.array[t] = this.lastChunkMask; else { const s = t * this.stride, e = s + this.stride; this.array.fill(4294967295, s, e - 1), (this.array[e - 1] = this.lastChunkMask); } } /** Counts the number of enabled states (set bits). */ optionsCount(t) { if (this.isSingleChunk) return B(this.array[t] & this.lastChunkMask); let s = 0; const e = t * this.stride, i = e + this.stride, r = this.array; for (let o = e; o < i - 1; o++) s += B(r[o]); return (s += B(r[i - 1] & this.lastChunkMask)), s; } /** Returns a view (subarray) of the mask. */ getMask(t) { if (this.isSingleChunk) return this.array.subarray(t, t + 1); const s = t * this.stride; return this.array.subarray(s, s + this.stride); } /** * Gets the index of the single set bit, assuming the mask at 'index' * represents a collapsed state with exactly one option enabled. * Uses a fast clz32 method. * Returns -1 if the guarantee is violated (no bits set). * * @param index The index of the collapsed cell. * @returns The index of the single set bit, or -1 if none found (error). */ collapsedTile(t) { const s = this.array; if (this.isSingleChunk) { const e = s[t] & this.lastChunkMask; return e === 0 ? -1 : 31 - Math.clz32(e); } else { const e = t * this.stride, i = e + this.stride; for (let r = e; r < i; r++) { let o = s[r]; if ((r === i - 1 && (o &= this.lastChunkMask), o === 0)) continue; const n = (r - e) * _, h = 31 - Math.clz32(o); return n + h; } } return -1; } /** * Sets up a cell's neighboring constraints based on the current state of another cell. * * @param cellIdx The index of the reference cell * @param neighborIdx The index of the neighbor cell to be constrained * @param edgeIdx The edge index on the neighbor that connects to the reference cell * @param tiles The tile buffer containing compatibility information * @param tilesDefinitions The tile definitions (for debugging) * * @return * - `true`, to signal changes. * - `false`, nothing changed. * - `null`, to signal a zero remaining cells contradiction. */ propagate(t, s, e) { const i = this.tiles, r = this.getMask(t), o = this.stride, n = i.count, h = this.unionMask.fill(0); for (let l = 0; l < o; l++) { const a = r[l]; if (a === 0) continue; const u = l * _; let g = a; for (; g !== 0; ) { const f = g & -g, d = 31 - Math.clz32(f); g &= ~f; const I = u + d; if (I >= n) continue; const y = i.getEdgeMask(e, I); for (let k = 0; k < o; k++) h[k] |= y[k]; } } return this.intersect(s, h) ? (this.optionsCount(s) ? !0 : null) : !1; } /** * Performs bitwise AND intersection between the mask at the given index * and the provided mask. Modifies the internal mask in place. * * @param index The index of the mask to modify. * @param mask The mask to intersect with. */ intersect(t, s) { let e = !1; if (this.isSingleChunk) { const i = this.array[t], r = s[0] ?? 0, o = i & r, h = this.count % 32 !== 0 ? this.lastChunkMask : 4294967295; (this.array[t] = o), (e = (o & h) !== (i & h)); } else { const i = this.stride, r = t * i, o = this.array; for (let n = 0; n < i; ++n) { const h = r + n, l = o[h], a = s[n] ?? 0, u = l & a, d = n === i - 1 && this.count % 32 !== 0 ? this.lastChunkMask : 4294967295; (u & d) !== (l & d) && (e = !0), (o[h] = u); } } return e; } /** * Gets the indices of all set bits in the given mask. * @param index The index of the mask to check * @returns Array of indices of set bits, or null if no bits are set */ getIndices(t) { const s = this.outputIndices, e = this.array, i = this.maskLength; let r = 0; if (this.isSingleChunk) { const o = e[t] & this.lastChunkMask; if (o === 0) return null; r = this._extractSetBits(o, 0, i, s, r); } else { const o = this.stride, n = t * o, h = n + o; for (let l = n; l < h; l++) { let a = e[l]; if ((l === h - 1 && (a &= this.lastChunkMask), a === 0)) continue; const u = (l - n) * _; if (u >= i) break; r = this._extractSetBits(a, u, i, s, r); } } return r === 0 ? null : s.subarray(0, r); } /** * Helper method to extract set bit indices from a single chunk. * Uses bitwise operations for reliability and performance. * * @param chunk The 32-bit integer chunk to process. * @param baseBitPos The starting bit position offset for this chunk. * @param maskLength The overall mask length limit. * @param setIndices The array to store the found indices. * @param currentCount The current number of indices already found and stored. * @returns The updated count of indices found after processing this chunk. */ _extractSetBits(t, s, e, i, r) { for (; t !== 0; ) { const o = t & -t, n = 31 - Math.clz32(o), h = s + n; h < e && (i[r++] = h), (t &= ~o); } return r; } } const N = (p) => { let t = 2166136261; for (let s = 0, e = p.length; s < e; s++) { const i = `${p[s]}`; (t ^= i.length), (t = Math.imul(t, 16777619) >>> 0); for (let r = 0, o = i.length; r < o; r++) (t ^= i.charCodeAt(r)), (t = Math.imul(t, 16777619) >>> 0); } return t; }; class X { constructor(t, s = !0) { c(this, "count", 0); c(this, "weight"); c(this, "edges"); c(this, "tiles"); c(this, "initialEntropy", 0); for (let i = 0, r = t.length; i < r; i++) t.push(...t[i].transformClones()); const e = t.length; return ( (this.tiles = t), (this.count = e), (this.weight = new Float32Array(e)), (this.edges = Array.from({ length: s ? 4 : 6 }, () => new W(e, this))), this._initialize(), this ); } _initialize() { const t = this.count, s = this.tiles, e = this.edges, i = e.length, r = /* @__PURE__ */ new Map(), o = (l, a) => l * i + a; let n = 0, h = 0; for (let l = 0; l < t; l++) { const a = s[l].weight; (this.weight[l] = a), (n += a), (h += a * Math.log(a)); for (let u = 0; u < i; u++) r.set(o(l, u), N(s[l].edges[u])); } this.initialEntropy = Math.log(n) - h / n; for (let l = 0; l < t; l++) for (let a = 0; a < t; a++) for (let u = 0; u < i; u++) { const g = u ^ 1, f = r.get(o(l, u)), d = r.get(o(a, g)); f === d && e[u].flag(l, a); } } getWeight(t) { return this.weight[t]; } getEdgeMask(t, s) { return this.edges[t].getMask(s); } } class Y { constructor(t) { c(this, "buffer"); c(this, "bitset"); c(this, "size"); c(this, "tail"); (this.buffer = new Uint32Array(t)), (this.bitset = new Uint8Array(t)), (this.size = 0), (this.tail = 0); } push(t) { this.bitset[t] || ((this.buffer[this.tail++] = t), this.size++, (this.bitset[t] = 1)); } pop() { if (this.size === 0) return; const t = this.buffer[--this.tail]; return this.size--, (this.bitset[t] = 0), t; } reset() { return (this.tail = 0), (this.size = 0), this; } } class V { constructor(t, s, e, i, r = { x: 0, y: 0 }, o = 1e-5) { c(this, "count"); c(this, "tiles"); c(this, "collapsed"); c(this, "entropyHeap"); c(this, "options"); c(this, "rows"); c(this, "cols"); c(this, "seed"); c(this, "noise"); c(this, "origin"); c(this, "stackBuffer"); c(this, "random"); const n = s * e; (this.count = n), (this.cols = s), (this.rows = e), (this.seed = i), (this.noise = o), (this.origin = { ...r }), (this.random = i ? G(i, s, this.origin) : Math.random), (this.tiles = new X(t)), (this.options = new W(n, this.tiles)), (this.collapsed = new Int16Array(n)), (this.entropyHeap = new K(n)), (this.stackBuffer = new Y(n)); const h = this.tiles.initialEntropy, { options: l, collapsed: a, entropyHeap: u, random: g } = this; for (let f = 0; f < n; f++) (a[f] = -1), l.enableAll(f), u.push(f, h + g(f) * o); } get isCompleted() { return this.entropyHeap.isEmpty(); } get remainingCells() { return this.entropyHeap.size; } /** * * @returns */ collapse() { const t = this.entropyHeap.pop(); return t !== null && this._collapseCell(t); } /** * Repeatedly calls `collapse()` until all cells are collapsed or a contradiction occurs. * @returns `true` if the entire grid was successfully collapsed, `false` if a contradiction occurred. */ collapseAll() { for (; !this.entropyHeap.isEmpty(); ) if (!this.collapse()) return !this.entropyHeap.isEmpty(); return !0; } /** * Collapses a specific cell to a randomly chosen valid tile based on current options * and propagates the constraints. * * @param index - The index of the cell to collapse. * * @returns `true` if the cell was successfully collapsed and propagated, * `false` if the index is invalid, the cell is already collapsed, * the cell has no options (contradiction), or propagation fails. */ collapseCell(t) { return t < 0 || t >= this.count ? (console.error(`WFC CollapseCell: Invalid index ${t}.`), !1) : this.collapsed[t] !== -1 ? !0 : this.options.optionsCount(t) ? this._collapseCell(t) : (console.error( `WFC Contradiction: Attempting to collapse cell ${t} which already has no options.` ), !1); } /** * Returns the collapsed cell tile's index, or `-1` if un-collapsed * * @param index * @returns The collapsed cell tile's index, `-1` if un-collapsed. */ getCollapsedTile(t) { return this.collapsed[t]; } cellIndex({ x: t, y: s }) { return ( (t = this.origin.x + ((t + this.cols) % this.cols)), (s = this.origin.y + ((s + this.rows) % this.rows)), s * this.cols + t ); } cellPosition(t, s) { const e = t % this.cols, i = Math.floor(t / this.cols); return ( (s.x = (e - this.origin.x + this.cols) % this.cols), (s.y = (i - this.origin.y + this.rows) % this.rows), s ); } /** * Slides the grid logically by the given offset. * Cells that move "out" are discarded, and cells that move "in" are reset * to their initial un-collapsed state with entropy calculated based on the new origin. * Triggers propagation from the boundary cells adjacent to the reset area. * * @param offset - A Vector2Like object with x and y properties indicating the slide amount. * Positive y slides down (reset top rows), positive x slides right (reset left cols). */ offset({ x: t, y: s }) { const e = Math.round(t), i = Math.round(s); if (e === 0 && i === 0) return !0; (this.origin.x = (this.origin.x + e + this.cols) % this.cols), (this.origin.y = (this.origin.y + i + this.rows) % this.rows); const r = this.cols, o = this.rows, n = this.collapsed, h = this.options, l = this.entropyHeap, a = this.tiles.initialEntropy, u = this.noise, g = this.random, f = e > 0 ? (this.cols - e) % this.cols : 0, d = Math.abs(e), I = i > 0 ? (this.rows - i) % this.rows : 0, y = Math.abs(i), k = /* @__PURE__ */ new Set(); if (d > 0) for (let C = 0; C < d; C++) { const m = (f + C) % r; for (let b = 0; b < o; b++) { const M = b * r + m; (n[M] = -1), h.enableAll(M), l.put(M, a + g(M) * u); const S = (b - 1 + o) % o; if (Math.abs(S - b) === 1) { const T = S * r + m; n[T] !== -1 && k.add(T); } const x = (b + 1) % o; if (Math.abs(x - b) === 1) { const T = x * r + m; n[T] !== -1 && k.add(T); } const A = (m - 1 + r) % r; if (Math.abs(A - m) === 1) { const T = b * r + A; n[T] !== -1 && k.add(T); } const P = (m + 1) % r; if (Math.abs(P - m) === 1) { const T = b * r + P; n[T] !== -1 && k.add(T); } } } if (y > 0) for (let C = 0; C < y; C++) { const m = (I + C) % o, b = d > 0 ? d : 0; for (let M = b; M < r; M++) { const S = m * r + M; (n[S] = -1), h.enableAll(S), l.put(S, a + g(S) * u); } } for (const C of k) if (!this._propagate(C)) return ( console.error( `WFC Offset: Contradiction during propagation from boundary cell ${C}.` ), !1 ); return !0; } /** * Internal helper to perform the collapse logic for a specific cell index. * * @param index The index of the cell to collapse. * @returns `true` on success, `false` on failure (contradiction during choice or propagation). * @private */ _collapseCell(t) { const s = this.tiles, e = this.options.getIndices(t), i = e.length; let r = 0; for (let h = 0; h < i; h++) r += s.getWeight(e[h]); let o = this.random(t + i) * r, n = 0; for (let h = 0; h < i; h++) { const l = e[h]; if (((o -= s.getWeight(l)), o <= 0)) { n = l; break; } } return ( this.options.collapse(t, n), (this.collapsed[t] = n), this.entropyHeap.remove(t), this._propagate(t) ? (this.entropyHeap.peek() === 0 && this.collapse(), !0) : (console.error( `WFC Collapse Failed: Contradiction detected during propagation after collapsing cell ${t} to tile ${n}.` ), !1) ); } /** * Propagates constraints starting from a cell whose options have been reduced. * Uses an iterative approach with a stack and a Set to track items currently on the stack. * * @param cellIdx The index of the cell that triggered the propagation. * @returns `true` if propagation completed successfully, `false` if a contradiction was detected. */ _propagate(t) { const s = this.stackBuffer.reset(), e = this.options, i = this.cols, r = this.rows, o = this.collapsed; let n = t; for (; n !== void 0; ) { const h = n % i, l = Math.floor(n / i); for (let a = 0; a < L; a++) { let u = h, g = l; switch (a) { case v: if (l === 0) continue; g--; break; case D: if (l === r - 1) continue; g++; break; case R: if (h === 0) continue; u--; break; default: if (h === i - 1) continue; u++; break; } const f = g * i + u; if (o[f] !== -1) continue; const d = e.propagate(n, f, a); if (d) { this._computeEntropy(f), s.push(f); continue; } if (d === null) return ( console.error( `WFC Propagation Contradiction: Cell "${f}" (Neighbor of "${n}" on the "${ U[a ^ 1] }" edge) has no options left after propagation from cell ${n}.` ), this.entropyHeap.remove(n), !1 ); } n = s.pop(); } return !0; } /** Recomputes the Shannon entropy for a given cell */ _computeEntropy(t) { const s = this.options.getIndices(t), e = s.length; if (e === 1) { this.entropyHeap.update(t, 0); return; } let i = 0, r = 0; const o = this.tiles; for (let h = 0; h < e; h++) { const l = s[h], a = o.getWeight(l); (i += a), (r += a * Math.log(a)); } const n = Math.log(i) - r / i; this.entropyHeap.update(t, n + this.random(t) * this.noise); } } class J { constructor({ canvas: t, width: s, height: e, cellSize: i, seed: r, drawGrid: o, }) { c(this, "canvas"); c(this, "width"); c(this, "height"); c(this, "size"); // Cell size in pixels c(this, "drawGrid", !1); c(this, "wfcBuffer"); c(this, "seed"); c(this, "offset", new $()); // Optional drawing offset c(this, "tiles", []); // Original tiles added by the user c(this, "ctx"); (this.canvas = t), (this.width = s), (this.height = e), (this.size = i), (this.ctx = this.canvas.getContext("2d")), (this.canvas.width = s), (this.canvas.height = e), r && (this.seed = r), (this.drawGrid = !!o); } addTile(...t) { this.tiles.push(...t); } removeTile(...t) { t.forEach((s) => { const e = this.tiles.indexOf(s); ~e && this.tiles.splice(e, 1); }); } clear() { this.tiles.length = 0; } init() { const t = Math.ceil(this.width / this.size), s = Math.ceil(this.height / this.size); (this.canvas.width = t * this.size), (this.canvas.height = s * this.size), (this.wfcBuffer = new V(this.tiles, t, s, this.seed)); } collapseAll() { return this.wfcBuffer ? this.wfcBuffer.collapseAll() : (console.error("WFC not initialized. Call init() first."), !1); } collapse() { return this.wfcBuffer ? this.wfcBuffer.collapse() : (console.error("WFC not initialized. Call init() first."), !1); } collapseCell(t) { return this.wfcBuffer ? this.wfcBuffer.collapseCell(t) : (console.error("WFC not initialized. Call init() first."), !1); } /** * Draws the current state of the WFC grid onto the canvas. * Applies transformations (rotation, reflection) dynamically for debugging. */ draw() { const t = this.ctx, s = this.wfcBuffer, e = s.cols, i = s.rows, r = this.tiles, o = this.size, n = this.offset.x, h = this.offset.y, l = this.drawGrid; t.clearRect(0, 0, this.canvas.width, this.canvas.height), (t.strokeStyle = "#555"); for (let a = 0; a < i; a++) for (let u = 0; u < e; u++) { const g = a * e + u, f = u * o + n, d = a * o + h; if (s.collapsed[g] !== -1) { const I = s.getCollapsedTile(g), y = I !== -1 ? r[I] : null; if (y && y.image) { const k = y.image; if (y._rotation !== 0 || y._reflectX || y._reflectY) { t.save(); const m = f + o / 2, b = d + o / 2; t.translate(m, b), y._rotation > 0 && t.rotate((y._rotation * Math.PI) / 2), (y._reflectX || y._reflectY) && t.scale(y._reflectX ? -1 : 1, y._reflectY ? -1 : 1); const M = o / 2; k instanceof O ? ((t.fillStyle = `#${k.getHexString()}`), t.fillRect(-M, -M, o, o)) : t.drawImage(k, -M, -M, o, o), t.restore(); } else k instanceof O ? ((t.fillStyle = `#${k.getHexString()}`), t.fillRect(f, d, o, o)) : t.drawImage(k, f, d, o, o); l && t.strokeRect(f, d, o, o); } else (t.fillStyle = "magenta"), t.fillRect(f, d, o, o), console.error( `Collapsed cell [${u}, ${a}] (index ${g}) has invalid tile index ${I} or missing content.` ); } else { t.strokeRect(f, d, o, o); const I = s.options.optionsCount(g), y = s.tiles.count, k = I / y, C = Math.floor(50 + k * 100); (t.fillStyle = `rgba(${C}, ${C}, ${C}, 0.7)`), t.fillRect(f, d, o, o), (t.fillStyle = "white"), (t.textAlign = "center"), (t.textBaseline = "middle"), (t.font = `bold ${o * 0.4}px sans-serif`), t.fillText(I.toString(), f + o / 2, d + o / 2), (t.fillStyle = "#cccccc"), (t.textAlign = "left"), (t.textBaseline = "top"), (t.font = `${o * 0.2}px sans-serif`), t.fillText( g.toString(), // Draw the cell's linear index f + 2, // Small padding from the left edge d + 2 // Small padding from the top edge ); } } } } export { J as WFC2D }; //# sourceMappingURL=three-wfc.js.map