test
This commit is contained in:
parent
76c4c83a2d
commit
5ebcaa3714
85
index.html
Normal file
85
index.html
Normal file
@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>WFC with Web Workers</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
canvas {
|
||||
border: 1px solid black;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
#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 */
|
||||
}
|
||||
#controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 250px;
|
||||
}
|
||||
#statsContainer {
|
||||
font-size: 0.9em;
|
||||
white-space: pre-wrap;
|
||||
background-color: #f0f0f0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
min-width: 300px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="controls">
|
||||
<label for="imageUrlInput">Image URL:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="imageUrlInput"
|
||||
value="https://raw.githubusercontent.com/mxgmn/WaveFunctionCollapse/master/samples/Flowers.png"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<label for="patternSizeSelect">Pattern Size (N):</label>
|
||||
<select id="patternSizeSelect">
|
||||
<option value="2">2</option>
|
||||
<option value="3" selected>3</option>
|
||||
<option value="4">4</option>
|
||||
</select>
|
||||
<label
|
||||
><input type="checkbox" id="randomSelectionCheckbox" /> Random Selection
|
||||
for Collapse</label
|
||||
>
|
||||
<label
|
||||
><input type="checkbox" id="enableRotationsCheckbox" /> Enable
|
||||
Rotations</label
|
||||
>
|
||||
<button id="restartButton">Load Image & Run WFC</button>
|
||||
<p id="status">Loading...</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Source Image (scaled)</h3>
|
||||
<canvas id="sourceCanvas"></canvas>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Output</h3>
|
||||
<canvas id="outputCanvas"></canvas>
|
||||
</div>
|
||||
<div id="statsContainer">WFC Statistics will appear here.</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1270
old_main.js
Normal file
1270
old_main.js
Normal file
File diff suppressed because it is too large
Load Diff
970
script.js
Normal file
970
script.js
Normal file
@ -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 = `<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;
|
||||
|
||||
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();
|
||||
})();
|
||||
65
style.css
Normal file
65
style.css
Normal file
@ -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 */
|
||||
}
|
||||
398
wfc_setup_worker.js
Normal file
398
wfc_setup_worker.js
Normal file
@ -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
|
||||
};
|
||||
641
wfc_solver_worker.js
Normal file
641
wfc_solver_worker.js
Normal file
@ -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
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user