// Main script (script.js) (function () { //more samples at https://github.com/mxgmn/WaveFunctionCollapse/tree/master/samples let currentImageUrl = 'https://raw.githubusercontent.com/mxgmn/WaveFunctionCollapse/master/samples/LessRooms.png'; //'https://raw.githubusercontent.com/mxgmn/WaveFunctionCollapse/master/samples/Fabric.png'; //fails! //'https://raw.githubusercontent.com/mxgmn/WaveFunctionCollapse/master/samples/Dungeon.png'; //'https://raw.githubusercontent.com/mxgmn/WaveFunctionCollapse/master/samples/ColoredCity.png'; //'https://raw.githubusercontent.com/mxgmn/WaveFunctionCollapse/master/samples/Disk.png'; //'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; const canvasWidth = pCols + currentPatternSize - 1; const canvasHeight = pRows + currentPatternSize - 1; outputCanvas.width = canvasWidth; outputCanvas.height = canvasHeight; outputCtx.clearRect(0, 0, canvasWidth, canvasHeight); 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; } // Use the definitive final grid from the payload to force a full redraw. if (payload.finalGrid) { payload.finalGrid.forEach((row, r) => { row.forEach((cellOptions, c) => { // We use addDirtyTile to update our internal state and queue the draw addDirtyTile(c, r, cellOptions); }); }); } // Immediately draw all the tiles from the final grid. 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; const pixelY = r; 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(); })();