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.
220 lines (186 loc) • 6.25 kB
text/typescript
import { copyReverse } from "./utils/copyReverse";
/**
* Represents a single tile type in the Wave Function Collapse algorithm.
* Stores its visual content, weight, edge connection rules (tags),
* allowed transformations (rotation, reflection), and provides methods
* to generate transformed clones.
*/
export class WFCTile2D {
/** */
name: string = "";
/** Visual representation (THREE.Color, Image, or Canvas) */
image: HTMLImageElement | HTMLCanvasElement;
/** Edge connection tags for the top side */
top: (string | number)[];
/** Edge connection tags for the right side */
right: (string | number)[];
/** Edge connection tags for the bottom side */
bottom: (string | number)[];
/** Edge connection tags for the left side */
left: (string | number)[];
/** Convenience array holding edge tags in [UP, DOWN, LEFT, RIGHT] order */
edges: (string | number)[][];
/** Allowed rotations (90, 180, 270 degrees clockwise) */
rotations: (1 | 2 | 3)[];
/** Whether reflection along the vertical axis (X-reflection) is allowed */
reflectX: boolean;
/** Whether reflection along the horizontal axis (Y-reflection) is allowed */
reflectY: boolean;
/** Optional function for more complex placement rules beyond edge matching */
rules?: () => boolean;
/** The rotation applied to this specific tile instance (0 = none) */
_rotation: 0 | 1 | 2 | 3 = 0;
/** Whether this specific tile instance is reflected horizontally */
_reflectX: boolean = false;
/** Whether this specific tile instance is reflected vertically */
_reflectY: boolean = false;
private _weight: number = 10;
/**
* Creates a new WFCTile instance.
* @param config - Configuration object for the tile.
*/
constructor({
content,
weight = 1,
rotations = [],
top = [],
right = [],
bottom = [],
left = [],
reflectX = false,
reflectY = false,
rules,
name,
}: {
content: HTMLImageElement | HTMLCanvasElement;
name?: string;
weight?: number;
rotations?: (1 | 2 | 3)[];
top?: (string | number)[];
right?: (string | number)[];
bottom?: (string | number)[];
left?: (string | number)[];
reflectX?: boolean;
reflectY?: boolean;
rules?: () => boolean;
}) {
this.name = name || "";
this.image = content;
this.weight = weight;
this.top = top;
this.bottom = bottom;
this.left = left;
this.right = right;
this.edges = [this.top, this.bottom, this.left, this.right];
this.rotations = rotations;
this.reflectX = reflectX;
this.reflectY = reflectY;
this.rules = rules;
}
get weight(): number {
return this._weight;
}
set weight(value: number) {
this._weight = value > 0 ? value : 1;
}
/**
* Generates all unique transformed versions (clones) of this tile
* based on the allowed rotations and reflections defined in its properties.
* Does not generate combinations (e.g., rotated *and* reflected).
* @returns An array of WFCTile instances representing the transformations.
*/
transformClones(): WFCTile2D[] {
const clones: WFCTile2D[] = [];
[...new Set(this.rotations)].forEach((rotation) =>
clones.push(this._rotate(rotation))
);
if (this.reflectX) clones.push(this._reflect("x"));
if (this.reflectY) clones.push(this._reflect("y"));
return clones;
}
/**
* Performs a deep copy of properties from a source tile into this tile.
* Ensures arrays and Color objects are cloned, not just referenced.
* @param source - The WFCTile to copy from.
* @returns This WFCTile instance for chaining.
*/
copy(source: WFCTile2D): this {
Object.assign(this, source);
this.top = [...source.top];
this.bottom = [...source.bottom];
this.left = [...source.left];
this.right = [...source.right];
this.edges = [this.top, this.bottom, this.left, this.right];
this.rotations = [...source.rotations];
return this;
}
/**
* Creates a new WFCTile instance that is a deep copy of this one.
* @returns A new WFCTile instance.
*/
clone(): WFCTile2D {
return new WFCTile2D({ content: this.image }).copy(this);
}
/**
* Creates a new WFCTile instance rotated clockwise by the specified amount.
* Updates the edge tags accordingly.
* @param rotation - The rotation amount (1 = 90°, 2 = 180°, 3 = 270°).
* @returns A new, rotated WFCTile clone.
* @private
*/
private _rotate(rotation: 1 | 2 | 3): WFCTile2D {
const clone = this.clone();
clone._rotation = rotation;
const { top, bottom, left, right } = this;
switch (rotation) {
case 1:
clone.top = copyReverse(left);
clone.right = [...top];
clone.bottom = copyReverse(right);
clone.left = [...bottom];
break;
case 2:
clone.top = copyReverse(bottom);
clone.right = copyReverse(left);
clone.bottom = copyReverse(top);
clone.left = copyReverse(right);
break;
case 3:
clone.top = [...right];
clone.right = copyReverse(bottom);
clone.bottom = [...left];
clone.left = copyReverse(top);
break;
}
clone.name += `-rot-${rotation}`;
clone.edges = [clone.top, clone.bottom, clone.left, clone.right];
return clone;
}
/**
* Creates a new WFCTile instance reflected across the specified axis.
* Updates the edge tags accordingly.
* @param axis - The axis of reflection ('x' for vertical axis, 'y' for horizontal axis).
* @returns A new, reflected WFCTile clone.
* @private
*/
private _reflect(axis: "x" | "y"): WFCTile2D {
const clone = this.clone();
const { top, bottom, left, right } = this;
if (axis === "x") {
clone._reflectX = true;
clone.top = copyReverse(top);
clone.right = [...left];
clone.bottom = copyReverse(bottom);
clone.left = [...right];
} else {
clone._reflectY = true;
clone.top = [...bottom];
clone.right = copyReverse(right);
clone.bottom = [...top];
clone.left = copyReverse(left);
}
clone.name += `-ref-${axis}`;
clone.edges = [clone.top, clone.bottom, clone.left, clone.right];
return clone;
}
}