@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
526 lines (525 loc) • 20.3 kB
JavaScript
"use strict";
import { WFCTilesCollection } from "./WFCTilesCollection";
import { configTilesStats, solidTilesStats } from "./WFCCommon";
import { EMPTY_TILE_ID, GRID_BORDER_ID, GRID_BORDER_SIDE_NAME } from "./WFCConstant";
import { tileConfigToString, ERRORED_TILE_CONFIG, tileConfigsToString } from "./WFCTileConfig";
import { CoreWFCTileAttribute, WFCQuadAttribute } from "./WFCAttributes";
import { Attribute } from "../geometry/Attribute";
import { pushOnArrayAtEntry, popFromArrayAtEntry, addToSetAtEntry } from "../MapUtils";
import { arrayUniq, sample, spliceSample } from "../ArrayUtils";
import { setToArray } from "../SetUtils";
import { CCW_HALF_EDGE_SIDES } from "../geometry/modules/quad/graph/QuadGraphCommon";
import { mod } from "../math/_Module";
import { WFCFloorGraph } from "./WFCFloorGraph";
import { isQuadNodeSolveAllowed, quadPrimitiveFloorIndex } from "./WFCUtils";
import { QuadPrimitive } from "../geometry/modules/quad/QuadPrimitive";
import { WFCTileConfigSampler } from "./WFCTileConfigSampler";
import { corePrimitiveClassFactory } from "../geometry/CoreObjectFactory";
const _neighbourData = {
quadNode: null,
neighbourIndex: null
};
const _configStats = {
solid: 0,
empty: 0
};
const _entropiesSet = /* @__PURE__ */ new Set();
const _sortedEntropies = [];
const _tileConfigSampler = new WFCTileConfigSampler();
export class WFCSolver {
//
constructor(options) {
this._resolvedQuadsCount = 0;
this._stepsCount = 0;
this._quadPositionArrays = [];
this._lowestEntropy = Number.POSITIVE_INFINITY;
this._quadNodeByEntropy = /* @__PURE__ */ new Map();
this._floorGraphs = [];
this._quadIndicesByFloorIndex = /* @__PURE__ */ new Map();
this._floorGraphIndexByQuadNode = /* @__PURE__ */ new Map();
this._samplingWithWeightRequired = false;
this._allTileConfigs = [];
const { quadObject } = options;
this._maxResolvedQuadsCount = options.maxResolvedQuadsCount;
this._tilesCollection = new WFCTilesCollection(options);
const tiles = this._tilesCollection.tiles();
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 });
}
}
const weights = [];
arrayUniq(
tiles.map((tile) => CoreWFCTileAttribute.getWeight(tile)),
weights
);
if (weights.length > 1) {
this._samplingWithWeightRequired = true;
}
const primitivesCount = QuadPrimitive.entitiesCount(quadObject);
for (let i = 0; i < primitivesCount; i++) {
const floorIndex = quadPrimitiveFloorIndex(quadObject, i);
addToSetAtEntry(this._quadIndicesByFloorIndex, floorIndex, i);
}
const solvedTileConfigs = new Array(primitivesCount).fill("");
const solvedTileIdsAttribute = {
isString: true,
array: solvedTileConfigs,
itemSize: 1
};
QuadPrimitive.addAttribute(quadObject, WFCQuadAttribute.SOLVED_TILE_CONFIGS, solvedTileIdsAttribute);
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);
}
_setSolvedTileConfigs(object) {
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;
}
// objects(): Object3D[] {
// return this._objects;
// }
_setupQuadNode(floorGraph, quadIndex, allTileConfigs) {
if (!isQuadNodeSolveAllowed(floorGraph.quadObject, quadIndex)) {
return;
}
const { quadNode, quadTileConfigs } = floorGraph.setupQuadNode(quadIndex, allTileConfigs);
this._floorGraphIndexByQuadNode.set(quadNode, floorGraph.floorIndex);
this._initQuadNodeEntropyCache(quadNode, quadTileConfigs);
}
_resetQuadNode(quadNode) {
const floorGraph = this._floorGraph(quadNode);
const { quadTileConfigs } = floorGraph.resetQuadNode(quadNode, this._allTileConfigs);
this._initQuadNodeEntropyCache(quadNode, quadTileConfigs);
}
_initQuadNodeEntropyCache(quadNode, quadTileConfigs) {
const entropy = quadTileConfigs.length;
pushOnArrayAtEntry(this._quadNodeByEntropy, entropy, quadNode);
if (entropy < this._lowestEntropy) {
this._lowestEntropy = entropy;
}
}
//
//
//
//
//
process(options, comparableQuadNodes) {
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) {
break;
}
}
}
this._commitConfigsAttributes();
}
step(quadSeed, configSeed, comparableQuadNodes) {
this._stepsCount++;
const quadNode = this._quadNodeWithLowestEntropy(quadSeed + this._stepsCount);
if (!quadNode) {
return false;
}
const floorGraph = this._floorGraph(quadNode);
const allowedConfigs = floorGraph.allowedTileConfigsForQuadNode(quadNode);
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._approveConfigForQuad(quadNode, config);
this._updateNeighboursEntropy(quadNode);
}
addSoftContraint(options) {
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 = {
tileId,
rotation
};
floorGraph.setAllowedTileConfigsForQuadNode(quadNode, [config]);
this._approveConfigForQuad(quadNode, config);
const resetQuadNodeIds = /* @__PURE__ */ new Set([quadNode]);
this._resetAndUpdateNeighboursEntropy(quadNode, resetQuadNodeIds);
this.process(options, resetQuadNodeIds);
}
//
//
//
//
//
_solveReachMaxCount() {
return this._maxResolvedQuadsCount >= 0 && this._resolvedQuadsCount >= this._maxResolvedQuadsCount;
}
_selectConfig(allowedConfigs, seed) {
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);
}
}
_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);
}
}
});
}
_placeErrorTileObject(quadNode) {
this._commitConfigAttributeToQuadPrimitive(quadNode, ERRORED_TILE_CONFIG);
this._resolvedQuadsCount++;
}
_approveConfigForQuad(quadNode, config) {
this._commitConfigAttributeToQuadPrimitive(quadNode, config);
this._resolvedQuadsCount++;
}
_commitConfigsAttributeToQuadPrimitive(quadNode, tileConfigs) {
this._solvedTileConfigs[quadNode.id] = tileConfigsToString(tileConfigs);
}
_commitConfigAttributeToQuadPrimitive(quadNode, tileConfig) {
this._solvedTileConfigs[quadNode.id] = tileConfigToString(tileConfig);
}
_updateNeighboursEntropy(startQuadNode) {
const stack = [startQuadNode];
while (stack.length > 0 && !this._solveReachMaxCount()) {
const currentQuad = stack.pop();
const floorIndex = this._floorGraphIndexByQuadNode.get(currentQuad);
const floorGraph = this._floorGraphs[floorIndex];
for (let i = 0; i < 4; i++) {
floorGraph.neighbourData(currentQuad.id, i, _neighbourData);
if (_neighbourData.quadNode && floorGraph.allowedTileConfigsForQuadNode(_neighbourData.quadNode).length > 1) {
this._updateQuadEntropy(_neighbourData.quadNode, stack);
}
}
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);
}
}
}
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);
}
}
}
}
}
_resetAndUpdateNeighboursEntropy(startQuadNode, resetQuadNodeIds) {
const stack = [startQuadNode];
while (stack.length > 0 && !this._solveReachMaxCount()) {
const currentQuad = stack.pop();
const floorIndex = this._floorGraphIndexByQuadNode.get(currentQuad);
const floorGraph = this._floorGraphs[floorIndex];
for (let i = 0; i < 4; i++) {
floorGraph.neighbourData(currentQuad.id, i, _neighbourData);
if (_neighbourData.quadNode) {
this._resetQuadNodeEntropyIfNotReset(_neighbourData.quadNode, resetQuadNodeIds);
if (floorGraph.allowedTileConfigsForQuadNode(_neighbourData.quadNode).length > 1) {
this._updateQuadEntropy(_neighbourData.quadNode, stack, resetQuadNodeIds);
}
}
}
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);
}
}
}
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);
}
}
}
}
}
_updateQuadEntropy(quadNode, stack, comparableQuadNodes) {
if (this._solveReachMaxCount()) {
return;
}
const floorGraph = this._floorGraph(quadNode);
const allowedTileConfigs = floorGraph.allowedTileConfigsForQuadNode(quadNode);
const updatedEntropy = this._reduceEntropyWithCache(quadNode, allowedTileConfigs, comparableQuadNodes);
if (updatedEntropy === void 0) {
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: {
}
}
}
_reduceEntropyWithCache(quadNode, allowedTileConfigs, comparableQuadNodes) {
const previousEntropy = allowedTileConfigs.length;
this._reduceEntropy(quadNode, allowedTileConfigs, comparableQuadNodes);
const updatedEntropy = allowedTileConfigs.length;
return this._setQuadNodeEntropyCache(quadNode, updatedEntropy, previousEntropy);
}
_setQuadNodeEntropyCache(quadNode, updatedEntropy, previousEntropy) {
if (updatedEntropy == previousEntropy) {
return;
}
popFromArrayAtEntry(this._quadNodeByEntropy, previousEntropy, quadNode);
pushOnArrayAtEntry(this._quadNodeByEntropy, updatedEntropy, quadNode);
if (updatedEntropy <= this._lowestEntropy) {
this._lowestEntropy = updatedEntropy;
}
return updatedEntropy;
}
_resetQuadNodeEntropyIfNotReset(quadNode, resetQuadNodeIds) {
if (resetQuadNodeIds.has(quadNode)) {
return;
}
this._resetQuadNode(quadNode);
resetQuadNodeIds.add(quadNode);
}
_reduceEntropy(quadNode, allowedTileConfigs, comparableQuadNodes) {
let i = 0;
while (i < allowedTileConfigs.length) {
const allowed = this._checkConfigAgainstNeighbours(quadNode, allowedTileConfigs[i], comparableQuadNodes);
if (allowed) {
i++;
} else {
allowedTileConfigs.splice(i, 1);
}
}
if (allowedTileConfigs.length > 1) {
configTilesStats(allowedTileConfigs, _configStats);
switch (_configStats.solid) {
case 0: {
allowedTileConfigs.splice(1, allowedTileConfigs.length - 1);
return;
}
case 1: {
const _getIndex = () => {
let index2 = 0;
for (const tileConfig of allowedTileConfigs) {
if (tileConfig.tileId != EMPTY_TILE_ID) {
return index2;
}
index2++;
}
return index2;
};
const index = _getIndex();
allowedTileConfigs.splice(index + 1, allowedTileConfigs.length).splice(index - 1, index);
}
}
}
}
_checkConfigAgainstNeighbours(quadNode, tileConfig, comparableQuadNodes) {
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;
}
_isConfigAllowedWithNeighbour(quadNode, tileConfig, neighbourIndex, comparableQuadNodes) {
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) {
const isAllowed = this._tilesCollection.allowedTileConfig(
tileConfig.tileId,
presentedSide0,
GRID_BORDER_ID,
GRID_BORDER_SIDE_NAME
);
return isAllowed;
}
if (comparableQuadNodes) {
if (!comparableQuadNodes.has(_neighbourData.quadNode)) {
return true;
}
}
const neighbourConfigs = floorGraph.allowedTileConfigsForQuadNode(_neighbourData.quadNode);
if (neighbourConfigs.length == 0) {
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;
}
_isConfigAllowedWithVerticalNeighbour(quadNode, tileConfig, floorOffset, comparableQuadNodes) {
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) {
const isAllowed = this._tilesCollection.allowedTileConfig(
tileConfig.tileId,
presentedSide0,
GRID_BORDER_ID,
GRID_BORDER_SIDE_NAME
);
return isAllowed;
}
const neighbourQuadNode = neighbourFloorGraph.quadNodeFromOtherQuadNode(quadNode);
if (!neighbourQuadNode) {
const isAllowed = this._tilesCollection.allowedTileConfig(
tileConfig.tileId,
presentedSide0,
GRID_BORDER_ID,
GRID_BORDER_SIDE_NAME
);
return isAllowed;
}
if (comparableQuadNodes) {
if (!comparableQuadNodes.has(neighbourQuadNode)) {
return true;
}
}
const neighbourConfigs = neighbourFloorGraph.allowedTileConfigsForQuadNode(neighbourQuadNode);
if (neighbourConfigs.length == 0) {
return true;
}
for (const neighbourConfig of neighbourConfigs) {
const isAllowed = this._tilesCollection.allowedTileConfig(
tileConfig.tileId,
presentedSide0,
neighbourConfig.tileId,
presentedSide1
);
if (isAllowed) {
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;
}
_quadNodeWithLowestEntropy(seed) {
if (this._solveReachMaxCount()) {
return;
}
let quadNodes = this._quadNodeByEntropy.get(this._lowestEntropy);
while (quadNodes == null || quadNodes.length == 0) {
this._quadNodeByEntropy.delete(this._lowestEntropy);
_entropiesSet.clear();
this._quadNodeByEntropy.forEach((quadNodes2, 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;
}
_floorGraph(quadNode) {
const floorIndex = this._floorGraphIndexByQuadNode.get(quadNode);
return this._floorGraphs[floorIndex];
}
}