@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
659 lines (625 loc) • 23.4 kB
text/typescript
import {WFCTilesCollection} from './WFCTilesCollection';
import {configTilesStats, TileConfigStats, solidTilesStats} from './WFCCommon';
import {EMPTY_TILE_ID, GRID_BORDER_ID, GRID_BORDER_SIDE_NAME} from './WFCConstant';
import {TileConfig, tileConfigToString, ERRORED_TILE_CONFIG, tileConfigsToString} from './WFCTileConfig';
import {CoreWFCTileAttribute, WFCQuadAttribute} from './WFCAttributes';
import {Object3D,TypedArray} from 'three';
import {NeighbourData} from '../geometry/modules/quad/graph/QuadGraph';
import {QuadNode} from '../geometry/modules/quad/graph/QuadNode';
import {Attribute} from '../geometry/Attribute';
import {pushOnArrayAtEntry, popFromArrayAtEntry, addToSetAtEntry} from '../MapUtils';
import {arrayUniq, sample, spliceSample} from '../ArrayUtils';
import {setToArray} from '../SetUtils';
import {NeighbourIndex, CCW_HALF_EDGE_SIDES} from '../geometry/modules/quad/graph/QuadGraphCommon';
import {mod} from '../math/_Module';
import {QuadObject} from '../geometry/modules/quad/QuadObject';
import {WFCFloorGraph} from './WFCFloorGraph';
import {isQuadNodeSolveAllowed, quadPrimitiveFloorIndex} from './WFCUtils';
import {QuadPrimitive} from '../geometry/modules/quad/QuadPrimitive';
import {WFCTileConfigSampler} from './WFCTileConfigSampler';
import {PrimitiveStringAttribute} from '../geometry/entities/primitive/PrimitiveAttribute';
import {CoreObjectType, ObjectContent} from '../geometry/ObjectContent';
import {corePrimitiveClassFactory} from '../geometry/CoreObjectFactory';
const _neighbourData: NeighbourData = {
quadNode: null,
neighbourIndex: null,
};
const _configStats: TileConfigStats = {
solid: 0,
empty: 0,
};
const _entropiesSet: Set<number> = new Set();
const _sortedEntropies: number[] = [];
interface WFCSolverOptions {
tileAndRuleObjects: Object3D[];
quadObject: QuadObject;
// height: number;
maxResolvedQuadsCount: number;
}
interface WFCSolverProcessOptions {
stepsCount: number;
quadSeed: number;
configSeed: number;
}
interface WFCSolverUpdateOptions extends WFCSolverProcessOptions {
object: ObjectContent<CoreObjectType>;
floorId: number;
quadId: number;
tileId: string;
rotation: NeighbourIndex;
}
const _tileConfigSampler = new WFCTileConfigSampler();
export class WFCSolver {
private _resolvedQuadsCount: number = 0;
private _maxResolvedQuadsCount: number;
private _stepsCount: number = 0;
private _tilesCollection: WFCTilesCollection;
private _quadPositionArrays: TypedArray[] = [];
private _lowestEntropy: number = Number.POSITIVE_INFINITY;
private _quadNodeByEntropy: Map<number, QuadNode[]> = new Map();
private _floorGraphs: WFCFloorGraph[] = [];
private _quadIndicesByFloorIndex: Map<number, Set<number>> = new Map();
private _floorGraphIndexByQuadNode: Map<QuadNode, number> = new Map();
private _samplingWithWeightRequired: boolean = false;
private _solvedTileConfigs: string[];
private _allTileConfigs: TileConfig[] = [];
//
constructor(options: WFCSolverOptions) {
const {quadObject} = options;
// this._height = options.height;
this._maxResolvedQuadsCount = options.maxResolvedQuadsCount;
// get tile configs
this._tilesCollection = new WFCTilesCollection(options);
const tiles = this._tilesCollection.tiles();
// const allTileConfigs: TileConfig[] = [];
for (const tile of tiles) {
const tileId = CoreWFCTileAttribute.getTileId(tile);
if (tileId == EMPTY_TILE_ID) {
this._allTileConfigs.push({tileId, rotation: 0});
} else {
this._allTileConfigs.push({tileId, rotation: 0});
this._allTileConfigs.push({tileId, rotation: 1});
this._allTileConfigs.push({tileId, rotation: 2});
this._allTileConfigs.push({tileId, rotation: 3});
}
}
// check if the tiles have different weights
const weights: number[] = [];
arrayUniq(
tiles.map((tile) => CoreWFCTileAttribute.getWeight(tile)),
weights
);
if (weights.length > 1) {
this._samplingWithWeightRequired = true;
}
// get floors count
const primitivesCount = QuadPrimitive.entitiesCount(quadObject);
for (let i = 0; i < primitivesCount; i++) {
const floorIndex = quadPrimitiveFloorIndex(quadObject, i);
addToSetAtEntry(this._quadIndicesByFloorIndex, floorIndex, i);
}
// create quad object attributes
const solvedTileConfigs = new Array(primitivesCount).fill('');
const solvedTileIdsAttribute: PrimitiveStringAttribute = {
isString: true,
array: solvedTileConfigs,
itemSize: 1,
};
QuadPrimitive.addAttribute(quadObject, WFCQuadAttribute.SOLVED_TILE_CONFIGS, solvedTileIdsAttribute);
// create graphs
this._quadIndicesByFloorIndex.forEach((quadIndices, floorIndex) => {
this._quadPositionArrays[floorIndex] = quadObject.geometry.attributes[Attribute.POSITION].array;
const floorGraph = new WFCFloorGraph(quadObject, floorIndex);
this._floorGraphs.push(floorGraph);
for (const quadIndex of quadIndices) {
this._setupQuadNode(floorGraph, quadIndex, this._allTileConfigs);
}
});
this._solvedTileConfigs = this._setSolvedTileConfigs(quadObject);
}
private _setSolvedTileConfigs(object: ObjectContent<CoreObjectType>): string[] {
const primitiveAttributes = corePrimitiveClassFactory(object).attributes(object);
if (!primitiveAttributes) {
console.error('no primitive attributes found', object);
return [];
}
const solvedTileConfigAttribute = primitiveAttributes[WFCQuadAttribute.SOLVED_TILE_CONFIGS];
if (!solvedTileConfigAttribute) {
console.error(`primitive attribute ${WFCQuadAttribute.SOLVED_TILE_CONFIGS} not found`);
return [];
}
return (this._solvedTileConfigs = solvedTileConfigAttribute.array as string[]);
}
// objects(): Object3D[] {
// return this._objects;
// }
private _setupQuadNode(floorGraph: WFCFloorGraph, quadIndex: number, allTileConfigs: TileConfig[]) {
if (!isQuadNodeSolveAllowed(floorGraph.quadObject, quadIndex)) {
return;
}
const {quadNode, quadTileConfigs} = floorGraph.setupQuadNode(quadIndex, allTileConfigs);
this._floorGraphIndexByQuadNode.set(quadNode, floorGraph.floorIndex);
this._initQuadNodeEntropyCache(quadNode, quadTileConfigs);
}
private _resetQuadNode(quadNode: QuadNode) {
const floorGraph = this._floorGraph(quadNode);
const {quadTileConfigs} = floorGraph.resetQuadNode(quadNode, this._allTileConfigs);
this._initQuadNodeEntropyCache(quadNode, quadTileConfigs);
}
private _initQuadNodeEntropyCache(quadNode: QuadNode, quadTileConfigs: TileConfig[]) {
const entropy = quadTileConfigs.length;
pushOnArrayAtEntry(this._quadNodeByEntropy, entropy, quadNode);
if (entropy < this._lowestEntropy) {
this._lowestEntropy = entropy;
}
}
//
//
//
//
//
process(options: WFCSolverProcessOptions, comparableQuadNodes?: Set<QuadNode>) {
const {stepsCount, quadSeed, configSeed} = options;
if (stepsCount < 0) {
let result = this.step(quadSeed, configSeed, comparableQuadNodes);
while (result != false) {
result = this.step(quadSeed, configSeed, comparableQuadNodes);
}
} else {
for (let i = 0; i < stepsCount; i++) {
const result = this.step(quadSeed, configSeed, comparableQuadNodes);
if (result == false) {
// stop loop if step has no quad to continue
break;
}
}
}
this._commitConfigsAttributes();
}
private step(quadSeed: number, configSeed: number, comparableQuadNodes?: Set<QuadNode>): boolean | undefined {
// const isFirstStep = this._stepsCount == 0;
this._stepsCount++;
const quadNode = this._quadNodeWithLowestEntropy(quadSeed + this._stepsCount);
if (!quadNode) {
return false;
}
const floorGraph = this._floorGraph(quadNode);
const allowedConfigs = floorGraph.allowedTileConfigsForQuadNode(quadNode); //this._allowedTileConfigsByQuadId.get(quadNode.id);
if (!(allowedConfigs && allowedConfigs.length > 0)) {
return;
}
const updatedEntropy = this._reduceEntropyWithCache(quadNode, allowedConfigs, comparableQuadNodes);
if (!(allowedConfigs && allowedConfigs.length > 0)) {
popFromArrayAtEntry(this._quadNodeByEntropy, updatedEntropy, quadNode);
this._placeErrorTileObject(quadNode);
return;
}
configTilesStats(allowedConfigs, _configStats);
const config =
_configStats.solid == 0
? allowedConfigs[0]
: this._selectConfig(allowedConfigs, configSeed + this._stepsCount);
floorGraph.setAllowedTileConfigsForQuadNode(quadNode, [config]); //this._allowedTileConfigsByQuadId.set(quadNode.id, [config]);
this._approveConfigForQuad(quadNode, config);
this._updateNeighboursEntropy(quadNode);
}
addSoftContraint(options: WFCSolverUpdateOptions) {
const {object, floorId, quadId, tileId, rotation} = options;
this._setSolvedTileConfigs(object);
const floorGraph = this._floorGraphs[floorId];
const quadNode = floorGraph.quadNodeFromId(quadId);
if (!quadNode) {
return;
}
const config: TileConfig = {
tileId,
rotation,
};
floorGraph.setAllowedTileConfigsForQuadNode(quadNode, [config]);
this._approveConfigForQuad(quadNode, config);
// this._updateNeighboursEntropy(quadNode);
//
const resetQuadNodeIds: Set<QuadNode> = new Set([quadNode]);
this._resetAndUpdateNeighboursEntropy(quadNode, resetQuadNodeIds);
// step
this.process(options, resetQuadNodeIds);
}
//
//
//
//
//
private _solveReachMaxCount() {
return this._maxResolvedQuadsCount >= 0 && this._resolvedQuadsCount >= this._maxResolvedQuadsCount;
}
private _selectConfig(allowedConfigs: TileConfig[], seed: number): TileConfig {
if (this._samplingWithWeightRequired) {
_tileConfigSampler.setItemsAndWeights(
allowedConfigs,
allowedConfigs.map((config) =>
CoreWFCTileAttribute.getWeight(this._tilesCollection.tile(config.tileId)!)
)
);
return _tileConfigSampler.sample(seed);
} else {
return sample(solidTilesStats(allowedConfigs), seed)!;
}
}
protected _commitConfigsAttributes() {
this._quadNodeByEntropy.forEach((quadNodes, entropy) => {
for (const quadNode of quadNodes) {
const floorGraph = this._floorGraph(quadNode);
const allowedConfigs = floorGraph.allowedTileConfigsForQuadNode(quadNode);
if (allowedConfigs) {
this._commitConfigsAttributeToQuadPrimitive(quadNode, allowedConfigs);
}
}
});
}
private _placeErrorTileObject(quadNode: QuadNode) {
this._commitConfigAttributeToQuadPrimitive(quadNode, ERRORED_TILE_CONFIG);
this._resolvedQuadsCount++;
}
private _approveConfigForQuad(quadNode: QuadNode, config: TileConfig) {
this._commitConfigAttributeToQuadPrimitive(quadNode, config);
this._resolvedQuadsCount++;
}
private _commitConfigsAttributeToQuadPrimitive(quadNode: QuadNode, tileConfigs: TileConfig[]) {
this._solvedTileConfigs[quadNode.id] = tileConfigsToString(tileConfigs);
}
private _commitConfigAttributeToQuadPrimitive(quadNode: QuadNode, tileConfig: TileConfig) {
this._solvedTileConfigs[quadNode.id] = tileConfigToString(tileConfig);
}
private _updateNeighboursEntropy(startQuadNode: QuadNode) {
const stack: QuadNode[] = [startQuadNode];
while (stack.length > 0 && !this._solveReachMaxCount()) {
const currentQuad = stack.pop()!;
const floorIndex = this._floorGraphIndexByQuadNode.get(currentQuad)!;
const floorGraph = this._floorGraphs[floorIndex];
// update horizontal neighbours
for (let i: NeighbourIndex = 0; i < 4; i++) {
floorGraph.neighbourData(currentQuad.id, i as NeighbourIndex, _neighbourData);
if (
_neighbourData.quadNode &&
floorGraph.allowedTileConfigsForQuadNode(_neighbourData.quadNode)!.length > 1
) {
this._updateQuadEntropy(_neighbourData.quadNode, stack);
}
}
// update above
const floorGraphAbove = this._floorGraphs[floorIndex + 1];
if (floorGraphAbove) {
const aboveQuad = floorGraphAbove.quadNodeFromOtherQuadNode(currentQuad);
if (aboveQuad) {
if (floorGraphAbove.allowedTileConfigsForQuadNode(aboveQuad)!.length > 1) {
this._updateQuadEntropy(aboveQuad, stack);
}
}
}
// update below
const floorGraphBelow = this._floorGraphs[floorIndex - 1];
if (floorGraphBelow) {
const belowQuad = floorGraphBelow.quadNodeFromOtherQuadNode(currentQuad);
if (belowQuad) {
if (floorGraphBelow.allowedTileConfigsForQuadNode(belowQuad)!.length > 1) {
this._updateQuadEntropy(belowQuad, stack);
}
}
}
}
}
private _resetAndUpdateNeighboursEntropy(startQuadNode: QuadNode, resetQuadNodeIds: Set<QuadNode>) {
const stack: QuadNode[] = [startQuadNode];
while (stack.length > 0 && !this._solveReachMaxCount()) {
const currentQuad = stack.pop()!;
const floorIndex = this._floorGraphIndexByQuadNode.get(currentQuad)!;
const floorGraph = this._floorGraphs[floorIndex];
// update horizontal neighbours
for (let i: NeighbourIndex = 0; i < 4; i++) {
floorGraph.neighbourData(currentQuad.id, i as NeighbourIndex, _neighbourData);
if (_neighbourData.quadNode) {
this._resetQuadNodeEntropyIfNotReset(_neighbourData.quadNode, resetQuadNodeIds);
if (floorGraph.allowedTileConfigsForQuadNode(_neighbourData.quadNode)!.length > 1) {
this._updateQuadEntropy(_neighbourData.quadNode, stack, resetQuadNodeIds);
}
}
}
// update above
const floorGraphAbove = this._floorGraphs[floorIndex + 1];
if (floorGraphAbove) {
const aboveQuad = floorGraphAbove.quadNodeFromOtherQuadNode(currentQuad);
if (aboveQuad) {
this._resetQuadNodeEntropyIfNotReset(aboveQuad, resetQuadNodeIds);
if (floorGraphAbove.allowedTileConfigsForQuadNode(aboveQuad)!.length > 1) {
this._updateQuadEntropy(aboveQuad, stack, resetQuadNodeIds);
}
}
}
// update below
const floorGraphBelow = this._floorGraphs[floorIndex - 1];
if (floorGraphBelow) {
const belowQuad = floorGraphBelow.quadNodeFromOtherQuadNode(currentQuad);
if (belowQuad) {
this._resetQuadNodeEntropyIfNotReset(belowQuad, resetQuadNodeIds);
if (floorGraphBelow.allowedTileConfigsForQuadNode(belowQuad)!.length > 1) {
this._updateQuadEntropy(belowQuad, stack, resetQuadNodeIds);
}
}
}
}
}
private _updateQuadEntropy(quadNode: QuadNode, stack: QuadNode[], comparableQuadNodes?: Set<QuadNode>) {
if (this._solveReachMaxCount()) {
return;
}
const floorGraph = this._floorGraph(quadNode);
const allowedTileConfigs = floorGraph.allowedTileConfigsForQuadNode(quadNode)!;
const updatedEntropy = this._reduceEntropyWithCache(quadNode, allowedTileConfigs, comparableQuadNodes);
if (updatedEntropy === undefined) {
return;
}
stack.push(quadNode);
switch (updatedEntropy) {
case 0: {
popFromArrayAtEntry(this._quadNodeByEntropy, updatedEntropy, quadNode);
this._placeErrorTileObject(quadNode);
return;
}
case 1: {
popFromArrayAtEntry(this._quadNodeByEntropy, updatedEntropy, quadNode);
const config = allowedTileConfigs[0];
this._approveConfigForQuad(quadNode, config);
return;
}
default: {
}
}
}
private _reduceEntropyWithCache(
quadNode: QuadNode,
allowedTileConfigs: TileConfig[],
comparableQuadNodes?: Set<QuadNode>
): number | undefined {
const previousEntropy: number = allowedTileConfigs.length;
this._reduceEntropy(quadNode, allowedTileConfigs, comparableQuadNodes);
const updatedEntropy: number = allowedTileConfigs.length;
return this._setQuadNodeEntropyCache(quadNode, updatedEntropy, previousEntropy);
// if (updatedEntropy == previousEntropy) {
// return;
// }
// popFromArrayAtEntry(this._quadNodeByEntropy, previousEntropy, quadNode);
// pushOnArrayAtEntry(this._quadNodeByEntropy, updatedEntropy, quadNode);
// if (updatedEntropy <= this._lowestEntropy /*&& updatedEntropy > 1*/) {
// this._lowestEntropy = updatedEntropy;
// }
// return updatedEntropy;
}
private _setQuadNodeEntropyCache(quadNode: QuadNode, updatedEntropy: number, previousEntropy: number) {
if (updatedEntropy == previousEntropy) {
return;
}
popFromArrayAtEntry(this._quadNodeByEntropy, previousEntropy, quadNode);
pushOnArrayAtEntry(this._quadNodeByEntropy, updatedEntropy, quadNode);
if (updatedEntropy <= this._lowestEntropy /*&& updatedEntropy > 1*/) {
this._lowestEntropy = updatedEntropy;
}
return updatedEntropy;
}
private _resetQuadNodeEntropyIfNotReset(quadNode: QuadNode, resetQuadNodeIds: Set<QuadNode>) {
if (resetQuadNodeIds.has(quadNode)) {
return;
}
this._resetQuadNode(quadNode);
resetQuadNodeIds.add(quadNode);
}
private _reduceEntropy(quadNode: QuadNode, allowedTileConfigs: TileConfig[], comparableQuadNodes?: Set<QuadNode>) {
let i = 0;
while (i < allowedTileConfigs.length) {
const allowed = this._checkConfigAgainstNeighbours(quadNode, allowedTileConfigs[i], comparableQuadNodes);
if (allowed) {
i++;
} else {
allowedTileConfigs.splice(i, 1);
}
}
// if the allowedTileConfigs contain a single solid tile and empty tiles, remove the empty tiles
if (allowedTileConfigs.length > 1) {
configTilesStats(allowedTileConfigs, _configStats);
switch (_configStats.solid) {
case 0: {
// if none is solid, we keep the first empty
allowedTileConfigs.splice(1, allowedTileConfigs.length - 1);
return;
}
case 1: {
// if one is solid, we keep this one
const _getIndex = () => {
let index = 0;
for (const tileConfig of allowedTileConfigs) {
if (tileConfig.tileId != EMPTY_TILE_ID) {
return index;
}
index++;
}
return index;
};
const index = _getIndex();
allowedTileConfigs.splice(index + 1, allowedTileConfigs.length).splice(index - 1, index);
}
}
}
}
private _checkConfigAgainstNeighbours(
quadNode: QuadNode,
tileConfig: TileConfig,
comparableQuadNodes?: Set<QuadNode>
): boolean {
if (!this._isConfigAllowedWithNeighbour(quadNode, tileConfig, 0, comparableQuadNodes)) {
return false;
}
if (!this._isConfigAllowedWithNeighbour(quadNode, tileConfig, 1, comparableQuadNodes)) {
return false;
}
if (!this._isConfigAllowedWithNeighbour(quadNode, tileConfig, 2, comparableQuadNodes)) {
return false;
}
if (!this._isConfigAllowedWithNeighbour(quadNode, tileConfig, 3, comparableQuadNodes)) {
return false;
}
if (!this._isConfigAllowedWithVerticalNeighbour(quadNode, tileConfig, -1, comparableQuadNodes)) {
return false;
}
if (!this._isConfigAllowedWithVerticalNeighbour(quadNode, tileConfig, +1, comparableQuadNodes)) {
return false;
}
return true;
}
protected _isConfigAllowedWithNeighbour(
quadNode: QuadNode,
tileConfig: TileConfig,
neighbourIndex: NeighbourIndex,
comparableQuadNodes?: Set<QuadNode>
): boolean {
const presentedSide0 = CCW_HALF_EDGE_SIDES[mod(neighbourIndex - tileConfig.rotation, 4)];
const floorGraph = this._floorGraph(quadNode);
floorGraph.neighbourData(quadNode.id, neighbourIndex, _neighbourData);
if (!_neighbourData.quadNode || _neighbourData.neighbourIndex == null) {
// we are on the grid border
const isAllowed = this._tilesCollection.allowedTileConfig(
tileConfig.tileId,
presentedSide0,
GRID_BORDER_ID,
GRID_BORDER_SIDE_NAME
);
return isAllowed;
}
if (comparableQuadNodes) {
// if we have a comparableQuadNodes,
// we only compare if the neighbour node is included in the comparableQuadNodes
if (!comparableQuadNodes.has(_neighbourData.quadNode)) {
return true;
}
}
//
const neighbourConfigs = floorGraph.allowedTileConfigsForQuadNode(_neighbourData.quadNode)!;
if (neighbourConfigs.length == 0) {
// if neighbour has no config, it is empty, so the config is allowed
return true;
}
for (const neighbourConfig of neighbourConfigs) {
const presentedSide1 =
CCW_HALF_EDGE_SIDES[mod(_neighbourData.neighbourIndex - neighbourConfig.rotation, 4)];
const isAllowed = this._tilesCollection.allowedTileConfig(
tileConfig.tileId,
presentedSide0,
neighbourConfig.tileId,
presentedSide1
);
if (isAllowed) {
return true;
}
}
return false;
}
protected _isConfigAllowedWithVerticalNeighbour(
quadNode: QuadNode,
tileConfig: TileConfig,
floorOffset: 1 | -1,
comparableQuadNodes?: Set<QuadNode>
): boolean {
const floorIndex = this._floorGraphIndexByQuadNode.get(quadNode)!;
const presentedSide0 = floorOffset > 0 ? 't' : 'b';
const presentedSide1 = floorOffset > 0 ? 'b' : 't';
const neighbourFloorIndex = floorIndex + floorOffset;
const neighbourFloorGraph = this._floorGraphs[neighbourFloorIndex];
if (!neighbourFloorGraph) {
// we are on the grid border
const isAllowed = this._tilesCollection.allowedTileConfig(
tileConfig.tileId,
presentedSide0,
GRID_BORDER_ID,
GRID_BORDER_SIDE_NAME
);
return isAllowed;
}
const neighbourQuadNode = neighbourFloorGraph.quadNodeFromOtherQuadNode(quadNode);
if (!neighbourQuadNode) {
// we are on the grid border
const isAllowed = this._tilesCollection.allowedTileConfig(
tileConfig.tileId,
presentedSide0,
GRID_BORDER_ID,
GRID_BORDER_SIDE_NAME
);
return isAllowed;
}
if (comparableQuadNodes) {
// if we have a comparableQuadNodes,
// we only compare if the neighbour node is included in the comparableQuadNodes
if (!comparableQuadNodes.has(neighbourQuadNode)) {
return true;
}
}
const neighbourConfigs = neighbourFloorGraph.allowedTileConfigsForQuadNode(neighbourQuadNode)!;
if (neighbourConfigs.length == 0) {
// if neighbour has no config, it is empty, so the config is allowed
return true;
}
for (const neighbourConfig of neighbourConfigs) {
const isAllowed = this._tilesCollection.allowedTileConfig(
tileConfig.tileId,
presentedSide0,
neighbourConfig.tileId,
presentedSide1
);
if (isAllowed) {
// if the connection is allowed, and since we are only testing vertical connection,
// we only take a config if it has the same rotation,
// OR if one of the tiles is the empty tile or the grid border
const isAllowedIfNeighbourEmptyOrSharesRotation =
neighbourConfig.tileId == EMPTY_TILE_ID ||
tileConfig.tileId == EMPTY_TILE_ID ||
neighbourConfig.tileId == GRID_BORDER_ID ||
tileConfig.tileId == GRID_BORDER_ID ||
neighbourConfig.rotation == tileConfig.rotation;
if (isAllowedIfNeighbourEmptyOrSharesRotation) {
return true;
}
}
}
return false;
}
private _quadNodeWithLowestEntropy(seed: number) {
if (this._solveReachMaxCount()) {
return;
}
let quadNodes = this._quadNodeByEntropy.get(this._lowestEntropy);
// update lowest entropy if needed
while (quadNodes == null || quadNodes.length == 0) {
this._quadNodeByEntropy.delete(this._lowestEntropy);
_entropiesSet.clear();
this._quadNodeByEntropy.forEach((quadNodes, entropy) => {
_entropiesSet.add(entropy);
});
setToArray(_entropiesSet, _sortedEntropies);
const sortedEntropies = _sortedEntropies.sort((a, b) => a - b);
if (sortedEntropies.length == 0) {
return;
}
this._lowestEntropy = sortedEntropies[0];
quadNodes = this._quadNodeByEntropy.get(this._lowestEntropy);
}
if (!quadNodes) {
return;
}
const quadNode = spliceSample(quadNodes, seed);
if (!quadNode) {
return;
}
return quadNode;
}
private _floorGraph(quadNode: QuadNode): WFCFloorGraph {
const floorIndex = this._floorGraphIndexByQuadNode.get(quadNode)!;
return this._floorGraphs[floorIndex];
}
}