test_repo/old_main.js
2025-06-05 22:11:10 +02:00

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();
})();