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
JavaScript
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