test_repo/script.js
2025-06-10 13:41:51 +02:00

995 lines
33 KiB
JavaScript

// 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 = `<strong>WFC Statistics:</strong><br />`;
html += `Status: ${stats.wfcRun.status}<br />`;
if (stats.wfcRun.durationMs > 0) {
html += `Total Time: ${(stats.wfcRun.durationMs / 1000).toFixed(
2
)}s<br />`;
}
html += `<br />`;
if (stats.sourceImage.loaded) {
html += `<strong>Source Image:</strong><br />`;
html += ` Dimensions: ${stats.sourceImage.width}x${stats.sourceImage.height}<br />`;
html += ` Pattern Size: ${currentPatternSize}x${currentPatternSize}<br />`;
html += ` Rotations Enabled: ${stats.patternExtraction.rotationsEnabled}<br />`;
html += `<br />`;
html += `<strong>Pattern Extraction (${(
stats.patternExtraction.timeMs / 1000
).toFixed(2)}s):</strong><br />`;
html += ` Raw Overlapping Patterns: ${stats.patternExtraction.rawPatternCount}<br />`;
html += ` Unique Patterns: ${stats.patternExtraction.uniquePatternCount}<br />`;
html += ` Memory (Pattern Objects): ~${formatBytes(
stats.memory.patternsArrayBytes
)}<br />`;
html += ` Memory (Pattern Pixel Data): ~${formatBytes(
stats.memory.patternCellsBytes
)}<br />`;
html += ` Memory (Pattern ID Map): ~${formatBytes(
stats.memory.patternByIdBytes
)}<br />`;
html += `<br />`;
html += `<strong>Adjacency Rules (${(
stats.adjacency.timeMs / 1000
).toFixed(2)}s):</strong><br />`;
html += ` Total Adjacency Rules: ${stats.adjacency.totalRules}<br />`;
html += ` Memory (Adjacency Map): ~${formatBytes(
stats.memory.adjacencyBytes
)}<br />`;
html += `<br />`;
html += `<strong>Output Grid (${(stats.grid.timeMs / 1000).toFixed(
2
)}s initialization):</strong><br />`;
html += ` Output Grid Pixel Size: ${OUTPUT_SIZE * currentPatternSize}x${
OUTPUT_SIZE * currentPatternSize
} (pixels)<br />`;
html += ` Pattern Grid Dimensions: ${stats.grid.pCols}x${
stats.grid.pRows
} (${stats.grid.pCols * stats.grid.pRows} cells)<br />`;
html += ` Initial Options/Cell: ${stats.grid.initialCellOptions}<br />`;
html += ` Est. Memory per Grid State (Solver): ~${formatBytes(
stats.memory.estimatedGridSnapshotBytes
)}<br />`;
html += `<br />`;
}
html += `<strong>WFC Run Dynamics:</strong><br />`;
html += ` Max Backtrack Stack Depth: ${stats.wfcRun.maxBacktrackStackDepthReached} / ${MAX_BACKTRACK_DEPTH}<br />`;
html += ` Est. Peak Backtrack Stack Memory (Solver): ~${formatBytes(
stats.memory.backtrackStackPeakBytes
)}<br />`;
html += ` Total Backtracks: ${stats.wfcRun.totalBacktracks}<br />`;
html += ` Forced Backtracks (Max Depth): ${stats.wfcRun.forcedBacktracksByDepth}<br />`;
html += ` Total Contradictions: ${stats.wfcRun.totalContradictions}<br />`;
html += ` Cells Collapsed: ${stats.wfcRun.cellsCollapsed}<br />`;
html += ` Propagation Steps: ${stats.wfcRun.propagationSteps}<br />`;
statsContainer.innerHTML = html;
}
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();
})();