995 lines
33 KiB
JavaScript
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();
|
|
})();
|