diff --git a/index.html b/index.html
new file mode 100644
index 0000000..c99820e
--- /dev/null
+++ b/index.html
@@ -0,0 +1,85 @@
+
+
+
+
+ WFC with Web Workers
+
+
+
+
+
+
+
+
+
+
+
+
Loading...
+
+
+
Source Image (scaled)
+
+
+
+
Output
+
+
+ WFC Statistics will appear here.
+
+
+
diff --git a/old_main.js b/old_main.js
new file mode 100644
index 0000000..b9e4850
--- /dev/null
+++ b/old_main.js
@@ -0,0 +1,1270 @@
+(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 = `WFC Statistics:
`;
+ html += `Status: ${stats.wfcRun.status}
`;
+ if (stats.wfcRun.durationMs > 0) {
+ html += `Total Time: ${(stats.wfcRun.durationMs / 1000).toFixed(
+ 2
+ )}s
`;
+ }
+ html += `
`;
+
+ if (stats.sourceImage.loaded) {
+ html += `Source Image:
`;
+ html += ` Dimensions: ${stats.sourceImage.width}x${stats.sourceImage.height}
`;
+ html += ` Pattern Size: ${currentPatternSize}x${currentPatternSize}
`;
+ html += ` Rotations Enabled: ${stats.patternExtraction.rotationsEnabled}
`;
+ html += `
`;
+
+ html += `Pattern Extraction (${(
+ stats.patternExtraction.timeMs / 1000
+ ).toFixed(2)}s):
`;
+ html += ` Raw Overlapping Patterns: ${stats.patternExtraction.rawPatternCount}
`;
+ html += ` Unique Patterns: ${stats.patternExtraction.uniquePatternCount}
`;
+ html += ` Memory (Pattern Objects): ~${formatBytes(
+ stats.memory.patternsArrayBytes
+ )}
`;
+ html += ` Memory (Pattern Pixel Data): ~${formatBytes(
+ stats.memory.patternCellsBytes
+ )}
`;
+ html += ` Memory (Pattern ID Map): ~${formatBytes(
+ stats.memory.patternByIdBytes
+ )}
`;
+ html += `
`;
+
+ html += `Adjacency Rules (${(
+ stats.adjacency.timeMs / 1000
+ ).toFixed(2)}s):
`;
+ html += ` Total Adjacency Rules: ${stats.adjacency.totalRules}
`;
+ html += ` Memory (Adjacency Map): ~${formatBytes(
+ stats.memory.adjacencyBytes
+ )}
`;
+ html += `
`;
+
+ html += `Output Grid (${(stats.grid.timeMs / 1000).toFixed(
+ 2
+ )}s initialization):
`;
+ html += ` Output Grid Pixel Size: ${OUTPUT_SIZE}x${OUTPUT_SIZE}
`;
+ html += ` Pattern Grid Dimensions: ${stats.grid.pCols}x${
+ stats.grid.pRows
+ } (${stats.grid.pCols * stats.grid.pRows} cells)
`;
+ html += ` Initial Options/Cell: ${stats.grid.initialCellOptions}
`;
+ html += ` Est. Memory per Grid State: ~${formatBytes(
+ stats.memory.estimatedGridSnapshotBytes
+ )}
`;
+ html += `
`;
+ }
+
+ html += `WFC Run Dynamics:
`;
+ html += ` Max Backtrack Stack Depth: ${stats.wfcRun.maxBacktrackStackDepthReached} / ${MAX_BACKTRACK_DEPTH}
`;
+ html += ` Est. Peak Backtrack Stack Memory: ~${formatBytes(
+ stats.memory.backtrackStackPeakBytes
+ )}
`;
+ html += ` Total Backtracks: ${stats.wfcRun.totalBacktracks}
`;
+ html += ` Forced Backtracks (Max Depth): ${stats.wfcRun.forcedBacktracksByDepth}
`;
+ html += ` Total Contradictions: ${stats.wfcRun.totalContradictions}
`;
+ html += ` Cells Collapsed: ${stats.wfcRun.cellsCollapsed}
`;
+ html += ` Propagation Steps: ${stats.wfcRun.propagationSteps}
`;
+
+ 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();
+})();
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..828caf6
--- /dev/null
+++ b/script.js
@@ -0,0 +1,970 @@
+// Main script (script.js)
+(function () {
+ let currentImageUrl =
+ 'https://raw.githubusercontent.com/mxgmn/WaveFunctionCollapse/master/samples/Flowers.png';
+ // To test with the uploaded image, you'd change the URL to 'input_file_0.png'
+ // For now, let's assume Flowers.png or a similar valid, varied image URL.
+ // currentImageUrl = 'input_file_0.png'; // If you want to test with the local file (requires serving)
+
+ let currentPatternSize = 3;
+ let enableRotations = false;
+ let OUTPUT_SIZE = 64;
+ const SOURCE_VISUAL_SCALE = 4;
+ const MAX_BACKTRACK_DEPTH = 4096;
+
+ 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 phase = 'idle';
+ let dirtyTilesForFrame = new Set();
+ let animationFrameId = null;
+
+ let setupWorker;
+ let solverWorker;
+
+ let stats = {};
+ let patternsData = {
+ patterns: null,
+ patternByID: null,
+ patternIdToIndex: null,
+ adjacency: null,
+ patternByIndexForDrawing: null,
+ };
+
+ 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'];
+ 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;
+ }
+
+ function estimateObjectOverhead(obj) {
+ if (!obj) return 0;
+ return (
+ Object.keys(obj).length *
+ (estimateStringBytes('key_placeholder_avg') + 8 + 4) +
+ 16
+ );
+ }
+
+ function renderStats() {
+ if (!statsContainer) return;
+ let html = `WFC Statistics:
`;
+ html += `Status: ${stats.wfcRun.status}
`;
+ if (stats.wfcRun.durationMs > 0) {
+ html += `Total Time: ${(stats.wfcRun.durationMs / 1000).toFixed(
+ 2
+ )}s
`;
+ }
+ html += `
`;
+
+ if (stats.sourceImage.loaded) {
+ html += `Source Image:
`;
+ html += ` Dimensions: ${stats.sourceImage.width}x${stats.sourceImage.height}
`;
+ html += ` Pattern Size: ${currentPatternSize}x${currentPatternSize}
`;
+ html += ` Rotations Enabled: ${stats.patternExtraction.rotationsEnabled}
`;
+ html += `
`;
+
+ html += `Pattern Extraction (${(
+ stats.patternExtraction.timeMs / 1000
+ ).toFixed(2)}s):
`;
+ html += ` Raw Overlapping Patterns: ${stats.patternExtraction.rawPatternCount}
`;
+ html += ` Unique Patterns: ${stats.patternExtraction.uniquePatternCount}
`;
+ html += ` Memory (Pattern Objects): ~${formatBytes(
+ stats.memory.patternsArrayBytes
+ )}
`;
+ html += ` Memory (Pattern Pixel Data): ~${formatBytes(
+ stats.memory.patternCellsBytes
+ )}
`;
+ html += ` Memory (Pattern ID Map): ~${formatBytes(
+ stats.memory.patternByIdBytes
+ )}
`;
+ html += `
`;
+
+ html += `Adjacency Rules (${(
+ stats.adjacency.timeMs / 1000
+ ).toFixed(2)}s):
`;
+ html += ` Total Adjacency Rules: ${stats.adjacency.totalRules}
`;
+ html += ` Memory (Adjacency Map): ~${formatBytes(
+ stats.memory.adjacencyBytes
+ )}
`;
+ html += `
`;
+
+ html += `Output Grid (${(stats.grid.timeMs / 1000).toFixed(
+ 2
+ )}s initialization):
`;
+ html += ` Output Grid Pixel Size: ${OUTPUT_SIZE * currentPatternSize}x${
+ OUTPUT_SIZE * currentPatternSize
+ } (pixels)
`;
+ html += ` Pattern Grid Dimensions: ${stats.grid.pCols}x${
+ stats.grid.pRows
+ } (${stats.grid.pCols * stats.grid.pRows} cells)
`;
+ html += ` Initial Options/Cell: ${stats.grid.initialCellOptions}
`;
+ html += ` Est. Memory per Grid State (Solver): ~${formatBytes(
+ stats.memory.estimatedGridSnapshotBytes
+ )}
`;
+ html += `
`;
+ }
+
+ html += `WFC Run Dynamics:
`;
+ html += ` Max Backtrack Stack Depth: ${stats.wfcRun.maxBacktrackStackDepthReached} / ${MAX_BACKTRACK_DEPTH}
`;
+ html += ` Est. Peak Backtrack Stack Memory (Solver): ~${formatBytes(
+ stats.memory.backtrackStackPeakBytes
+ )}
`;
+ html += ` Total Backtracks: ${stats.wfcRun.totalBacktracks}
`;
+ html += ` Forced Backtracks (Max Depth): ${stats.wfcRun.forcedBacktracksByDepth}
`;
+ html += ` Total Contradictions: ${stats.wfcRun.totalContradictions}
`;
+ html += ` Cells Collapsed: ${stats.wfcRun.cellsCollapsed}
`;
+ html += ` Propagation Steps: ${stats.wfcRun.propagationSteps}
`;
+
+ statsContainer.innerHTML = html;
+ }
+
+ imageUrlInput.value = currentImageUrl;
+ patternSizeSelect.value = currentPatternSize.toString();
+ enableRotationsCheckbox.checked = enableRotations;
+
+ const img = new Image();
+ img.crossOrigin = 'Anonymous'; // Keep this for external images
+
+ img.onload = () => {
+ if (phase === 'loading_image_error') return;
+ phase = 'processing_setup';
+ stats.wfcRun.status = 'Processing Image...';
+ renderStats();
+
+ const sampleWidth = img.width;
+ const sampleHeight = img.height;
+
+ console.log(
+ `[Main Thread] img.onload: Image URL: ${img.src}, Loaded dimensions: ${sampleWidth}x${sampleHeight}`
+ );
+ if (sampleWidth === 0 || sampleHeight === 0) {
+ console.error(
+ '[Main Thread] Image loaded with zero dimensions. Cannot proceed.'
+ );
+ statusEl.textContent = `Error: Image loaded with zero dimensions.`;
+ phase = 'error';
+ stats.wfcRun.status = 'Error: Image zero dimensions';
+ renderStats();
+ return;
+ }
+
+ stats.sourceImage = {
+ loaded: true,
+ width: sampleWidth,
+ height: sampleHeight,
+ pixels: sampleWidth * sampleHeight,
+ };
+
+ sourceCanvas.width = sampleWidth;
+ sourceCanvas.height = sampleHeight;
+ console.log(
+ `[Main Thread] sourceCanvas dimensions set to: ${sourceCanvas.width}x${sourceCanvas.height}`
+ );
+
+ let beforeData = [0, 0, 0, 0]; // Default if getImageData fails
+ try {
+ // Clear canvas to a known state (e.g., transparent black) before drawing new image
+ // This helps ensure we are not reading stale data from a previous run if canvas is reused.
+ sourceCtx.clearRect(0, 0, sourceCanvas.width, sourceCanvas.height);
+ beforeData = sourceCtx.getImageData(0, 0, 1, 1).data;
+ console.log(
+ `[Main Thread] sourceCanvas pixel (0,0) AFTER clearRect & BEFORE drawImage: (${beforeData[0]},${beforeData[1]},${beforeData[2]},${beforeData[3]})`
+ );
+ } catch (e) {
+ console.error(
+ '[Main Thread] Error getting pixel data BEFORE drawImage (after clearRect):',
+ e
+ );
+ }
+
+ sourceCtx.drawImage(img, 0, 0);
+ console.log('[Main Thread] sourceCtx.drawImage(img, 0, 0) called.');
+
+ 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';
+
+ setTimeout(() => {
+ if (phase !== 'processing_setup') {
+ console.log(
+ '[Main Thread] setTimeout: Phase changed during timeout, aborting worker start.'
+ );
+ return;
+ }
+
+ 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;
+ }
+
+ let afterData = [0, 0, 0, 0];
+ try {
+ afterData = sourceCtx.getImageData(0, 0, 1, 1).data;
+ console.log(
+ `[Main Thread] setTimeout: sourceCanvas pixel (0,0) AFTER drawImage: (${afterData[0]},${afterData[1]},${afterData[2]},${afterData[3]})`
+ );
+
+ if (
+ afterData[0] === 191 &&
+ afterData[1] === 232 &&
+ afterData[2] === 242
+ ) {
+ console.warn(
+ "[Main Thread] setTimeout WARNING: Pixel (0,0) is the uniform blue. 'drawImage' might not have rendered the image correctly or canvas is being cleared to this color."
+ );
+ } else if (
+ afterData[0] === 0 &&
+ afterData[1] === 0 &&
+ afterData[2] === 0 &&
+ afterData[3] === 0 &&
+ beforeData[3] !== 0
+ ) {
+ console.warn(
+ "[Main Thread] setTimeout WARNING: Pixel (0,0) is transparent black. 'drawImage' might have cleared it or image is transparent."
+ );
+ } else if (
+ afterData[0] === beforeData[0] &&
+ afterData[1] === beforeData[1] &&
+ afterData[2] === beforeData[2] &&
+ afterData[3] === beforeData[3] &&
+ (afterData[0] !== 0 ||
+ afterData[1] !== 0 ||
+ afterData[2] !== 0 ||
+ beforeData[3] !== 0)
+ ) {
+ // Only warn if the 'before' state wasn't already transparent black
+ if (
+ !(
+ beforeData[0] === 0 &&
+ beforeData[1] === 0 &&
+ beforeData[2] === 0 &&
+ beforeData[3] === 0
+ )
+ ) {
+ console.warn(
+ "[Main Thread] setTimeout WARNING: Pixel (0,0) did NOT change after drawImage. 'drawImage' likely had no effect or image is fully transparent."
+ );
+ }
+ }
+ } catch (e) {
+ console.error(
+ '[Main Thread] setTimeout: Error getting pixel data AFTER drawImage:',
+ e
+ );
+ }
+
+ console.log(
+ '[Main Thread] setTimeout: About to getImageData for full srcData. sourceCanvas dimensions:',
+ sourceCanvas.width,
+ sourceCanvas.height
+ );
+ const srcData = sourceCtx.getImageData(
+ 0,
+ 0,
+ sampleWidth,
+ sampleHeight
+ ).data;
+
+ if (srcData && srcData.length > 0) {
+ let mainThreadPixelDataSample = [];
+ for (let i = 0; i < Math.min(100, srcData.length); i += 4) {
+ mainThreadPixelDataSample.push(
+ `(${srcData[i]},${srcData[i + 1]},${srcData[i + 2]},${
+ srcData[i + 3]
+ })`
+ );
+ }
+ console.log(
+ '[Main Thread] setTimeout: srcData first ~25 pixels after getImageData:',
+ mainThreadPixelDataSample.join(' ')
+ );
+
+ if (srcData[0] === 191 && srcData[1] === 232 && srcData[2] === 242) {
+ console.error(
+ '[Main Thread] setTimeout CRITICAL: Full getImageData is returning the uniform blue color. Problem with canvas painting before getImageData is definitive.'
+ );
+ } else if (
+ srcData.every((val, index) =>
+ index % 4 === 3 ? val === 0 : val === 0
+ )
+ ) {
+ // Check if all data is transparent black
+ console.warn(
+ '[Main Thread] setTimeout WARNING: Full getImageData is returning all zeros (transparent black).'
+ );
+ }
+ } else {
+ console.error(
+ '[Main Thread] setTimeout: srcData is null or empty after getImageData!'
+ );
+ statusEl.textContent = `Error: Failed to get image pixel data.`;
+ phase = 'error';
+ stats.wfcRun.status = 'Error: getImageData failed';
+ renderStats();
+ return;
+ }
+
+ stats.wfcRun.status = 'Extracting patterns (Worker)...';
+ statusEl.textContent = `Extracting ${currentPatternSize}x${currentPatternSize} patterns (Worker)...`;
+ renderStats();
+
+ setupWorker = new Worker('wfc_setup_worker.js');
+ setupWorker.onmessage = handleSetupWorkerMessage;
+ setupWorker.onerror = (err) => {
+ console.error('Setup Worker Error:', err);
+ statusEl.textContent = `Setup Worker Error: ${err.message}`;
+ phase = 'error';
+ stats.wfcRun.status = 'Error: Setup Worker Failed';
+ renderStats();
+ };
+
+ if (
+ !srcData ||
+ !srcData.buffer ||
+ !(srcData.buffer instanceof ArrayBuffer)
+ ) {
+ console.error(
+ '[Main Thread] setTimeout: srcData.buffer is invalid for transfer!',
+ srcData
+ );
+ statusEl.textContent = `Error: Image data buffer is invalid.`;
+ phase = 'error';
+ stats.wfcRun.status = 'Error: Invalid buffer';
+ renderStats();
+ return;
+ }
+
+ setupWorker.postMessage(
+ {
+ type: 'START_SETUP',
+ imageDataBuffer: srcData.buffer,
+ width: sampleWidth,
+ height: sampleHeight,
+ patternSize: currentPatternSize,
+ enableRotations: enableRotations,
+ },
+ [srcData.buffer]
+ );
+ }, 100); // Timeout value, can be adjusted
+ };
+
+ img.onerror = () => {
+ statusEl.textContent =
+ 'Error: Failed to load source image. Check URL and CORS policy.';
+ phase = 'loading_image_error';
+ stats.wfcRun.status = 'Error: Image load failed';
+ stats.sourceImage.loaded = false;
+ renderStats();
+ };
+
+ function resetWfcStateAndLoadNewImage() {
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ animationFrameId = null;
+ }
+ if (setupWorker) setupWorker.terminate();
+ if (solverWorker) solverWorker.terminate();
+
+ phase = 'loading';
+ resetStats();
+ stats.wfcRun.status = 'Loading image...';
+ if (outputCanvas.width > 0 && outputCanvas.height > 0) {
+ outputCtx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);
+ }
+ dirtyTilesForFrame.clear();
+
+ currentImageUrl = imageUrlInput.value.trim();
+ currentPatternSize = parseInt(patternSizeSelect.value, 10);
+ enableRotations = enableRotationsCheckbox.checked;
+
+ patternsData = {
+ patterns: null,
+ patternByID: null,
+ patternIdToIndex: null,
+ adjacency: null,
+ patternByIndexForDrawing: null,
+ };
+ currentGridCellOptions = {};
+
+ if (!currentImageUrl) {
+ statusEl.textContent = 'Error: Image URL cannot be empty.';
+ phase = 'error';
+ stats.wfcRun.status = 'Error: No image URL';
+ renderStats();
+ return;
+ }
+ // If currentImageUrl is 'input_file_0.png', ensure it's served correctly by your local server.
+ // For testing, you might hardcode the GitHub URL if local serving of input_file_0.png is an issue.
+ // currentImageUrl = 'https://raw.githubusercontent.com/mxgmn/WaveFunctionCollapse/master/samples/Flowers.png'; // Fallback for testing
+ // imageUrlInput.value = currentImageUrl; // Update input field if hardcoded
+
+ img.src = '';
+ img.src = currentImageUrl;
+ statusEl.textContent = 'Loading image...';
+ renderStats();
+ }
+ restartButton.addEventListener('click', resetWfcStateAndLoadNewImage);
+
+ function handleSetupWorkerMessage(e) {
+ const { type, payload, error } = e.data;
+ if (error || (payload && payload.error)) {
+ const errorMessage = error || payload.error;
+ console.error('Error from Setup Worker:', errorMessage);
+ statusEl.textContent = `Error from Setup Worker: ${errorMessage}`;
+ phase = 'error';
+ stats.wfcRun.status = `Error: Setup failed (${errorMessage})`;
+ renderStats();
+ return;
+ }
+
+ if (type === 'SETUP_PROGRESS') {
+ statusEl.textContent = payload.statusMessage;
+ if (payload.statsUpdate) {
+ Object.assign(
+ stats.patternExtraction,
+ payload.statsUpdate.patternExtraction || {}
+ );
+ Object.assign(stats.memory, payload.statsUpdate.memory || {});
+ }
+ renderStats();
+ } else if (type === 'SETUP_COMPLETE') {
+ const receivedPatternsData = payload.patternsData;
+
+ patternsData.patterns = receivedPatternsData.patterns;
+ patternsData.patternIdToIndex = receivedPatternsData.patternIdToIndex;
+ patternsData.adjacency = receivedPatternsData.adjacency;
+ patternsData.patternByID = receivedPatternsData.patternByID;
+
+ Object.assign(stats.patternExtraction, payload.stats.patternExtraction);
+ Object.assign(stats.adjacency, payload.stats.adjacency);
+ Object.assign(stats.memory, payload.stats.memory);
+
+ if (!patternsData.patterns || patternsData.patterns.length === 0) {
+ statusEl.textContent = `Error: No patterns found by worker.`;
+ phase = 'error';
+ stats.wfcRun.status = 'Error: No patterns from worker';
+ renderStats();
+ return;
+ }
+
+ const sourceArrayForDrawing =
+ receivedPatternsData.patternByIndex || patternsData.patterns;
+ patternsData.patternByIndexForDrawing = sourceArrayForDrawing.map(
+ (p_orig, p_idx) => {
+ if (!(p_orig.cells instanceof Uint8Array)) {
+ console.error(
+ `CRITICAL in handleSetupWorkerMessage (map for drawing): Pattern at index ${p_idx}, p_orig.cells is NOT a Uint8Array. It is:`,
+ p_orig.cells
+ );
+ if (p_orig.cells instanceof ArrayBuffer) {
+ return {
+ id: p_orig.id,
+ cells: new Uint8Array(p_orig.cells.slice(0)),
+ frequency: p_orig.frequency,
+ index: p_orig.index,
+ };
+ }
+ throw new Error(
+ 'p_orig.cells is not a Uint8Array or ArrayBuffer in handleSetupWorkerMessage map'
+ );
+ }
+ return {
+ id: p_orig.id,
+ cells: new Uint8Array(p_orig.cells.buffer.slice(0)),
+ frequency: p_orig.frequency,
+ index: p_orig.index,
+ };
+ }
+ );
+
+ if (
+ patternsData.patternByIndexForDrawing &&
+ patternsData.patternByIndexForDrawing.length > 0
+ ) {
+ const firstPatternForDrawing = patternsData.patternByIndexForDrawing[0];
+ console.log(
+ `[Main Thread] After copy for drawing: Pattern 0 (${
+ firstPatternForDrawing.id
+ }) - .cells is Uint8Array: ${
+ firstPatternForDrawing.cells instanceof Uint8Array
+ }`
+ );
+ console.log(
+ `[Main Thread] After copy for drawing: Pattern 0 .cells.length: ${firstPatternForDrawing.cells.length}`
+ );
+ let pixelsToLog = [];
+ for (
+ let i = 0;
+ i <
+ Math.min(
+ firstPatternForDrawing.cells.length,
+ currentPatternSize * currentPatternSize * 4
+ );
+ i += 4
+ ) {
+ pixelsToLog.push(
+ `(${firstPatternForDrawing.cells[i]},${
+ firstPatternForDrawing.cells[i + 1]
+ },${firstPatternForDrawing.cells[i + 2]},${
+ firstPatternForDrawing.cells[i + 3]
+ })`
+ );
+ }
+ console.log(
+ `[Main Thread] After copy for drawing: Pattern 0 pixels:`,
+ pixelsToLog.join(' ')
+ );
+ }
+
+ statusEl.textContent = 'Setup complete. Initializing WFC grid...';
+ stats.wfcRun.status = `Initializing grid...`;
+ renderStats();
+ initializeGridAndStartSolver();
+ }
+ }
+
+ function initializeGridAndStartSolver() {
+ const t2 = performance.now();
+ const pCols = OUTPUT_SIZE;
+ const pRows = OUTPUT_SIZE;
+
+ outputCanvas.width = pCols * currentPatternSize;
+ outputCanvas.height = pRows * currentPatternSize;
+ outputCtx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);
+
+ stats.grid.pCols = pCols;
+ stats.grid.pRows = pRows;
+ stats.grid.initialCellOptions = patternsData.patterns
+ ? patternsData.patterns.length
+ : 0;
+ stats.grid.timeMs = performance.now() - t2;
+
+ if (patternsData.patternByIndexForDrawing) {
+ 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,
+ patternsData.patternByIndexForDrawing.map((_, i) => i)
+ );
+ }
+ }
+ }
+ processAndDrawDirtyTiles();
+
+ statusEl.textContent = 'Starting WFC solver worker...';
+ stats.wfcRun.status = 'Running WFC (Solver Worker)...';
+ stats.wfcRun.startTime = performance.now();
+ renderStats();
+
+ solverWorker = new Worker('wfc_solver_worker.js');
+ solverWorker.onmessage = handleSolverWorkerMessage;
+ solverWorker.onerror = (err) => {
+ console.error('Solver Worker Error:', err);
+ statusEl.textContent = `Solver Worker Error: ${err.message}`;
+ phase = 'error';
+ stats.wfcRun.status = 'Error: Solver Worker Failed';
+ if (stats.wfcRun.startTime > 0 && stats.wfcRun.endTime === 0) {
+ stats.wfcRun.endTime = performance.now();
+ stats.wfcRun.durationMs = stats.wfcRun.endTime - stats.wfcRun.startTime;
+ }
+ renderStats();
+ };
+
+ const patternCellBuffersToTransfer = patternsData.patterns.map(
+ (p, index) => {
+ if (!p || !p.cells) {
+ console.error(
+ `Pattern at index ${index} or its .cells is null/undefined for transfer.`,
+ p
+ );
+ throw new Error(
+ `Pattern at index ${index} or its .cells is null/undefined. Cannot get buffer.`
+ );
+ }
+ if (!(p.cells instanceof Uint8Array)) {
+ console.error(
+ `Pattern at index ${index}: p.cells is NOT a Uint8Array for transfer. It is:`,
+ p.cells
+ );
+ if (p.cells instanceof ArrayBuffer) {
+ console.warn(
+ `Pattern at index ${index}: p.cells is an ArrayBuffer. Using p.cells directly for transfer list.`
+ );
+ return p.cells;
+ }
+ throw new Error(
+ `Pattern at index ${index}: p.cells is not a Uint8Array. Cannot safely get .buffer.`
+ );
+ }
+ if (!(p.cells.buffer instanceof ArrayBuffer)) {
+ console.error(
+ `Pattern at index ${index}: p.cells is Uint8Array, but p.cells.buffer is NOT an ArrayBuffer. p.cells.buffer is:`,
+ p.cells.buffer
+ );
+ throw new Error(
+ `Pattern at index ${index}: p.cells.buffer is not an ArrayBuffer.`
+ );
+ }
+ return p.cells.buffer;
+ }
+ );
+
+ try {
+ solverWorker.postMessage(
+ {
+ type: 'START_SOLVE',
+ pCols: pCols,
+ pRows: pRows,
+ patterns: patternsData.patterns,
+ patternIdToIndex: patternsData.patternIdToIndex,
+ adjacency: patternsData.adjacency,
+ currentPatternSize: currentPatternSize,
+ outputPixelWidth: outputCanvas.width,
+ outputPixelHeight: outputCanvas.height,
+ maxBacktrackDepth: MAX_BACKTRACK_DEPTH,
+ useRandomSelection: randomSelectionCheckbox.checked,
+ },
+ patternCellBuffersToTransfer
+ );
+ } catch (cloneError) {
+ console.error('DataCloneError during postMessage to solver:', cloneError);
+ phase = 'error';
+ stats.wfcRun.status = 'Error: DataCloneError posting to solver';
+ renderStats();
+ return;
+ }
+ phase = 'collapse_solve';
+ }
+
+ let lastStatsRenderTime = 0;
+ const STATS_RENDER_INTERVAL = 250;
+
+ function handleSolverWorkerMessage(e) {
+ const { type, payload, error } = e.data;
+
+ if (error) {
+ console.error('Error from Solver Worker:', error);
+ statusEl.textContent = `Error from Solver Worker: ${error}`;
+ phase = 'error';
+ stats.wfcRun.status = `Error: Solver failed (${error})`;
+ if (stats.wfcRun.startTime > 0 && stats.wfcRun.endTime === 0) {
+ stats.wfcRun.endTime = performance.now();
+ stats.wfcRun.durationMs = stats.wfcRun.endTime - stats.wfcRun.startTime;
+ }
+ renderStats();
+ if (solverWorker) solverWorker.terminate();
+ return;
+ }
+
+ switch (type) {
+ case 'TILE_UPDATE':
+ payload.updates.forEach((update) => {
+ addDirtyTile(update.c, update.r, update.cellOptions);
+ });
+ break;
+ case 'STATUS_UPDATE':
+ statusEl.textContent = payload.message;
+ stats.wfcRun.status = payload.wfcStatus || stats.wfcRun.status;
+ break;
+ case 'STATS_UPDATE':
+ Object.assign(stats.wfcRun, payload.wfcRun || {});
+ Object.assign(stats.memory, payload.memory || {});
+ break;
+ case 'WFC_COMPLETE':
+ phase = 'done';
+ statusEl.textContent = 'WFC complete!';
+ stats.wfcRun.status = 'WFC Complete!';
+ if (payload.finalStats)
+ Object.assign(stats.wfcRun, payload.finalStats.wfcRun || {});
+ if (stats.wfcRun.startTime > 0 && stats.wfcRun.endTime === 0) {
+ stats.wfcRun.endTime = performance.now();
+ stats.wfcRun.durationMs =
+ stats.wfcRun.endTime - stats.wfcRun.startTime;
+ }
+ processAndDrawDirtyTiles();
+ if (solverWorker) solverWorker.terminate();
+ break;
+ case 'WFC_FAILED':
+ phase = 'error';
+ statusEl.textContent = payload.message || 'WFC failed.';
+ stats.wfcRun.status = payload.wfcStatus || 'WFC Failed';
+ if (payload.finalStats)
+ Object.assign(stats.wfcRun, payload.finalStats.wfcRun || {});
+ if (stats.wfcRun.startTime > 0 && stats.wfcRun.endTime === 0) {
+ stats.wfcRun.endTime = performance.now();
+ stats.wfcRun.durationMs =
+ stats.wfcRun.endTime - stats.wfcRun.startTime;
+ }
+ processAndDrawDirtyTiles();
+ if (solverWorker) solverWorker.terminate();
+ break;
+ case 'REQUEST_RESTART_FROM_SOLVER':
+ statusEl.textContent =
+ payload.message || 'Solver requested restart. Re-initializing...';
+ stats.wfcRun.status = payload.wfcStatus || 'Solver Restarting...';
+ if (payload.currentRunStats)
+ Object.assign(stats.wfcRun, payload.currentRunStats.wfcRun || {});
+ if (stats.wfcRun.startTime > 0) {
+ stats.wfcRun.endTime = performance.now();
+ stats.wfcRun.durationMs =
+ stats.wfcRun.endTime - stats.wfcRun.startTime;
+ }
+ renderStats();
+ stats.wfcRun.startTime = 0;
+ stats.wfcRun.endTime = 0;
+ stats.wfcRun.durationMs = 0;
+ stats.wfcRun.maxBacktrackStackDepthReached = 0;
+ stats.wfcRun.cellsCollapsed = 0;
+ stats.wfcRun.propagationSteps = 0;
+ stats.memory.backtrackStackPeakBytes = 0;
+ if (solverWorker) solverWorker.terminate();
+ initializeGridAndStartSolver();
+ return;
+ }
+
+ processAndDrawDirtyTiles();
+
+ const now = performance.now();
+ if (
+ phase !== 'done' &&
+ phase !== 'error' &&
+ phase !== 'loading_image_error' &&
+ stats.wfcRun.startTime > 0
+ ) {
+ stats.wfcRun.durationMs = now - stats.wfcRun.startTime;
+ }
+ if (
+ now - lastStatsRenderTime > STATS_RENDER_INTERVAL ||
+ phase === 'done' ||
+ phase === 'error'
+ ) {
+ renderStats();
+ lastStatsRenderTime = now;
+ }
+ }
+
+ let currentGridCellOptions = {};
+
+ function addDirtyTile(c, r, cellOptions) {
+ currentGridCellOptions[`${c},${r}`] = cellOptions;
+ dirtyTilesForFrame.add(`${c},${r}`);
+ if (
+ !animationFrameId &&
+ (phase === 'collapse_solve' ||
+ phase === 'done' ||
+ phase === 'processing_setup')
+ ) {
+ animationFrameId = requestAnimationFrame(() => {
+ processAndDrawDirtyTiles();
+ animationFrameId = null;
+ });
+ }
+ }
+
+ function processAndDrawDirtyTiles() {
+ if (dirtyTilesForFrame.size === 0) return;
+ dirtyTilesForFrame.forEach((key) => {
+ const [c, r] = key.split(',').map(Number);
+ const cellOptions = currentGridCellOptions[key];
+ if (cellOptions && patternsData.patternByIndexForDrawing) {
+ drawTile(
+ c,
+ r,
+ cellOptions,
+ patternsData.patternByIndexForDrawing,
+ currentPatternSize
+ );
+ }
+ });
+ dirtyTilesForFrame.clear();
+ }
+
+ function drawTile(c, r, cellOptions, patternArrayForDrawing, pSize) {
+ if (!cellOptions || !patternArrayForDrawing) return;
+ const numOptions = cellOptions.length;
+ const pixelX = c * pSize;
+ const pixelY = r * pSize;
+
+ if (numOptions === 1) {
+ drawCollapsedPatternForCell(
+ pixelX,
+ pixelY,
+ cellOptions[0],
+ patternArrayForDrawing,
+ pSize
+ );
+ } else if (numOptions > 1) {
+ drawAveragePatternForCell(
+ pixelX,
+ pixelY,
+ cellOptions,
+ patternArrayForDrawing,
+ pSize
+ );
+ } else {
+ outputCtx.fillStyle = 'rgba(255, 0, 0, 0.7)';
+ outputCtx.fillRect(pixelX, pixelY, pSize, pSize);
+ }
+ }
+
+ function drawCollapsedPatternForCell(
+ pixelX,
+ pixelY,
+ patIndex,
+ patternArrayForDrawing,
+ pSize
+ ) {
+ const pat = patternArrayForDrawing[patIndex];
+ if (!pat || !pat.cells) {
+ outputCtx.fillStyle = 'rgba(255, 0, 255, 0.7)';
+ outputCtx.fillRect(pixelX, pixelY, pSize, pSize);
+ return;
+ }
+ try {
+ const imageData = outputCtx.createImageData(pSize, pSize);
+ imageData.data.set(pat.cells);
+ outputCtx.putImageData(imageData, pixelX, pixelY);
+ } catch (e) {
+ console.error(
+ 'Error drawing collapsed cell:',
+ e,
+ 'Pattern Index:',
+ patIndex,
+ 'Coords:',
+ pixelX,
+ pixelY,
+ 'Pattern data:',
+ pat
+ );
+ outputCtx.fillStyle = 'rgba(255, 165, 0, 0.7)';
+ outputCtx.fillRect(pixelX, pixelY, pSize, pSize);
+ }
+ }
+
+ function drawAveragePatternForCell(
+ pixelX,
+ pixelY,
+ possiblePatternIndices,
+ patternArrayForDrawing,
+ pSize
+ ) {
+ if (
+ !possiblePatternIndices ||
+ possiblePatternIndices.length === 0 ||
+ !patternArrayForDrawing
+ )
+ return;
+ 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 = patternArrayForDrawing[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, pixelX, pixelY);
+ }
+
+ resetWfcStateAndLoadNewImage();
+})();
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..792c262
--- /dev/null
+++ b/style.css
@@ -0,0 +1,65 @@
+body {
+ font-family: sans-serif;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 20px;
+}
+#controls {
+ margin-bottom: 20px;
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+#controls input[type='text'],
+#controls input[type='checkbox'] {
+ padding: 8px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
+#controls label {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+#controls select,
+#controls button {
+ padding: 8px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ background-color: #f8f8f8;
+}
+#canvases {
+ display: flex;
+ gap: 20px;
+ align-items: flex-start;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+#canvases > div {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+#sourceCanvas {
+ border: 1px solid #ccc;
+ margin-top: 5px;
+ image-rendering: pixelated; /* Maintain pixelated look when scaled by CSS */
+}
+#outputCanvas {
+ border: 1px solid #ccc;
+ margin-top: 5px;
+ width: 512px; /* Display size - 4x the drawing width */
+ height: 512px; /* Display size - 4x the drawing height */
+ image-rendering: pixelated; /* Keeps pixels sharp when scaled */
+}
+#statsContainer {
+ width: 512px;
+}
+#status {
+ margin-top: 15px;
+ font-weight: bold;
+ min-height: 1.2em; /* Prevent layout shift */
+}
diff --git a/wfc_setup_worker.js b/wfc_setup_worker.js
new file mode 100644
index 0000000..4c4c197
--- /dev/null
+++ b/wfc_setup_worker.js
@@ -0,0 +1,398 @@
+// wfc_setup_worker.js
+let sampleWidth,
+ sampleHeight,
+ srcDataView,
+ currentPatternSizeW,
+ enableRotationsW;
+let patterns, patternByID, patternByIndex, patternIdToIndex;
+let adjacency;
+
+// --- Helper functions (subset of original, or new for worker) ---
+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 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;
+ } // Should not happen with defined angles
+ 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;
+}
+
+// --- Stats helpers (subset for this worker) ---
+function estimateStringBytes(str) {
+ return str ? str.length * 2 : 0;
+}
+function estimateObjectOverhead(obj) {
+ if (!obj) return 0;
+ return (
+ Object.keys(obj).length *
+ (estimateStringBytes('key_placeholder_avg') + 8 + 4) +
+ 16
+ );
+}
+
+let workerStats = {
+ patternExtraction: {},
+ adjacency: {},
+ memory: {},
+};
+
+function extractPatterns() {
+ console.log(
+ '[Setup Worker] extractPatterns started. sampleWidth:',
+ sampleWidth,
+ 'sampleHeight:',
+ sampleHeight,
+ 'pSize:',
+ currentPatternSizeW
+ );
+ // Log a small portion of srcDataView to check its general content
+ if (srcDataView && srcDataView.length > 0) {
+ console.log(
+ '[Setup Worker] srcDataView first 100 bytes:',
+ srcDataView.slice(0, 100)
+ );
+ } else {
+ console.error(
+ '[Setup Worker] srcDataView is null, empty, or undefined at start of extractPatterns!'
+ );
+ // If srcDataView is bad, we can't proceed meaningfully.
+ throw new Error('srcDataView is invalid in extractPatterns');
+ }
+
+ const t0 = performance.now();
+ const countMap = new Map();
+ const keyToFlatCells = new Map();
+ const pSize = currentPatternSizeW;
+ 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;
+
+ if (srcIdx + 3 >= srcDataView.length) {
+ // Boundary check
+ console.error(
+ `[Setup Worker] Out of bounds access attempt: srcIdx=${srcIdx}, y=${y},x=${x}, dy=${dy},dx=${dx}, imgPixelY=${imgPixelY},imgPixelX=${imgPixelX}, sampleWidth=${sampleWidth}, sampleHeight=${sampleHeight}, srcDataView.length=${srcDataView.length}`
+ );
+ // Fill with a debug color or handle error
+ originalFlatBlock[k++] = 255;
+ originalFlatBlock[k++] = 0;
+ originalFlatBlock[k++] = 0;
+ originalFlatBlock[k++] = 255; // Red
+ continue; // Skip this pixel
+ }
+
+ originalFlatBlock[k++] = srcDataView[srcIdx];
+ originalFlatBlock[k++] = srcDataView[srcIdx + 1];
+ originalFlatBlock[k++] = srcDataView[srcIdx + 2];
+ originalFlatBlock[k++] = srcDataView[srcIdx + 3];
+ }
+ }
+
+ if (x === 0 && y === 0) {
+ // Log only the very first extracted block
+ console.log(
+ '[Setup Worker] First originalFlatBlock (x=0, y=0, pSize=' +
+ pSize +
+ '):'
+ );
+ let pixelsToLog = [];
+ for (let i = 0; i < originalFlatBlock.length; i += 4) {
+ pixelsToLog.push(
+ `(${originalFlatBlock[i]},${originalFlatBlock[i + 1]},${
+ originalFlatBlock[i + 2]
+ },${originalFlatBlock[i + 3]})`
+ );
+ }
+ console.log(pixelsToLog.join(' '));
+ }
+
+ const rotationsToConsider = enableRotationsW ? [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);
+ }
+ }
+ }
+ }
+ workerStats.patternExtraction.rawPatternCount =
+ rawPatternCounter * (enableRotationsW ? 4 : 1);
+
+ patterns = [];
+ patternByID = {};
+ patternByIndex = []; // This will be an array of pattern objects
+ patternIdToIndex = {};
+ let idx = 0;
+ for (const [key, freq] of countMap.entries()) {
+ const patID = 'p' + idx;
+ const flatCellsData = keyToFlatCells.get(key); // This is a Uint8Array
+ const patObj = {
+ id: patID,
+ cells: flatCellsData,
+ frequency: freq,
+ index: idx,
+ };
+ patterns.push(patObj);
+ patternByID[patID] = patObj;
+ patternByIndex[idx] = patObj; // Store by index
+ patternIdToIndex[patID] = idx;
+ idx++;
+ }
+ workerStats.patternExtraction.timeMs = performance.now() - t0;
+ workerStats.patternExtraction.uniquePatternCount = patterns.length;
+ workerStats.patternExtraction.rotationsEnabled = enableRotationsW;
+
+ workerStats.memory.patternCellsBytes = 0;
+ workerStats.memory.patternsArrayBytes = 0;
+ patterns.forEach((p) => {
+ workerStats.memory.patternCellsBytes += p.cells.byteLength;
+ workerStats.memory.patternsArrayBytes +=
+ estimateStringBytes(p.id) + 4 + 4 + 8 + 16;
+ });
+ workerStats.memory.patternsArrayBytes += 16;
+ workerStats.memory.patternByIdBytes = estimateObjectOverhead(patternByID);
+ Object.keys(patternByID).forEach((key) => {
+ workerStats.memory.patternByIdBytes += estimateStringBytes(key) + 8;
+ });
+
+ self.postMessage({
+ type: 'SETUP_PROGRESS',
+ payload: {
+ statusMessage: `Found ${patterns.length} unique patterns. Building adjacency...`,
+ statsUpdate: {
+ patternExtraction: workerStats.patternExtraction,
+ memory: workerStats.memory,
+ },
+ },
+ });
+}
+
+function buildAdjacency() {
+ const t1 = performance.now();
+ adjacency = {};
+ if (!patterns) return;
+ const pSize = currentPatternSizeW;
+ let ruleCount = 0;
+
+ for (const pat of patterns) {
+ // patterns is an array of pattern objects
+ 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++) {
+ for (let r_row = 0; r_row < pSize - 1; 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++;
+ }
+ }
+ }
+ workerStats.adjacency.totalRules = ruleCount;
+ workerStats.adjacency.timeMs = performance.now() - t1;
+
+ let adjMem = estimateObjectOverhead(adjacency);
+ if (adjacency) {
+ for (const patId in adjacency) {
+ adjMem += estimateStringBytes(patId);
+ adjMem += estimateObjectOverhead(adjacency[patId]);
+ ['up', 'down', 'left', 'right'].forEach((dir) => {
+ adjMem += estimateStringBytes(dir);
+ adjMem +=
+ adjacency[patId][dir].length * (patterns.length > 65535 ? 4 : 2) + 16;
+ });
+ }
+ }
+ workerStats.memory.adjacencyBytes = adjMem;
+}
+
+self.onmessage = function (e) {
+ const { type, imageDataBuffer, width, height, patternSize, enableRotations } =
+ e.data;
+
+ if (type === 'START_SETUP') {
+ console.log(
+ '[Setup Worker] Received START_SETUP. Image data buffer length:',
+ imageDataBuffer ? imageDataBuffer.byteLength : 'N/A'
+ );
+ sampleWidth = width;
+ sampleHeight = height;
+ srcDataView = new Uint8ClampedArray(imageDataBuffer);
+ currentPatternSizeW = patternSize;
+ enableRotationsW = enableRotations;
+
+ try {
+ extractPatterns();
+ if (!patterns || patterns.length === 0) {
+ self.postMessage({
+ type: 'SETUP_COMPLETE',
+ error: 'No patterns extracted.',
+ }); // Send error within SETUP_COMPLETE for main thread to handle
+ return;
+ }
+ buildAdjacency();
+
+ // patterns[i].cells are Uint8Arrays. Their .buffer will be transferred.
+ const transferablePatternCellBuffers = patterns.map(
+ (p) => p.cells.buffer
+ );
+
+ self.postMessage(
+ {
+ type: 'SETUP_COMPLETE',
+ payload: {
+ patternsData: {
+ patterns: patterns, // Array of pattern objects, .cells are Uint8Array
+ patternByID: patternByID,
+ patternByIndex: patternByIndex, // Send this explicitly if main thread uses it by this name
+ patternIdToIndex: patternIdToIndex,
+ adjacency: adjacency,
+ },
+ stats: workerStats,
+ },
+ },
+ transferablePatternCellBuffers
+ );
+ } catch (err) {
+ console.error('[Setup Worker] Error during setup process:', err);
+ self.postMessage({
+ type: 'SETUP_COMPLETE',
+ error: err.message + (err.stack ? '\n' + err.stack : ''),
+ });
+ }
+ }
+};
+
+// Catch unhandled errors in the worker
+self.onerror = function (message, source, lineno, colno, error) {
+ console.error(
+ '[Setup Worker] Unhandled error:',
+ message,
+ source,
+ lineno,
+ colno,
+ error
+ );
+ // Try to inform the main thread, though if postMessage itself fails, this might not work.
+ try {
+ self.postMessage({
+ type: 'SETUP_COMPLETE',
+ error: `Unhandled error in setup worker: ${message}`,
+ });
+ } catch (e) {
+ console.error(
+ '[Setup Worker] Failed to postMessage about unhandled error.',
+ e
+ );
+ }
+ return true; // Prevents default error handling if possible
+};
diff --git a/wfc_solver_worker.js b/wfc_solver_worker.js
new file mode 100644
index 0000000..49e6d26
--- /dev/null
+++ b/wfc_solver_worker.js
@@ -0,0 +1,641 @@
+// wfc_solver_worker.js
+let grid,
+ pCols,
+ pRows,
+ patternsW,
+ patternIdToIndexW,
+ adjacencyW,
+ currentPatternSizeW_solver;
+let queue, queuedCoordinatesSet, backtrackStack;
+let currentAttemptFailed;
+let maxTriesInWorker; // Renamed from maxTries to avoid confusion
+let useRandomSelectionW;
+let MAX_BACKTRACK_DEPTH_W;
+
+// --- Worker Stats (subset for WFC run) ---
+let solverRunStats = {};
+function resetSolverRunStats() {
+ solverRunStats = {
+ maxBacktrackStackDepthReached: 0,
+ totalBacktracks: 0,
+ totalContradictions: 0,
+ forcedBacktracksByDepth: 0,
+ cellsCollapsed: 0,
+ propagationSteps: 0,
+ // startTime/endTime/durationMs will be managed by main thread based on messages
+ };
+}
+let solverMemoryStats = {
+ estimatedGridSnapshotBytes: 0, // Will be calculated once grid is initialized
+ backtrackStackPeakBytes: 0,
+};
+
+// --- Helper functions for solver (deepCopy, getPatternNeighbors, etc.) ---
+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));
+}
+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 initializeGridInWorker() {
+ const allPatternIndices = Array.from(
+ { length: patternsW.length },
+ (_, i) => i
+ );
+ grid = Array.from({ length: pRows }, () =>
+ Array.from({ length: pCols }, () => [...allPatternIndices])
+ );
+ queue = [];
+ queuedCoordinatesSet = new Set();
+ maxTriesInWorker = pCols * pRows * 30 * currentPatternSizeW_solver; // Original logic for max tries
+ currentAttemptFailed = false;
+ backtrackStack = [];
+ resetSolverRunStats(); // Reset stats for this run
+
+ // Estimate memory for one grid snapshot (for backtrack stack calculations)
+ 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];
+ const bytesPerIndex =
+ patternsW.length > 65535 ? 4 : patternsW.length > 255 ? 2 : 1;
+ const cellBytes = 16 + cell.length * bytesPerIndex;
+ oneGridBytes += grid.length * grid[0].length * cellBytes;
+ }
+ solverMemoryStats.estimatedGridSnapshotBytes = oneGridBytes;
+ solverMemoryStats.backtrackStackPeakBytes = 0; // Reset for this run
+
+ // Send initial grid state (all tiles are "dirty" with all options)
+ const initialUpdates = [];
+ for (let r = 0; r < pRows; r++) {
+ for (let c = 0; c < pCols; c++) {
+ initialUpdates.push({ c, r, cellOptions: [...grid[r][c]] });
+ }
+ }
+ if (initialUpdates.length > 0) {
+ self.postMessage({
+ type: 'TILE_UPDATE',
+ payload: { updates: initialUpdates },
+ });
+ }
+ self.postMessage({
+ type: 'STATS_UPDATE',
+ payload: { memory: solverMemoryStats },
+ });
+}
+
+function pickCellWithLowestEntropy() {
+ /* ... Same as original ... */
+ 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; // Add jitter
+ if (currentEntropy < bestEntropy) {
+ bestEntropy = currentEntropy;
+ bestCells = [[c, r]];
+ } else if (numOptions === Math.floor(bestEntropy)) {
+ // Check only integer part for equality
+ bestCells.push([c, r]);
+ }
+ }
+ }
+ }
+ return bestCells.length > 0
+ ? bestCells[Math.floor(Math.random() * bestCells.length)]
+ : null;
+}
+
+function collapseCell(c, r) {
+ /* ... Same as original, but use solverRunStats and post TILE_UPDATE ... */
+ const initialOptionsForCellIndices = grid[r][c];
+ if (initialOptionsForCellIndices.length === 0) {
+ currentAttemptFailed = true;
+ solverRunStats.totalContradictions++;
+ return;
+ }
+
+ if (backtrackStack.length >= MAX_BACKTRACK_DEPTH_W) {
+ // Post status about max depth reached
+ self.postMessage({
+ type: 'STATUS_UPDATE',
+ payload: {
+ message: `Max backtrack depth (${MAX_BACKTRACK_DEPTH_W}) reached. Forcing backtrack.`,
+ },
+ });
+ currentAttemptFailed = true;
+ solverRunStats.forcedBacktracksByDepth++;
+ solverRunStats.totalContradictions++;
+ return;
+ }
+
+ let optionsToSystematicallyTry = [...initialOptionsForCellIndices];
+ // Randomize if checkbox was checked
+ if (useRandomSelectionW && 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],
+ ];
+ }
+ } else if (!useRandomSelectionW && optionsToSystematicallyTry.length > 1) {
+ // Weighted selection
+ let totalWeight = 0;
+ const weights = optionsToSystematicallyTry.map((idx) => {
+ const pat = patternsW[idx]; // patternsW is array of pattern objects
+ const w = pat && pat.frequency > 0 ? pat.frequency : 1;
+ totalWeight += w;
+ return w;
+ });
+ if (totalWeight === 0) {
+ // Should not happen if frequencies are positive
+ // Fallback to random if all weights are zero
+ } else {
+ let rnd = Math.random() * totalWeight;
+ let chosenIndexInSystematicList;
+ 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; // Should not be needed
+
+ // Move chosen to front
+ const actualChosenItem = optionsToSystematicallyTry.splice(
+ chosenIndexInSystematicList,
+ 1
+ )[0];
+ optionsToSystematicallyTry.unshift(actualChosenItem);
+ }
+ }
+
+ let chosenPatternIndex = optionsToSystematicallyTry[0]; // Try the first one (either random or weighted first)
+
+ if (typeof chosenPatternIndex === 'undefined') {
+ currentAttemptFailed = true;
+ solverRunStats.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: 0,
+ });
+ solverRunStats.maxBacktrackStackDepthReached = Math.max(
+ solverRunStats.maxBacktrackStackDepthReached,
+ backtrackStack.length
+ );
+ solverMemoryStats.backtrackStackPeakBytes = Math.max(
+ solverMemoryStats.backtrackStackPeakBytes,
+ backtrackStack.length * solverMemoryStats.estimatedGridSnapshotBytes
+ );
+ self.postMessage({
+ type: 'STATS_UPDATE',
+ payload: {
+ wfcRun: {
+ maxBacktrackStackDepthReached:
+ solverRunStats.maxBacktrackStackDepthReached,
+ },
+ memory: {
+ backtrackStackPeakBytes: solverMemoryStats.backtrackStackPeakBytes,
+ },
+ },
+ });
+
+ grid[r][c] = [chosenPatternIndex];
+ solverRunStats.cellsCollapsed++;
+ self.postMessage({
+ type: 'TILE_UPDATE',
+ payload: { updates: [{ c, r, cellOptions: grid[r][c] }] },
+ });
+
+ const coordKey = `${c},${r}`;
+ if (!queuedCoordinatesSet.has(coordKey)) {
+ queue.push([c, r]);
+ queuedCoordinatesSet.add(coordKey);
+ }
+}
+
+function propagateStep() {
+ /* ... Same as original, but use solverRunStats and post TILE_UPDATE ... */
+ solverRunStats.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;
+ solverRunStats.totalContradictions++;
+ return { success: false };
+ }
+ const currentPatternIndicesInCell = grid[r][c];
+ if (currentPatternIndicesInCell.length === 0) {
+ currentAttemptFailed = true;
+ solverRunStats.totalContradictions++;
+ return { success: false };
+ }
+
+ const tileUpdatesForPropagation = [];
+
+ for (const [nc, nr, dirToNeighbor] of getPatternNeighbors(c, r)) {
+ if (!grid[nr] || !grid[nr][nc]) {
+ currentAttemptFailed = true;
+ solverRunStats.totalContradictions++;
+ return { success: false };
+ }
+ const originalNeighborOptionIndices = grid[nr][nc];
+ if (originalNeighborOptionIndices.length === 0) {
+ currentAttemptFailed = true;
+ solverRunStats.totalContradictions++;
+ return { success: false }; // Already a contradiction
+ }
+
+ let cumulativeAllowedNeighborPatternIndices = new Set();
+ currentPatternIndicesInCell.forEach((p_idx) => {
+ const p_id_str = patternsW[p_idx].id; // patternsW is array of pattern objects
+ if (!adjacencyW[p_id_str]) return; // Should not happen with valid adjacency
+
+ let compatibleIndices;
+ if (dirToNeighbor === 'right')
+ compatibleIndices = adjacencyW[p_id_str].right;
+ else if (dirToNeighbor === 'left')
+ compatibleIndices = adjacencyW[p_id_str].left;
+ else if (dirToNeighbor === 'up')
+ compatibleIndices = adjacencyW[p_id_str].up;
+ else if (dirToNeighbor === 'down')
+ compatibleIndices = adjacencyW[p_id_str].down;
+ else return;
+
+ compatibleIndices.forEach((q_idx) =>
+ cumulativeAllowedNeighborPatternIndices.add(q_idx)
+ );
+ });
+
+ const newNeighborOptionIndices = originalNeighborOptionIndices.filter(
+ (idx) => cumulativeAllowedNeighborPatternIndices.has(idx)
+ );
+
+ if (newNeighborOptionIndices.length === 0) {
+ currentAttemptFailed = true;
+ solverRunStats.totalContradictions++;
+ return { success: false };
+ }
+
+ if (
+ newNeighborOptionIndices.length < originalNeighborOptionIndices.length
+ ) {
+ grid[nr][nc] = newNeighborOptionIndices;
+ tileUpdatesForPropagation.push({
+ c: nc,
+ r: nr,
+ cellOptions: grid[nr][nc],
+ });
+
+ const neighborCoordKey = `${nc},${nr}`;
+ if (!queuedCoordinatesSet.has(neighborCoordKey)) {
+ queue.push([nc, nr]);
+ queuedCoordinatesSet.add(neighborCoordKey);
+ }
+ }
+ }
+ if (tileUpdatesForPropagation.length > 0) {
+ self.postMessage({
+ type: 'TILE_UPDATE',
+ payload: { updates: tileUpdatesForPropagation },
+ });
+ }
+ return { success: true };
+}
+
+function allCollapsed() {
+ /* ... Same as original ... */
+ 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 solverPhase = 'collapse'; // 'collapse' or 'propagate'
+let runLoopTimeoutId = null;
+
+function runStepInWorker() {
+ if (runLoopTimeoutId) clearTimeout(runLoopTimeoutId); // Clear previous timeout if any
+
+ if (solverPhase === 'stop') {
+ // A way to stop the loop if main thread terminates worker
+ return;
+ }
+
+ if (currentAttemptFailed) {
+ currentAttemptFailed = false;
+ let backtrackedSuccessfully = false;
+ solverRunStats.totalBacktracks++;
+ self.postMessage({
+ type: 'STATS_UPDATE',
+ payload: { wfcRun: { totalBacktracks: solverRunStats.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); // Restore state
+ queue = deepCopyQueue(queueBeforeChoice);
+ queuedCoordinatesSet = new Set(queuedCoordinatesSetBeforeChoice);
+
+ const newChosenPatternIndex = optionsAtCell[nextOptionIndexToTryInList];
+ grid[cr][cc] = [newChosenPatternIndex];
+ // Post update for the cell being retried
+ self.postMessage({
+ type: 'TILE_UPDATE',
+ payload: { updates: [{ c: cc, r: cr, cellOptions: grid[cr][cc] }] },
+ });
+
+ const coordKeyBacktrack = `${cc},${cr}`;
+ if (!queuedCoordinatesSet.has(coordKeyBacktrack)) {
+ queue.push([cc, cr]);
+ queuedCoordinatesSet.add(coordKeyBacktrack);
+ }
+
+ // Push new decision point for this choice
+ if (backtrackStack.length < MAX_BACKTRACK_DEPTH_W) {
+ // Check before pushing
+ backtrackStack.push({
+ ...lastDecision, // Keep most of the old data
+ lastTriedOptionIndexInList: nextOptionIndexToTryInList, // Update the tried index
+ });
+ solverRunStats.maxBacktrackStackDepthReached = Math.max(
+ solverRunStats.maxBacktrackStackDepthReached,
+ backtrackStack.length
+ );
+ solverMemoryStats.backtrackStackPeakBytes = Math.max(
+ solverMemoryStats.backtrackStackPeakBytes,
+ backtrackStack.length * solverMemoryStats.estimatedGridSnapshotBytes
+ );
+ self.postMessage({
+ type: 'STATS_UPDATE',
+ payload: {
+ wfcRun: {
+ maxBacktrackStackDepthReached:
+ solverRunStats.maxBacktrackStackDepthReached,
+ },
+ memory: {
+ backtrackStackPeakBytes:
+ solverMemoryStats.backtrackStackPeakBytes,
+ },
+ },
+ });
+ } else {
+ // Cannot push new state if max depth would be exceeded by this push.
+ // This implies this new choice, if it leads to immediate contradiction, cannot be further backtracked from this exact point.
+ // This state is tricky; the original code had a check *before* collapse.
+ // For now, we just don't push if it would exceed.
+ }
+
+ self.postMessage({
+ type: 'STATUS_UPDATE',
+ payload: {
+ message: `Backtracked. Retrying option ${
+ nextOptionIndexToTryInList + 1
+ }/${optionsAtCell.length} at (${cc},${cr}). Stack: ${
+ backtrackStack.length
+ }`,
+ },
+ });
+ solverPhase = 'propagate';
+ backtrackedSuccessfully = true;
+
+ // When backtracking, the entire grid might have changed, so signal main to redraw all from current worker grid state.
+ // This is simpler than tracking all individual changes during backtrack restoration.
+ const allTilesUpdate = [];
+ 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]) {
+ // Ensure cell exists
+ allTilesUpdate.push({
+ c: c_idx,
+ r: r_idx,
+ cellOptions: [...grid[r_idx][c_idx]],
+ });
+ }
+ }
+ }
+ if (allTilesUpdate.length > 0) {
+ self.postMessage({
+ type: 'TILE_UPDATE',
+ payload: { updates: allTilesUpdate },
+ });
+ }
+ break; // Exit backtrack loop
+ }
+ }
+
+ if (!backtrackedSuccessfully) {
+ self.postMessage({
+ type: 'REQUEST_RESTART_FROM_SOLVER',
+ payload: {
+ message:
+ 'Backtrack stack exhausted -> WFC failed in worker. Requesting restart.',
+ wfcStatus: 'Failed (Backtrack Exhausted in Worker)',
+ currentRunStats: { wfcRun: solverRunStats }, // Send current stats before restart
+ },
+ });
+ solverPhase = 'stop'; // Stop this worker's loop
+ return;
+ }
+ }
+
+ if (allCollapsed()) {
+ solverPhase = 'stop';
+ self.postMessage({
+ type: 'WFC_COMPLETE',
+ payload: { finalStats: { wfcRun: solverRunStats } },
+ });
+ return;
+ }
+
+ if (maxTriesInWorker-- <= 0 && solverPhase !== 'propagate') {
+ self.postMessage({
+ type: 'REQUEST_RESTART_FROM_SOLVER',
+ payload: {
+ message: 'Max tries reached in worker -> Requesting restart.',
+ wfcStatus: 'Failed (Timeout in Worker)',
+ currentRunStats: { wfcRun: solverRunStats },
+ },
+ });
+ solverPhase = 'stop';
+ return;
+ }
+
+ if (solverPhase === 'collapse') {
+ const cellToCollapse = pickCellWithLowestEntropy();
+ if (!cellToCollapse) {
+ if (!allCollapsed()) {
+ // Should be a contradiction if no cell to collapse but not all done
+ currentAttemptFailed = true; // Trigger backtrack
+ solverRunStats.totalContradictions++;
+ self.postMessage({
+ type: 'STATUS_UPDATE',
+ payload: {
+ message:
+ 'Error: No cell to collapse, but not all collapsed. Contradiction.',
+ },
+ });
+ } else {
+ // Should have been caught by allCollapsed() earlier
+ solverPhase = 'stop';
+ self.postMessage({
+ type: 'WFC_COMPLETE',
+ payload: { finalStats: { wfcRun: solverRunStats } },
+ });
+ return;
+ }
+ } else {
+ collapseCell(cellToCollapse[0], cellToCollapse[1]);
+ if (!currentAttemptFailed) {
+ solverPhase = 'propagate';
+ }
+ }
+ } else if (solverPhase === 'propagate') {
+ const MAX_PROP_PER_BATCH = Math.max(30, Math.floor((pCols * pRows) / 20)); // Batch propagations
+ let processedInBatch = 0;
+ let propResult = { success: true };
+
+ while (
+ processedInBatch++ < MAX_PROP_PER_BATCH &&
+ queue.length > 0 &&
+ !currentAttemptFailed
+ ) {
+ propResult = propagateStep();
+ if (!propResult.success) break; // currentAttemptFailed would be true
+ }
+
+ if (queue.length === 0 || currentAttemptFailed || !propResult.success) {
+ solverPhase = 'collapse'; // Go back to collapse if queue empty or contradiction
+ }
+ }
+
+ // Post periodic stat updates
+ self.postMessage({
+ type: 'STATS_UPDATE',
+ payload: { wfcRun: solverRunStats },
+ });
+
+ if (solverPhase !== 'stop') {
+ runLoopTimeoutId = setTimeout(runStepInWorker, 0); // Continue the loop non-blockingly
+ }
+}
+
+self.onmessage = function (e) {
+ const {
+ type,
+ pCols: pc,
+ pRows: pr,
+ patterns: p,
+ patternIdToIndex: pIdToIdx,
+ adjacency: adj,
+ currentPatternSize: cPS,
+ maxBacktrackDepth,
+ useRandomSelection,
+ } = e.data;
+
+ if (type === 'START_SOLVE') {
+ pCols = pc;
+ pRows = pr;
+ // patternsW are received with ArrayBuffers for cells. These are now owned by this worker.
+ // We need to reconstruct them into Uint8Arrays if they are not already.
+ // Assuming main thread sent them as objects with .cells being ArrayBuffer.
+ patternsW = p.map((pat) => ({ ...pat, cells: new Uint8Array(pat.cells) }));
+ patternIdToIndexW = pIdToIdx;
+ adjacencyW = adj;
+ currentPatternSizeW_solver = cPS;
+ MAX_BACKTRACK_DEPTH_W = maxBacktrackDepth;
+ useRandomSelectionW = useRandomSelection;
+
+ solverPhase = 'collapse'; // Initial phase
+
+ try {
+ initializeGridInWorker();
+ self.postMessage({
+ type: 'STATUS_UPDATE',
+ payload: {
+ message: `Solver worker started. Grid: ${pCols}x${pRows}. Patterns: ${patternsW.length}`,
+ },
+ });
+ runStepInWorker(); // Start the loop
+ } catch (err) {
+ console.error('Error in solver worker START_SOLVE:', err);
+ self.postMessage({
+ type: null,
+ error: err.message + (err.stack ? '\n' + err.stack : ''),
+ });
+ solverPhase = 'stop';
+ }
+ }
+};
+
+// Catch unhandled errors in the worker
+self.onerror = function (message, source, lineno, colno, error) {
+ console.error(
+ 'Unhandled error in Solver Worker:',
+ message,
+ source,
+ lineno,
+ colno,
+ error
+ );
+ self.postMessage({
+ type: null,
+ error: `Unhandled: ${message} ${error ? error.stack : ''}`,
+ });
+ solverPhase = 'stop'; // Stop processing on unhandled error
+ return true; // Prevents default error handling
+};