1272 lines
42 KiB
JavaScript
1272 lines
42 KiB
JavaScript
(function () {
|
|
let currentImageUrl =
|
|
'https://raw.githubusercontent.com/mxgmn/WaveFunctionCollapse/master/samples/Flowers.png';
|
|
let currentPatternSize = 3;
|
|
let enableRotations = false;
|
|
let OUTPUT_SIZE = 64;
|
|
const SOURCE_VISUAL_SCALE = 4;
|
|
|
|
const imageUrlInput = document.getElementById('imageUrlInput');
|
|
const patternSizeSelect = document.getElementById('patternSizeSelect');
|
|
const randomSelectionCheckbox = document.getElementById(
|
|
'randomSelectionCheckbox'
|
|
);
|
|
|
|
const enableRotationsCheckbox = document.getElementById(
|
|
'enableRotationsCheckbox'
|
|
);
|
|
const restartButton = document.getElementById('restartButton');
|
|
|
|
const sourceCanvas = document.getElementById('sourceCanvas');
|
|
const sourceCtx = sourceCanvas.getContext('2d');
|
|
const outputCanvas = document.getElementById('outputCanvas');
|
|
const outputCtx = outputCanvas.getContext('2d');
|
|
outputCtx.imageSmoothingEnabled = false;
|
|
|
|
const statusEl = document.getElementById('status');
|
|
let statsContainer = document.getElementById('statsContainer');
|
|
|
|
let patterns, patternByID, patternByIndex, patternIdToIndex;
|
|
let adjacency;
|
|
let sampleWidth, sampleHeight, srcData;
|
|
let pCols, pRows, grid;
|
|
let phase = 'idle';
|
|
let queue = [],
|
|
queuedCoordinatesSet = new Set(),
|
|
maxTries;
|
|
let currentAttemptFailed = false;
|
|
let dirtyTilesForFrame = new Set();
|
|
let animationFrameId = null;
|
|
let backtrackStack = [];
|
|
|
|
const MAX_BACKTRACK_DEPTH = 4096;
|
|
|
|
// --- Stats Object ---
|
|
let stats = {};
|
|
|
|
function resetStats() {
|
|
stats = {
|
|
sourceImage: { loaded: false, width: 0, height: 0, pixels: 0 },
|
|
patternExtraction: {
|
|
rawPatternCount: 0,
|
|
uniquePatternCount: 0,
|
|
rotationsEnabled: false,
|
|
timeMs: 0,
|
|
},
|
|
adjacency: {
|
|
totalRules: 0,
|
|
timeMs: 0,
|
|
},
|
|
grid: {
|
|
pCols: 0,
|
|
pRows: 0,
|
|
initialCellOptions: 0,
|
|
timeMs: 0,
|
|
},
|
|
wfcRun: {
|
|
maxBacktrackStackDepthReached: 0,
|
|
totalBacktracks: 0,
|
|
totalContradictions: 0,
|
|
forcedBacktracksByDepth: 0,
|
|
cellsCollapsed: 0,
|
|
propagationSteps: 0,
|
|
startTime: 0,
|
|
endTime: 0,
|
|
durationMs: 0,
|
|
status: 'Not started',
|
|
},
|
|
memory: {
|
|
patternsArrayBytes: 0,
|
|
patternCellsBytes: 0,
|
|
patternByIdBytes: 0,
|
|
adjacencyBytes: 0,
|
|
estimatedGridSnapshotBytes: 0,
|
|
backtrackStackPeakBytes: 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
function formatBytes(bytes, decimals = 2) {
|
|
if (!Number.isFinite(bytes) || bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
// Prevent log(0) or log(negative) for Math.abs(bytes)
|
|
if (Math.abs(bytes) < 1) return bytes.toFixed(dm) + ' Bytes';
|
|
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function estimateStringBytes(str) {
|
|
return str ? str.length * 2 : 0; // UTF-16
|
|
}
|
|
|
|
function estimateObjectOverhead(obj) {
|
|
if (!obj) return 0;
|
|
// Rough estimate: (key string + pointer + type tag) per property + base object overhead
|
|
return (
|
|
Object.keys(obj).length *
|
|
(estimateStringBytes('key_placeholder_avg') + 8 + 4) +
|
|
16
|
|
);
|
|
}
|
|
|
|
function renderStats() {
|
|
if (!statsContainer) return;
|
|
let html = `<strong>WFC Statistics:</strong><br />`;
|
|
html += `Status: ${stats.wfcRun.status}<br />`;
|
|
if (stats.wfcRun.durationMs > 0) {
|
|
html += `Total Time: ${(stats.wfcRun.durationMs / 1000).toFixed(
|
|
2
|
|
)}s<br />`;
|
|
}
|
|
html += `<br />`;
|
|
|
|
if (stats.sourceImage.loaded) {
|
|
html += `<strong>Source Image:</strong><br />`;
|
|
html += ` Dimensions: ${stats.sourceImage.width}x${stats.sourceImage.height}<br />`;
|
|
html += ` Pattern Size: ${currentPatternSize}x${currentPatternSize}<br />`;
|
|
html += ` Rotations Enabled: ${stats.patternExtraction.rotationsEnabled}<br />`;
|
|
html += `<br />`;
|
|
|
|
html += `<strong>Pattern Extraction (${(
|
|
stats.patternExtraction.timeMs / 1000
|
|
).toFixed(2)}s):</strong><br />`;
|
|
html += ` Raw Overlapping Patterns: ${stats.patternExtraction.rawPatternCount}<br />`;
|
|
html += ` Unique Patterns: ${stats.patternExtraction.uniquePatternCount}<br />`;
|
|
html += ` Memory (Pattern Objects): ~${formatBytes(
|
|
stats.memory.patternsArrayBytes
|
|
)}<br />`;
|
|
html += ` Memory (Pattern Pixel Data): ~${formatBytes(
|
|
stats.memory.patternCellsBytes
|
|
)}<br />`;
|
|
html += ` Memory (Pattern ID Map): ~${formatBytes(
|
|
stats.memory.patternByIdBytes
|
|
)}<br />`;
|
|
html += `<br />`;
|
|
|
|
html += `<strong>Adjacency Rules (${(
|
|
stats.adjacency.timeMs / 1000
|
|
).toFixed(2)}s):</strong><br />`;
|
|
html += ` Total Adjacency Rules: ${stats.adjacency.totalRules}<br />`;
|
|
html += ` Memory (Adjacency Map): ~${formatBytes(
|
|
stats.memory.adjacencyBytes
|
|
)}<br />`;
|
|
html += `<br />`;
|
|
|
|
html += `<strong>Output Grid (${(stats.grid.timeMs / 1000).toFixed(
|
|
2
|
|
)}s initialization):</strong><br />`;
|
|
html += ` Output Grid Pixel Size: ${OUTPUT_SIZE}x${OUTPUT_SIZE}<br />`;
|
|
html += ` Pattern Grid Dimensions: ${stats.grid.pCols}x${
|
|
stats.grid.pRows
|
|
} (${stats.grid.pCols * stats.grid.pRows} cells)<br />`;
|
|
html += ` Initial Options/Cell: ${stats.grid.initialCellOptions}<br />`;
|
|
html += ` Est. Memory per Grid State: ~${formatBytes(
|
|
stats.memory.estimatedGridSnapshotBytes
|
|
)}<br />`;
|
|
html += `<br />`;
|
|
}
|
|
|
|
html += `<strong>WFC Run Dynamics:</strong><br />`;
|
|
html += ` Max Backtrack Stack Depth: ${stats.wfcRun.maxBacktrackStackDepthReached} / ${MAX_BACKTRACK_DEPTH}<br />`;
|
|
html += ` Est. Peak Backtrack Stack Memory: ~${formatBytes(
|
|
stats.memory.backtrackStackPeakBytes
|
|
)}<br />`;
|
|
html += ` Total Backtracks: ${stats.wfcRun.totalBacktracks}<br />`;
|
|
html += ` Forced Backtracks (Max Depth): ${stats.wfcRun.forcedBacktracksByDepth}<br />`;
|
|
html += ` Total Contradictions: ${stats.wfcRun.totalContradictions}<br />`;
|
|
html += ` Cells Collapsed: ${stats.wfcRun.cellsCollapsed}<br />`;
|
|
html += ` Propagation Steps: ${stats.wfcRun.propagationSteps}<br />`;
|
|
|
|
statsContainer.innerHTML = html; // Use innerHTML to render strong tags
|
|
}
|
|
|
|
imageUrlInput.value = currentImageUrl;
|
|
patternSizeSelect.value = currentPatternSize.toString();
|
|
enableRotationsCheckbox.checked = enableRotations;
|
|
|
|
function Uint8ArrayToHexString(u8a) {
|
|
let hex = '';
|
|
for (let i = 0; i < u8a.length; i++) {
|
|
let h = u8a[i].toString(16);
|
|
if (h.length < 2) {
|
|
h = '0' + h;
|
|
}
|
|
hex += h;
|
|
}
|
|
return hex;
|
|
}
|
|
|
|
function deepCopyGrid(gridToCopy) {
|
|
if (!gridToCopy) return null;
|
|
return gridToCopy.map((row) => row.map((cellOptions) => [...cellOptions]));
|
|
}
|
|
|
|
function deepCopyQueue(queueToCopy) {
|
|
if (!queueToCopy) return null;
|
|
return queueToCopy.map((item) => (Array.isArray(item) ? [...item] : item));
|
|
}
|
|
|
|
const img = new Image();
|
|
img.crossOrigin = 'Anonymous';
|
|
|
|
img.onload = () => {
|
|
if (phase === 'loading_image_error') return;
|
|
phase = 'processing';
|
|
stats.wfcRun.status = 'Processing Image';
|
|
renderStats();
|
|
|
|
sampleWidth = img.width;
|
|
sampleHeight = img.height;
|
|
stats.sourceImage = {
|
|
loaded: true,
|
|
width: sampleWidth,
|
|
height: sampleHeight,
|
|
pixels: sampleWidth * sampleHeight,
|
|
};
|
|
|
|
sourceCanvas.width = sampleWidth;
|
|
sourceCanvas.height = sampleHeight;
|
|
sourceCtx.drawImage(img, 0, 0);
|
|
|
|
const dynamicScale = Math.max(
|
|
1,
|
|
Math.min(
|
|
SOURCE_VISUAL_SCALE,
|
|
Math.floor(256 / Math.max(sampleWidth, sampleHeight))
|
|
)
|
|
);
|
|
sourceCanvas.style.width = sampleWidth * dynamicScale + 'px';
|
|
sourceCanvas.style.height = sampleHeight * dynamicScale + 'px';
|
|
|
|
if (sampleWidth < currentPatternSize || sampleHeight < currentPatternSize) {
|
|
statusEl.textContent = `Error: Image dimensions (${sampleWidth}x${sampleHeight}) are smaller than pattern size (${currentPatternSize}x${currentPatternSize}).`;
|
|
phase = 'error';
|
|
stats.wfcRun.status = 'Error: Image too small';
|
|
renderStats();
|
|
return;
|
|
}
|
|
srcData = sourceCtx.getImageData(0, 0, sampleWidth, sampleHeight).data;
|
|
statusEl.textContent = `Extracting ${currentPatternSize}x${currentPatternSize} patterns...`;
|
|
stats.wfcRun.status = `Extracting patterns...`;
|
|
renderStats();
|
|
|
|
requestAnimationFrame(() => {
|
|
// Use timeout/rAF to allow UI update for status
|
|
const t0 = performance.now();
|
|
try {
|
|
extractPatterns();
|
|
stats.patternExtraction.timeMs = performance.now() - t0;
|
|
if (!patterns || patterns.length === 0) {
|
|
statusEl.textContent = `Error: No patterns found. Make sure image is suitable. Check rotations if enabled.`;
|
|
phase = 'error';
|
|
stats.wfcRun.status = 'Error: No patterns';
|
|
renderStats();
|
|
return;
|
|
}
|
|
stats.patternExtraction.uniquePatternCount = patterns.length;
|
|
stats.patternExtraction.rotationsEnabled = enableRotations;
|
|
|
|
stats.memory.patternCellsBytes = 0;
|
|
stats.memory.patternsArrayBytes = 0; // For the 'patterns' array itself and objects within
|
|
patterns.forEach((p) => {
|
|
stats.memory.patternCellsBytes += p.cells.byteLength; // Actual pixel data
|
|
// Estimate size of pattern object (id string, freq num, index num, cells ref, object shell)
|
|
stats.memory.patternsArrayBytes +=
|
|
estimateStringBytes(p.id) + 4 + 4 + 8 + 16;
|
|
});
|
|
stats.memory.patternsArrayBytes += 16; // Base array overhead for 'patterns'
|
|
|
|
stats.memory.patternByIdBytes = estimateObjectOverhead(patternByID);
|
|
Object.keys(patternByID).forEach((key) => {
|
|
stats.memory.patternByIdBytes += estimateStringBytes(key) + 8; // key + reference
|
|
});
|
|
|
|
let statusMessage = `Found ${patterns.length} unique patterns.`;
|
|
if (patterns.length > 1000) {
|
|
statusMessage += ` (High count: ${patterns.length}, may be memory intensive or slow). Building adjacency...`;
|
|
} else {
|
|
statusMessage += ` Building adjacency...`;
|
|
}
|
|
statusEl.textContent = statusMessage;
|
|
stats.wfcRun.status = `Building adjacency...`;
|
|
renderStats();
|
|
|
|
requestAnimationFrame(() => {
|
|
const t1 = performance.now();
|
|
try {
|
|
buildAdjacency();
|
|
stats.adjacency.timeMs = performance.now() - t1;
|
|
statusEl.textContent = 'Initializing WFC grid...';
|
|
stats.wfcRun.status = `Initializing grid...`;
|
|
renderStats();
|
|
|
|
requestAnimationFrame(() => {
|
|
const t2 = performance.now();
|
|
try {
|
|
initializeGrid(); // This sets outputCanvas dimensions
|
|
stats.grid.timeMs = performance.now() - t2;
|
|
if (phase === 'error') {
|
|
renderStats();
|
|
return;
|
|
}
|
|
statusEl.textContent = 'Running progressive WFC...';
|
|
stats.wfcRun.status = 'Running WFC...';
|
|
stats.wfcRun.startTime = performance.now();
|
|
renderStats();
|
|
|
|
phase = 'collapse';
|
|
if (animationFrameId) cancelAnimationFrame(animationFrameId);
|
|
animationFrameId = requestAnimationFrame(runStep);
|
|
} catch (e) {
|
|
console.error('Error during grid initialization:', e);
|
|
statusEl.textContent = `Error initializing grid: ${e.message}`;
|
|
phase = 'error';
|
|
stats.wfcRun.status = `Error: Grid init failed (${e.message})`;
|
|
renderStats();
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.error('Error during adjacency building:', e);
|
|
statusEl.textContent = `Error building adjacency: ${e.message}`;
|
|
phase = 'error';
|
|
stats.wfcRun.status = `Error: Adjacency failed (${e.message})`;
|
|
renderStats();
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.error('Error during pattern extraction:', e);
|
|
statusEl.textContent = `Error extracting patterns: ${e.message}`;
|
|
phase = 'error';
|
|
stats.wfcRun.status = `Error: Pattern extraction failed (${e.message})`;
|
|
renderStats();
|
|
}
|
|
});
|
|
};
|
|
|
|
img.onerror = () => {
|
|
statusEl.textContent =
|
|
'Error: Failed to load source image. Check URL and CORS policy. Try a different image host.';
|
|
phase = 'loading_image_error';
|
|
stats.wfcRun.status = 'Error: Image load failed';
|
|
stats.sourceImage.loaded = false;
|
|
renderStats();
|
|
};
|
|
|
|
function resetWfcStateAndLoadNewImage() {
|
|
if (animationFrameId) {
|
|
cancelAnimationFrame(animationFrameId);
|
|
animationFrameId = null;
|
|
}
|
|
phase = 'loading';
|
|
resetStats();
|
|
stats.wfcRun.status = 'Loading image...';
|
|
renderStats();
|
|
if (outputCanvas.width > 0 && outputCanvas.height > 0) {
|
|
// Ensure canvas has dimensions before clearing
|
|
outputCtx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);
|
|
}
|
|
|
|
currentImageUrl = imageUrlInput.value.trim();
|
|
currentPatternSize = parseInt(patternSizeSelect.value, 10);
|
|
enableRotations = enableRotationsCheckbox.checked;
|
|
|
|
patterns = null;
|
|
patternByID = null;
|
|
patternByIndex = null;
|
|
patternIdToIndex = null;
|
|
adjacency = null;
|
|
grid = null;
|
|
queue = [];
|
|
queuedCoordinatesSet.clear();
|
|
currentAttemptFailed = false;
|
|
maxTries = 0;
|
|
dirtyTilesForFrame.clear();
|
|
pCols = 0;
|
|
pRows = 0;
|
|
srcData = null;
|
|
backtrackStack = [];
|
|
|
|
if (!currentImageUrl) {
|
|
statusEl.textContent = 'Error: Image URL cannot be empty.';
|
|
phase = 'error';
|
|
stats.wfcRun.status = 'Error: No image URL';
|
|
renderStats();
|
|
return;
|
|
}
|
|
img.src = '';
|
|
img.src = currentImageUrl;
|
|
}
|
|
restartButton.addEventListener('click', resetWfcStateAndLoadNewImage);
|
|
|
|
function rotatePatternCells(flatCells, pSize, angle) {
|
|
const newFlatCells = new Uint8Array(pSize * pSize * 4);
|
|
if (angle === 0) {
|
|
newFlatCells.set(flatCells);
|
|
return newFlatCells;
|
|
}
|
|
|
|
for (let r_orig = 0; r_orig < pSize; r_orig++) {
|
|
for (let c_orig = 0; c_orig < pSize; c_orig++) {
|
|
const oldIdxBase = (r_orig * pSize + c_orig) * 4;
|
|
let nr, nc;
|
|
|
|
if (angle === 90) {
|
|
nr = c_orig;
|
|
nc = pSize - 1 - r_orig;
|
|
} else if (angle === 180) {
|
|
nr = pSize - 1 - r_orig;
|
|
nc = pSize - 1 - c_orig;
|
|
} else if (angle === 270) {
|
|
nr = pSize - 1 - c_orig;
|
|
nc = r_orig;
|
|
} else {
|
|
newFlatCells.set(flatCells);
|
|
return newFlatCells;
|
|
}
|
|
|
|
const newIdxBase = (nr * pSize + nc) * 4;
|
|
newFlatCells[newIdxBase] = flatCells[oldIdxBase];
|
|
newFlatCells[newIdxBase + 1] = flatCells[oldIdxBase + 1];
|
|
newFlatCells[newIdxBase + 2] = flatCells[oldIdxBase + 2];
|
|
newFlatCells[newIdxBase + 3] = flatCells[oldIdxBase + 3];
|
|
}
|
|
}
|
|
return newFlatCells;
|
|
}
|
|
|
|
function extractPatterns() {
|
|
const countMap = new Map();
|
|
const keyToFlatCells = new Map();
|
|
const pSize = currentPatternSize;
|
|
let rawPatternCounter = 0;
|
|
|
|
for (let y = 0; y <= sampleHeight - pSize; y++) {
|
|
for (let x = 0; x <= sampleWidth - pSize; x++) {
|
|
rawPatternCounter++;
|
|
const originalFlatBlock = new Uint8Array(pSize * pSize * 4);
|
|
let k = 0;
|
|
for (let dy = 0; dy < pSize; dy++) {
|
|
for (let dx = 0; dx < pSize; dx++) {
|
|
const imgPixelY = y + dy;
|
|
const imgPixelX = x + dx;
|
|
const srcIdx = (imgPixelY * sampleWidth + imgPixelX) * 4;
|
|
originalFlatBlock[k++] = srcData[srcIdx];
|
|
originalFlatBlock[k++] = srcData[srcIdx + 1];
|
|
originalFlatBlock[k++] = srcData[srcIdx + 2];
|
|
originalFlatBlock[k++] = srcData[srcIdx + 3];
|
|
}
|
|
}
|
|
|
|
const rotationsToConsider = enableRotations ? [0, 90, 180, 270] : [0];
|
|
|
|
for (const angle of rotationsToConsider) {
|
|
const currentFlatBlockCells = rotatePatternCells(
|
|
originalFlatBlock,
|
|
pSize,
|
|
angle
|
|
);
|
|
const key = Uint8ArrayToHexString(currentFlatBlockCells);
|
|
|
|
countMap.set(key, (countMap.get(key) || 0) + 1);
|
|
if (!keyToFlatCells.has(key)) {
|
|
keyToFlatCells.set(key, currentFlatBlockCells);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
stats.patternExtraction.rawPatternCount =
|
|
rawPatternCounter * (enableRotations ? 4 : 1);
|
|
|
|
patterns = [];
|
|
patternByID = {};
|
|
patternByIndex = [];
|
|
patternIdToIndex = {};
|
|
let idx = 0;
|
|
for (const [key, freq] of countMap.entries()) {
|
|
const patID = 'p' + idx;
|
|
const flatCellsData = keyToFlatCells.get(key);
|
|
const patObj = {
|
|
id: patID,
|
|
cells: flatCellsData,
|
|
frequency: freq,
|
|
index: idx,
|
|
};
|
|
patterns.push(patObj);
|
|
patternByID[patID] = patObj;
|
|
patternByIndex[idx] = patObj;
|
|
patternIdToIndex[patID] = idx;
|
|
idx++;
|
|
}
|
|
}
|
|
|
|
function buildAdjacency() {
|
|
adjacency = {};
|
|
if (!patterns) return;
|
|
const pSize = currentPatternSize;
|
|
let ruleCount = 0;
|
|
|
|
for (const pat of patterns) {
|
|
adjacency[pat.id] = { up: [], down: [], left: [], right: [] };
|
|
}
|
|
|
|
for (const p of patterns) {
|
|
for (const q of patterns) {
|
|
if (p.id === q.id && patterns.length === 1) {
|
|
adjacency[p.id].right.push(q.index);
|
|
ruleCount++;
|
|
adjacency[q.id].left.push(p.index);
|
|
ruleCount++;
|
|
adjacency[p.id].down.push(q.index);
|
|
ruleCount++;
|
|
adjacency[q.id].up.push(p.index);
|
|
ruleCount++;
|
|
continue;
|
|
}
|
|
|
|
let canSitRight = true;
|
|
if (pSize > 1) {
|
|
for (let r = 0; r < pSize; r++) {
|
|
for (let c = 0; c < pSize - 1; c++) {
|
|
for (let ch = 0; ch < 4; ch++) {
|
|
if (
|
|
p.cells[(r * pSize + (c + 1)) * 4 + ch] !==
|
|
q.cells[(r * pSize + c) * 4 + ch]
|
|
) {
|
|
canSitRight = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!canSitRight) break;
|
|
}
|
|
if (!canSitRight) break;
|
|
}
|
|
} else {
|
|
canSitRight = true;
|
|
}
|
|
|
|
if (canSitRight) {
|
|
adjacency[p.id].right.push(q.index);
|
|
ruleCount++;
|
|
adjacency[q.id].left.push(p.index);
|
|
ruleCount++;
|
|
}
|
|
|
|
let canSitDown = true;
|
|
if (pSize > 1) {
|
|
for (let c_col = 0; c_col < pSize; c_col++) {
|
|
// Use c_col to avoid conflict with outer c
|
|
for (let r_row = 0; r_row < pSize - 1; r_row++) {
|
|
// Use r_row
|
|
for (let ch = 0; ch < 4; ch++) {
|
|
if (
|
|
p.cells[((r_row + 1) * pSize + c_col) * 4 + ch] !==
|
|
q.cells[(r_row * pSize + c_col) * 4 + ch]
|
|
) {
|
|
canSitDown = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!canSitDown) break;
|
|
}
|
|
if (!canSitDown) break;
|
|
}
|
|
} else {
|
|
canSitDown = true;
|
|
}
|
|
|
|
if (canSitDown) {
|
|
adjacency[p.id].down.push(q.index);
|
|
ruleCount++;
|
|
adjacency[q.id].up.push(p.index);
|
|
ruleCount++;
|
|
}
|
|
}
|
|
}
|
|
stats.adjacency.totalRules = ruleCount;
|
|
let adjMem = estimateObjectOverhead(adjacency);
|
|
if (adjacency) {
|
|
for (const patId in adjacency) {
|
|
adjMem += estimateStringBytes(patId); // For the key string
|
|
adjMem += estimateObjectOverhead(adjacency[patId]); // For the {up:[], ...} object
|
|
['up', 'down', 'left', 'right'].forEach((dir) => {
|
|
adjMem += estimateStringBytes(dir); // For "up", "down", etc. keys
|
|
adjMem +=
|
|
adjacency[patId][dir].length * (patterns.length > 65535 ? 4 : 2) +
|
|
16; // Array of indices + array overhead
|
|
});
|
|
}
|
|
}
|
|
stats.memory.adjacencyBytes = adjMem;
|
|
}
|
|
|
|
function addDirtyTile(c, r) {
|
|
dirtyTilesForFrame.add(`${c},${r}`);
|
|
}
|
|
|
|
function processAndDrawDirtyTiles() {
|
|
if (dirtyTilesForFrame.size === 0) return;
|
|
dirtyTilesForFrame.forEach((key) => {
|
|
const [c, r] = key.split(',').map(Number);
|
|
if (grid && grid[r] && grid[r][c]) {
|
|
drawTile(c, r);
|
|
}
|
|
});
|
|
dirtyTilesForFrame.clear();
|
|
}
|
|
|
|
function initializeGrid() {
|
|
outputCanvas.width = OUTPUT_SIZE;
|
|
outputCanvas.height = OUTPUT_SIZE;
|
|
|
|
pCols = OUTPUT_SIZE - (currentPatternSize - 1);
|
|
pRows = OUTPUT_SIZE - (currentPatternSize - 1);
|
|
|
|
if (pCols <= 0 || pRows <= 0) {
|
|
statusEl.textContent = `Error: Output size (${OUTPUT_SIZE}x${OUTPUT_SIZE}) is too small for pattern size ${currentPatternSize}x${currentPatternSize}. Resulting pattern grid would be ${pCols}x${pRows} (must be >0).`;
|
|
phase = 'error';
|
|
stats.wfcRun.status = `Error: Output size too small for patterns`;
|
|
return;
|
|
}
|
|
stats.grid.pCols = pCols;
|
|
stats.grid.pRows = pRows;
|
|
stats.grid.initialCellOptions = patterns.length;
|
|
|
|
const allPatternIndices = Array.from(
|
|
{ length: patterns.length },
|
|
(_, i) => i
|
|
);
|
|
grid = Array.from({ length: pRows }, () =>
|
|
Array.from({ length: pCols }, () => [...allPatternIndices])
|
|
);
|
|
queue = [];
|
|
queuedCoordinatesSet.clear();
|
|
maxTries = pCols * pRows * 30 * currentPatternSize;
|
|
currentAttemptFailed = false;
|
|
backtrackStack = [];
|
|
|
|
let oneGridBytes = 16; // Outer array
|
|
if (grid && grid.length > 0 && grid[0] && grid[0].length > 0) {
|
|
oneGridBytes += grid.length * 16; // Each row array
|
|
const cell = grid[0][0]; // Example cell (array of indices)
|
|
const bytesPerIndex =
|
|
patterns.length > 65535 ? 4 : patterns.length > 255 ? 2 : 1; // 1 byte if <256 patterns, 2 if <65536, else 4
|
|
const cellBytes = 16 + cell.length * bytesPerIndex; // Array of indices in cell + its overhead
|
|
oneGridBytes += grid.length * grid[0].length * cellBytes;
|
|
}
|
|
stats.memory.estimatedGridSnapshotBytes = oneGridBytes;
|
|
|
|
outputCtx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);
|
|
for (let r_idx = 0; r_idx < pRows; r_idx++) {
|
|
for (let c_idx = 0; c_idx < pCols; c_idx++) {
|
|
addDirtyTile(c_idx, r_idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
function pickCellWithLowestEntropy() {
|
|
if (!grid) return null;
|
|
let bestEntropy = Infinity;
|
|
let bestCells = [];
|
|
|
|
for (let r = 0; r < pRows; r++) {
|
|
for (let c = 0; c < pCols; c++) {
|
|
if (!grid[r] || !grid[r][c]) continue;
|
|
const numOptions = grid[r][c].length;
|
|
if (numOptions > 1) {
|
|
const currentEntropy = numOptions + Math.random() * 0.1;
|
|
if (currentEntropy < bestEntropy) {
|
|
bestEntropy = currentEntropy;
|
|
bestCells = [[c, r]];
|
|
} else if (numOptions === Math.floor(bestEntropy)) {
|
|
bestCells.push([c, r]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return bestCells.length > 0
|
|
? bestCells[Math.floor(Math.random() * bestCells.length)]
|
|
: null;
|
|
}
|
|
|
|
function collapseCell(c, r) {
|
|
const initialOptionsForCellIndices = grid[r][c];
|
|
if (initialOptionsForCellIndices.length === 0) {
|
|
currentAttemptFailed = true;
|
|
stats.wfcRun.totalContradictions++;
|
|
return;
|
|
}
|
|
|
|
if (backtrackStack.length >= MAX_BACKTRACK_DEPTH) {
|
|
statusEl.textContent = `Max backtrack depth (${MAX_BACKTRACK_DEPTH}) reached at cell (${c},${r}). Forcing backtrack. Stack: ${backtrackStack.length}`;
|
|
currentAttemptFailed = true;
|
|
stats.wfcRun.forcedBacktracksByDepth++;
|
|
stats.wfcRun.totalContradictions++;
|
|
return;
|
|
}
|
|
|
|
let optionsToSystematicallyTry = [...initialOptionsForCellIndices];
|
|
const useRandomSelection = randomSelectionCheckbox.checked;
|
|
|
|
if (useRandomSelection && optionsToSystematicallyTry.length > 1) {
|
|
for (let i = optionsToSystematicallyTry.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[optionsToSystematicallyTry[i], optionsToSystematicallyTry[j]] = [
|
|
optionsToSystematicallyTry[j],
|
|
optionsToSystematicallyTry[i],
|
|
];
|
|
}
|
|
}
|
|
|
|
let chosenPatternIndex;
|
|
let chosenIndexInSystematicList = 0;
|
|
|
|
if (!useRandomSelection && optionsToSystematicallyTry.length > 1) {
|
|
let totalWeight = 0;
|
|
const weights = optionsToSystematicallyTry.map((idx) => {
|
|
const pat = patternByIndex[idx];
|
|
const w = pat && pat.frequency > 0 ? pat.frequency : 1;
|
|
totalWeight += w;
|
|
return w;
|
|
});
|
|
|
|
if (totalWeight === 0) {
|
|
chosenIndexInSystematicList = 0;
|
|
} else {
|
|
let rnd = Math.random() * totalWeight;
|
|
for (let i = 0; i < optionsToSystematicallyTry.length; i++) {
|
|
rnd -= weights[i];
|
|
if (rnd <= 0) {
|
|
chosenIndexInSystematicList = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (typeof chosenIndexInSystematicList === 'undefined') {
|
|
chosenIndexInSystematicList = optionsToSystematicallyTry.length - 1;
|
|
}
|
|
const actualChosenItemIndex = optionsToSystematicallyTry.splice(
|
|
chosenIndexInSystematicList,
|
|
1
|
|
)[0];
|
|
optionsToSystematicallyTry.unshift(actualChosenItemIndex);
|
|
chosenIndexInSystematicList = 0;
|
|
}
|
|
|
|
chosenPatternIndex =
|
|
optionsToSystematicallyTry[chosenIndexInSystematicList];
|
|
|
|
if (typeof chosenPatternIndex === 'undefined') {
|
|
currentAttemptFailed = true;
|
|
stats.wfcRun.totalContradictions++;
|
|
return;
|
|
}
|
|
|
|
const gridSnapshot = deepCopyGrid(grid);
|
|
const queueSnapshot = deepCopyQueue(queue);
|
|
const queuedCoordinatesSetSnapshot = new Set(queuedCoordinatesSet);
|
|
|
|
backtrackStack.push({
|
|
gridBeforeChoice: gridSnapshot,
|
|
queueBeforeChoice: queueSnapshot,
|
|
queuedCoordinatesSetBeforeChoice: queuedCoordinatesSetSnapshot,
|
|
cellCoords: [c, r],
|
|
optionsAtCell: optionsToSystematicallyTry,
|
|
lastTriedOptionIndexInList: chosenIndexInSystematicList,
|
|
});
|
|
stats.wfcRun.maxBacktrackStackDepthReached = Math.max(
|
|
stats.wfcRun.maxBacktrackStackDepthReached,
|
|
backtrackStack.length
|
|
);
|
|
stats.memory.backtrackStackPeakBytes = Math.max(
|
|
stats.memory.backtrackStackPeakBytes,
|
|
backtrackStack.length * stats.memory.estimatedGridSnapshotBytes
|
|
);
|
|
|
|
grid[r][c] = [chosenPatternIndex];
|
|
stats.wfcRun.cellsCollapsed++;
|
|
addDirtyTile(c, r);
|
|
|
|
const coordKey = `${c},${r}`;
|
|
if (!queuedCoordinatesSet.has(coordKey)) {
|
|
queue.push([c, r]);
|
|
queuedCoordinatesSet.add(coordKey);
|
|
}
|
|
}
|
|
|
|
function getPatternNeighbors(c, r) {
|
|
return [
|
|
[c - 1, r, 'left'],
|
|
[c + 1, r, 'right'],
|
|
[c, r - 1, 'up'],
|
|
[c, r + 1, 'down'],
|
|
].filter(([nc, nr]) => nc >= 0 && nr >= 0 && nc < pCols && nr < pRows);
|
|
}
|
|
|
|
function propagateStep() {
|
|
stats.wfcRun.propagationSteps++;
|
|
if (queue.length === 0) return { success: true };
|
|
|
|
const [c, r] = queue.shift();
|
|
queuedCoordinatesSet.delete(`${c},${r}`);
|
|
|
|
if (!grid || !grid[r] || !grid[r][c]) {
|
|
currentAttemptFailed = true;
|
|
stats.wfcRun.totalContradictions++;
|
|
return { success: false };
|
|
}
|
|
const currentPatternIndicesInCell = grid[r][c];
|
|
if (currentPatternIndicesInCell.length === 0) {
|
|
currentAttemptFailed = true;
|
|
stats.wfcRun.totalContradictions++;
|
|
return { success: false };
|
|
}
|
|
|
|
for (const [nc, nr, dirToNeighbor] of getPatternNeighbors(c, r)) {
|
|
if (!grid[nr] || !grid[nr][nc]) {
|
|
currentAttemptFailed = true;
|
|
stats.wfcRun.totalContradictions++;
|
|
return { success: false };
|
|
}
|
|
const originalNeighborOptionIndices = grid[nr][nc];
|
|
if (originalNeighborOptionIndices.length === 0) {
|
|
currentAttemptFailed = true;
|
|
stats.wfcRun.totalContradictions++;
|
|
return { success: false };
|
|
}
|
|
|
|
let cumulativeAllowedNeighborPatternIndices = new Set();
|
|
currentPatternIndicesInCell.forEach((p_idx) => {
|
|
const p_id_str = patternByIndex[p_idx].id;
|
|
if (!adjacency[p_id_str]) return;
|
|
|
|
let compatiblePatternIndicesForNeighbor;
|
|
if (dirToNeighbor === 'right')
|
|
compatiblePatternIndicesForNeighbor = adjacency[p_id_str].right;
|
|
else if (dirToNeighbor === 'left')
|
|
compatiblePatternIndicesForNeighbor = adjacency[p_id_str].left;
|
|
else if (dirToNeighbor === 'up')
|
|
compatiblePatternIndicesForNeighbor = adjacency[p_id_str].up;
|
|
else if (dirToNeighbor === 'down')
|
|
compatiblePatternIndicesForNeighbor = adjacency[p_id_str].down;
|
|
else return;
|
|
|
|
compatiblePatternIndicesForNeighbor.forEach((q_idx) =>
|
|
cumulativeAllowedNeighborPatternIndices.add(q_idx)
|
|
);
|
|
});
|
|
|
|
const newNeighborOptionIndices = originalNeighborOptionIndices.filter(
|
|
(idx) => cumulativeAllowedNeighborPatternIndices.has(idx)
|
|
);
|
|
|
|
if (newNeighborOptionIndices.length === 0) {
|
|
currentAttemptFailed = true;
|
|
stats.wfcRun.totalContradictions++;
|
|
return { success: false };
|
|
}
|
|
|
|
if (
|
|
newNeighborOptionIndices.length < originalNeighborOptionIndices.length
|
|
) {
|
|
grid[nr][nc] = newNeighborOptionIndices;
|
|
addDirtyTile(nc, nr);
|
|
|
|
const neighborCoordKey = `${nc},${nr}`;
|
|
if (!queuedCoordinatesSet.has(neighborCoordKey)) {
|
|
queue.push([nc, nr]);
|
|
queuedCoordinatesSet.add(neighborCoordKey);
|
|
}
|
|
}
|
|
}
|
|
return { success: true };
|
|
}
|
|
|
|
function drawCollapsedPatternForCell(c, r) {
|
|
const patIndex = grid[r][c][0];
|
|
const pat = patternByIndex[patIndex];
|
|
const pSize = currentPatternSize;
|
|
|
|
if (!pat || !pat.cells) {
|
|
outputCtx.fillStyle = 'rgba(255, 0, 255, 0.7)';
|
|
outputCtx.fillRect(c, r, pSize, pSize);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const imageData = outputCtx.createImageData(pSize, pSize);
|
|
imageData.data.set(pat.cells);
|
|
outputCtx.putImageData(imageData, c, r);
|
|
} catch (e) {
|
|
console.error(
|
|
'Error drawing collapsed cell:',
|
|
e,
|
|
'Pattern Index:',
|
|
patIndex,
|
|
'Coords:',
|
|
c,
|
|
r
|
|
);
|
|
outputCtx.fillStyle = 'rgba(255, 165, 0, 0.7)';
|
|
outputCtx.fillRect(c, r, pSize, pSize);
|
|
}
|
|
}
|
|
|
|
function drawAveragePatternForCell(c, r) {
|
|
const possiblePatternIndices = grid[r][c];
|
|
if (!possiblePatternIndices || possiblePatternIndices.length === 0) return;
|
|
|
|
const pSize = currentPatternSize;
|
|
const tileImageData = outputCtx.createImageData(pSize, pSize);
|
|
const tileData = tileImageData.data;
|
|
|
|
for (let dy = 0; dy < pSize; dy++) {
|
|
for (let dx = 0; dx < pSize; dx++) {
|
|
let sumR = 0,
|
|
sumG = 0,
|
|
sumB = 0,
|
|
sumA = 0;
|
|
let count = 0;
|
|
for (const patIndex of possiblePatternIndices) {
|
|
const pattern = patternByIndex[patIndex];
|
|
if (pattern && pattern.cells) {
|
|
const pixelStartIndex = (dy * pSize + dx) * 4;
|
|
if (pixelStartIndex + 3 < pattern.cells.length) {
|
|
sumR += pattern.cells[pixelStartIndex];
|
|
sumG += pattern.cells[pixelStartIndex + 1];
|
|
sumB += pattern.cells[pixelStartIndex + 2];
|
|
sumA += pattern.cells[pixelStartIndex + 3];
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
const dataIdx = (dy * pSize + dx) * 4;
|
|
if (count > 0) {
|
|
tileData[dataIdx] = Math.round(sumR / count);
|
|
tileData[dataIdx + 1] = Math.round(sumG / count);
|
|
tileData[dataIdx + 2] = Math.round(sumB / count);
|
|
tileData[dataIdx + 3] = Math.round(sumA / count);
|
|
} else {
|
|
tileData[dataIdx] = 60;
|
|
tileData[dataIdx + 1] = 60;
|
|
tileData[dataIdx + 2] = 60;
|
|
tileData[dataIdx + 3] = 128;
|
|
}
|
|
}
|
|
}
|
|
outputCtx.putImageData(tileImageData, c, r);
|
|
}
|
|
|
|
function drawTile(c, r) {
|
|
if (!grid || !grid[r] || !grid[r][c]) return;
|
|
const numOptions = grid[r][c].length;
|
|
|
|
if (numOptions === 1) {
|
|
drawCollapsedPatternForCell(c, r);
|
|
} else if (numOptions > 1) {
|
|
drawAveragePatternForCell(c, r);
|
|
} else {
|
|
outputCtx.fillStyle = 'rgba(255, 0, 0, 0.7)';
|
|
outputCtx.fillRect(c, r, currentPatternSize, currentPatternSize);
|
|
}
|
|
}
|
|
|
|
function allCollapsed() {
|
|
if (!grid) return false;
|
|
for (let r_idx = 0; r_idx < pRows; r_idx++) {
|
|
for (let c_idx = 0; c_idx < pCols; c_idx++) {
|
|
if (
|
|
!grid[r_idx] ||
|
|
!grid[r_idx][c_idx] ||
|
|
grid[r_idx][c_idx].length > 1
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
let lastStatsRenderTime = 0;
|
|
const STATS_RENDER_INTERVAL = 250;
|
|
|
|
function runStep() {
|
|
if (
|
|
phase === 'error' ||
|
|
phase === 'loading' ||
|
|
phase === 'idle' ||
|
|
phase === 'loading_image_error'
|
|
) {
|
|
processAndDrawDirtyTiles();
|
|
if (
|
|
phase === 'error' ||
|
|
phase === 'loading_image_error' ||
|
|
phase === 'done'
|
|
) {
|
|
if (stats.wfcRun && stats.wfcRun.startTime > 0) {
|
|
// Ensure startTime was set
|
|
stats.wfcRun.endTime = performance.now();
|
|
stats.wfcRun.durationMs =
|
|
stats.wfcRun.endTime - stats.wfcRun.startTime;
|
|
} else if (stats.wfcRun) {
|
|
stats.wfcRun.durationMs = 0; // Or some indicator it didn't run
|
|
}
|
|
}
|
|
renderStats();
|
|
return;
|
|
}
|
|
|
|
if (phase === 'collapse') {
|
|
if (currentAttemptFailed) {
|
|
currentAttemptFailed = false;
|
|
let backtrackedSuccessfully = false;
|
|
stats.wfcRun.totalBacktracks++;
|
|
|
|
while (backtrackStack.length > 0) {
|
|
const lastDecision = backtrackStack.pop();
|
|
const {
|
|
gridBeforeChoice,
|
|
queueBeforeChoice,
|
|
queuedCoordinatesSetBeforeChoice,
|
|
cellCoords,
|
|
optionsAtCell,
|
|
lastTriedOptionIndexInList,
|
|
} = lastDecision;
|
|
|
|
const [cc, cr] = cellCoords;
|
|
let nextOptionIndexToTryInList = lastTriedOptionIndexInList + 1;
|
|
|
|
if (nextOptionIndexToTryInList < optionsAtCell.length) {
|
|
grid = deepCopyGrid(gridBeforeChoice);
|
|
queue = deepCopyQueue(queueBeforeChoice);
|
|
queuedCoordinatesSet = new Set(queuedCoordinatesSetBeforeChoice);
|
|
|
|
const newChosenPatternIndex =
|
|
optionsAtCell[nextOptionIndexToTryInList];
|
|
grid[cr][cc] = [newChosenPatternIndex];
|
|
addDirtyTile(cc, cr);
|
|
|
|
const coordKeyBacktrack = `${cc},${cr}`;
|
|
if (!queuedCoordinatesSet.has(coordKeyBacktrack)) {
|
|
queue.push([cc, cr]);
|
|
queuedCoordinatesSet.add(coordKeyBacktrack);
|
|
}
|
|
|
|
if (backtrackStack.length < MAX_BACKTRACK_DEPTH) {
|
|
backtrackStack.push({
|
|
gridBeforeChoice: gridBeforeChoice,
|
|
queueBeforeChoice: queueBeforeChoice,
|
|
queuedCoordinatesSetBeforeChoice:
|
|
queuedCoordinatesSetBeforeChoice,
|
|
cellCoords: cellCoords,
|
|
optionsAtCell: optionsAtCell,
|
|
lastTriedOptionIndexInList: nextOptionIndexToTryInList,
|
|
});
|
|
stats.wfcRun.maxBacktrackStackDepthReached = Math.max(
|
|
stats.wfcRun.maxBacktrackStackDepthReached,
|
|
backtrackStack.length
|
|
);
|
|
stats.memory.backtrackStackPeakBytes = Math.max(
|
|
stats.memory.backtrackStackPeakBytes,
|
|
backtrackStack.length * stats.memory.estimatedGridSnapshotBytes
|
|
);
|
|
} else {
|
|
console.warn(
|
|
'Backtrack: At MAX_BACKTRACK_DEPTH after pop and finding new option. This new choice cannot be further backtracked if it fails immediately due to depth limit on next step.'
|
|
);
|
|
}
|
|
|
|
statusEl.textContent = `Backtracked. Retrying option ${
|
|
nextOptionIndexToTryInList + 1
|
|
}/${optionsAtCell.length} at (${cc},${cr}). Stack: ${
|
|
backtrackStack.length
|
|
}`;
|
|
phase = 'propagate';
|
|
backtrackedSuccessfully = true;
|
|
|
|
outputCtx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);
|
|
for (let r_idx = 0; r_idx < pRows; r_idx++) {
|
|
for (let c_idx = 0; c_idx < pCols; c_idx++) {
|
|
if (grid[r_idx] && grid[r_idx][c_idx]) {
|
|
addDirtyTile(c_idx, r_idx);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!backtrackedSuccessfully) {
|
|
statusEl.textContent =
|
|
'Backtrack stack exhausted → WFC failed. Restarting WFC.';
|
|
stats.wfcRun.status =
|
|
'Failed (Backtrack stack exhausted), Restarting...';
|
|
renderStats();
|
|
if (patterns && patterns.length > 0) {
|
|
initializeGrid(); // This will reset some grid stats
|
|
|
|
stats.wfcRun.startTime = performance.now(); // Reset for the new attempt
|
|
stats.wfcRun.endTime = 0;
|
|
stats.wfcRun.durationMs = 0;
|
|
stats.wfcRun.maxBacktrackStackDepthReached = 0;
|
|
stats.wfcRun.totalBacktracks = 0;
|
|
stats.wfcRun.totalContradictions = 0;
|
|
stats.wfcRun.forcedBacktracksByDepth = 0;
|
|
stats.wfcRun.cellsCollapsed = 0;
|
|
stats.wfcRun.propagationSteps = 0;
|
|
stats.memory.backtrackStackPeakBytes = 0; // Reset peak for this new run
|
|
|
|
if (phase === 'error') {
|
|
processAndDrawDirtyTiles();
|
|
renderStats();
|
|
return;
|
|
}
|
|
phase = 'collapse';
|
|
stats.wfcRun.status = 'Running WFC (Restarted after fail)';
|
|
} else {
|
|
phase = 'error';
|
|
stats.wfcRun.status = 'Error: Cannot restart, no patterns';
|
|
statusEl.textContent =
|
|
'Cannot restart: No patterns loaded. Load an image first.';
|
|
}
|
|
processAndDrawDirtyTiles();
|
|
renderStats();
|
|
if (phase !== 'collapse') animationFrameId = null;
|
|
else animationFrameId = requestAnimationFrame(runStep);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (allCollapsed()) {
|
|
phase = 'done';
|
|
stats.wfcRun.status = 'WFC Complete!';
|
|
stats.wfcRun.endTime = performance.now();
|
|
stats.wfcRun.durationMs = stats.wfcRun.endTime - stats.wfcRun.startTime;
|
|
statusEl.textContent = 'WFC complete!';
|
|
processAndDrawDirtyTiles();
|
|
renderStats();
|
|
animationFrameId = null;
|
|
return;
|
|
}
|
|
|
|
if (maxTries-- <= 0 && phase !== 'propagate') {
|
|
statusEl.textContent = 'Max tries reached → Restarting WFC (timeout).';
|
|
stats.wfcRun.status = 'Failed (Timeout), Restarting...';
|
|
renderStats();
|
|
if (patterns && patterns.length > 0) {
|
|
initializeGrid();
|
|
stats.wfcRun.startTime = performance.now();
|
|
stats.wfcRun.endTime = 0;
|
|
stats.wfcRun.durationMs = 0;
|
|
stats.wfcRun.maxBacktrackStackDepthReached = 0;
|
|
stats.wfcRun.totalBacktracks = 0;
|
|
stats.wfcRun.totalContradictions = 0;
|
|
stats.wfcRun.forcedBacktracksByDepth = 0;
|
|
stats.wfcRun.cellsCollapsed = 0;
|
|
stats.wfcRun.propagationSteps = 0;
|
|
stats.memory.backtrackStackPeakBytes = 0;
|
|
if (phase === 'error') {
|
|
processAndDrawDirtyTiles();
|
|
renderStats();
|
|
return;
|
|
}
|
|
phase = 'collapse';
|
|
stats.wfcRun.status = 'Running WFC (Restarted after timeout)';
|
|
} else {
|
|
phase = 'error';
|
|
stats.wfcRun.status = 'Error: Cannot restart, no patterns';
|
|
statusEl.textContent = 'Cannot restart: No patterns loaded.';
|
|
}
|
|
processAndDrawDirtyTiles();
|
|
renderStats();
|
|
if (phase !== 'collapse') animationFrameId = null;
|
|
else animationFrameId = requestAnimationFrame(runStep);
|
|
return;
|
|
}
|
|
|
|
if (phase === 'collapse') {
|
|
const cellToCollapse = pickCellWithLowestEntropy();
|
|
if (!cellToCollapse) {
|
|
if (!allCollapsed()) {
|
|
statusEl.textContent =
|
|
'Error: No cell to collapse, but not all collapsed. Contradiction.';
|
|
stats.wfcRun.status = 'Error: No cell to collapse (Contradiction)';
|
|
currentAttemptFailed = true;
|
|
stats.wfcRun.totalContradictions++;
|
|
} else {
|
|
phase = 'done';
|
|
stats.wfcRun.status = 'WFC Complete (no cell, all done)';
|
|
stats.wfcRun.endTime = performance.now();
|
|
stats.wfcRun.durationMs =
|
|
stats.wfcRun.endTime - stats.wfcRun.startTime;
|
|
statusEl.textContent =
|
|
'WFC complete (no cell to collapse, all done).';
|
|
}
|
|
} else {
|
|
collapseCell(cellToCollapse[0], cellToCollapse[1]);
|
|
if (!currentAttemptFailed) {
|
|
phase = 'propagate';
|
|
}
|
|
}
|
|
}
|
|
} else if (phase === 'propagate') {
|
|
let processedInFrame = 0;
|
|
const MAX_PROP_PER_FRAME = Math.max(30, Math.floor((pCols * pRows) / 15));
|
|
|
|
while (
|
|
processedInFrame++ < MAX_PROP_PER_FRAME &&
|
|
queue.length > 0 &&
|
|
!currentAttemptFailed
|
|
) {
|
|
propagateStep();
|
|
}
|
|
|
|
if (queue.length === 0 || currentAttemptFailed) {
|
|
phase = 'collapse';
|
|
}
|
|
}
|
|
|
|
processAndDrawDirtyTiles();
|
|
|
|
const now = performance.now();
|
|
if (now - lastStatsRenderTime > STATS_RENDER_INTERVAL) {
|
|
if (
|
|
phase !== 'done' &&
|
|
phase !== 'error' &&
|
|
phase !== 'loading_image_error' &&
|
|
stats.wfcRun.startTime > 0
|
|
) {
|
|
stats.wfcRun.durationMs = now - stats.wfcRun.startTime;
|
|
}
|
|
renderStats();
|
|
lastStatsRenderTime = now;
|
|
}
|
|
|
|
if (
|
|
phase !== 'done' &&
|
|
phase !== 'error' &&
|
|
phase !== 'loading_image_error'
|
|
) {
|
|
animationFrameId = requestAnimationFrame(runStep);
|
|
} else {
|
|
// Done, error, or loading_image_error
|
|
if (
|
|
stats.wfcRun &&
|
|
stats.wfcRun.startTime > 0 &&
|
|
stats.wfcRun.endTime === 0
|
|
) {
|
|
// Ensure endTime is set if run started
|
|
stats.wfcRun.endTime = performance.now();
|
|
stats.wfcRun.durationMs = stats.wfcRun.endTime - stats.wfcRun.startTime;
|
|
} else if (stats.wfcRun && stats.wfcRun.startTime === 0) {
|
|
stats.wfcRun.durationMs = 0; // Didn't really run
|
|
}
|
|
renderStats(); // Final render for terminal states
|
|
animationFrameId = null;
|
|
}
|
|
}
|
|
|
|
resetWfcStateAndLoadNewImage();
|
|
})();
|