UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

526 lines (525 loc) 20.3 kB
"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]; } }