fix dithering bug

This commit is contained in:
Morten Andersson 2025-06-10 08:51:34 +02:00
parent e3eeb432e1
commit 1797fe00b3
3 changed files with 154 additions and 133 deletions

View File

@ -599,9 +599,11 @@
const pCols = OUTPUT_SIZE; const pCols = OUTPUT_SIZE;
const pRows = OUTPUT_SIZE; const pRows = OUTPUT_SIZE;
outputCanvas.width = pCols * currentPatternSize; const canvasWidth = pCols + currentPatternSize - 1;
outputCanvas.height = pRows * currentPatternSize; const canvasHeight = pRows + currentPatternSize - 1;
outputCtx.clearRect(0, 0, outputCanvas.width, outputCanvas.height); outputCanvas.width = canvasWidth;
outputCanvas.height = canvasHeight;
outputCtx.clearRect(0, 0, canvasWidth, canvasHeight);
stats.grid.pCols = pCols; stats.grid.pCols = pCols;
stats.grid.pRows = pRows; stats.grid.pRows = pRows;
@ -743,19 +745,34 @@
Object.assign(stats.memory, payload.memory || {}); Object.assign(stats.memory, payload.memory || {});
break; break;
case 'WFC_COMPLETE': case 'WFC_COMPLETE':
phase = 'done'; phase = 'done';
statusEl.textContent = 'WFC complete!'; statusEl.textContent = 'WFC complete!';
stats.wfcRun.status = 'WFC Complete!'; stats.wfcRun.status = 'WFC Complete!';
if (payload.finalStats) if (payload.finalStats) {
Object.assign(stats.wfcRun, payload.finalStats.wfcRun || {}); Object.assign(stats.wfcRun, payload.finalStats.wfcRun || {});
if (stats.wfcRun.startTime > 0 && stats.wfcRun.endTime === 0) { }
if (stats.wfcRun.startTime > 0 && stats.wfcRun.endTime === 0) {
stats.wfcRun.endTime = performance.now(); stats.wfcRun.endTime = performance.now();
stats.wfcRun.durationMs = stats.wfcRun.durationMs = stats.wfcRun.endTime - stats.wfcRun.startTime;
stats.wfcRun.endTime - stats.wfcRun.startTime; }
}
processAndDrawDirtyTiles(); // Use the definitive final grid from the payload to force a full redraw.
if (solverWorker) solverWorker.terminate(); if (payload.finalGrid) {
break; 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': case 'WFC_FAILED':
phase = 'error'; phase = 'error';
statusEl.textContent = payload.message || 'WFC failed.'; statusEl.textContent = payload.message || 'WFC failed.';
@ -854,8 +871,8 @@
function drawTile(c, r, cellOptions, patternArrayForDrawing, pSize) { function drawTile(c, r, cellOptions, patternArrayForDrawing, pSize) {
if (!cellOptions || !patternArrayForDrawing) return; if (!cellOptions || !patternArrayForDrawing) return;
const numOptions = cellOptions.length; const numOptions = cellOptions.length;
const pixelX = c * pSize; const pixelX = c;
const pixelY = r * pSize; const pixelY = r;
if (numOptions === 1) { if (numOptions === 1) {
drawCollapsedPatternForCell( drawCollapsedPatternForCell(

View File

@ -215,6 +215,9 @@ function extractPatterns() {
}); });
} }
// --- CORRECTED `buildAdjacency` for the OVERLAPPING MODEL ---
// --- Paste this into wfc_setup_worker.js ---
function buildAdjacency() { function buildAdjacency() {
const t1 = performance.now(); const t1 = performance.now();
adjacency = {}; adjacency = {};
@ -223,7 +226,6 @@ function buildAdjacency() {
let ruleCount = 0; let ruleCount = 0;
for (const pat of patterns) { for (const pat of patterns) {
// patterns is an array of pattern objects
adjacency[pat.id] = { up: [], down: [], left: [], right: [] }; adjacency[pat.id] = { up: [], down: [], left: [], right: [] };
} }
@ -240,15 +242,18 @@ function buildAdjacency() {
ruleCount++; ruleCount++;
continue; continue;
} }
// Check if q can be to the RIGHT of p (OVERLAPPING MODEL)
// The right N-1 columns of p must match the left N-1 columns of q.
let canSitRight = true; let canSitRight = true;
if (pSize > 1) { if (pSize > 1) {
for (let r = 0; r < pSize; r++) { for (let r = 0; r < pSize; r++) { // For each row
for (let c = 0; c < pSize - 1; c++) { for (let c = 0; c < pSize - 1; c++) { // For each overlapping column
for (let ch = 0; ch < 4; ch++) { for (let ch = 0; ch < 4; ch++) {
if ( // Compare p's pixel at [r][c+1] with q's pixel at [r][c]
p.cells[(r * pSize + (c + 1)) * 4 + ch] !== const pPixel = p.cells[(r * pSize + (c + 1)) * 4 + ch];
q.cells[(r * pSize + c) * 4 + ch] const qPixel = q.cells[(r * pSize + c) * 4 + ch];
) { if (pPixel !== qPixel) {
canSitRight = false; canSitRight = false;
break; break;
} }
@ -258,7 +263,7 @@ function buildAdjacency() {
if (!canSitRight) break; if (!canSitRight) break;
} }
} else { } else {
canSitRight = true; canSitRight = true; // 1x1 patterns are always adjacent
} }
if (canSitRight) { if (canSitRight) {
@ -268,15 +273,17 @@ function buildAdjacency() {
ruleCount++; ruleCount++;
} }
// Check if q can be DOWN from p (OVERLAPPING MODEL)
// The bottom N-1 rows of p must match the top N-1 rows of q.
let canSitDown = true; let canSitDown = true;
if (pSize > 1) { if (pSize > 1) {
for (let c_col = 0; c_col < pSize; c_col++) { for (let r = 0; r < pSize - 1; r++) { // For each overlapping row
for (let r_row = 0; r_row < pSize - 1; r_row++) { for (let c = 0; c < pSize; c++) { // For each column in that row
for (let ch = 0; ch < 4; ch++) { for (let ch = 0; ch < 4; ch++) {
if ( // Compare p's pixel at [r+1][c] with q's pixel at [r][c]
p.cells[((r_row + 1) * pSize + c_col) * 4 + ch] !== const pPixel = p.cells[((r + 1) * pSize + c) * 4 + ch];
q.cells[(r_row * pSize + c_col) * 4 + ch] const qPixel = q.cells[(r * pSize + c) * 4 + ch];
) { if (pPixel !== qPixel) {
canSitDown = false; canSitDown = false;
break; break;
} }
@ -286,7 +293,7 @@ function buildAdjacency() {
if (!canSitDown) break; if (!canSitDown) break;
} }
} else { } else {
canSitDown = true; canSitDown = true; // 1x1 patterns are always adjacent
} }
if (canSitDown) { if (canSitDown) {
@ -395,4 +402,4 @@ self.onerror = function (message, source, lineno, colno, error) {
); );
} }
return true; // Prevents default error handling if possible return true; // Prevents default error handling if possible
}; };

View File

@ -372,112 +372,105 @@ function runStepInWorker() {
payload: { wfcRun: { totalBacktracks: solverRunStats.totalBacktracks } }, payload: { wfcRun: { totalBacktracks: solverRunStats.totalBacktracks } },
}); });
// THIS IS THE CORRECTED BACKTRACKING LOOP
while (backtrackStack.length > 0) { while (backtrackStack.length > 0) {
const lastDecision = backtrackStack.pop(); const lastDecision = backtrackStack.pop();
const { const {
gridBeforeChoice, gridBeforeChoice,
queueBeforeChoice, queueBeforeChoice,
queuedCoordinatesSetBeforeChoice, queuedCoordinatesSetBeforeChoice,
cellCoords, cellCoords,
optionsAtCell, optionsAtCell,
lastTriedOptionIndexInList, lastTriedOptionIndexInList,
} = lastDecision; } = lastDecision;
const [cc, cr] = cellCoords; const [cc, cr] = cellCoords;
let nextOptionIndexToTryInList = lastTriedOptionIndexInList + 1; let nextOptionIndexToTryInList = lastTriedOptionIndexInList + 1;
if (nextOptionIndexToTryInList < optionsAtCell.length) { if (nextOptionIndexToTryInList < optionsAtCell.length) {
grid = deepCopyGrid(gridBeforeChoice); // Restore state grid = deepCopyGrid(gridBeforeChoice); // Restore state
queue = deepCopyQueue(queueBeforeChoice); queue = deepCopyQueue(queueBeforeChoice);
queuedCoordinatesSet = new Set(queuedCoordinatesSetBeforeChoice); queuedCoordinatesSet = new Set(queuedCoordinatesSetBeforeChoice);
const newChosenPatternIndex = optionsAtCell[nextOptionIndexToTryInList]; const newChosenPatternIndex = optionsAtCell[nextOptionIndexToTryInList];
grid[cr][cc] = [newChosenPatternIndex]; grid[cr][cc] = [newChosenPatternIndex];
// Post update for the cell being retried // Post update for the cell being retried
self.postMessage({ self.postMessage({
type: 'TILE_UPDATE', type: 'TILE_UPDATE',
payload: { updates: [{ c: cc, r: cr, cellOptions: grid[cr][cc] }] }, payload: { updates: [{ c: cc, r: cr, cellOptions: grid[cr][cc] }] },
}); });
const coordKeyBacktrack = `${cc},${cr}`; const coordKeyBacktrack = `${cc},${cr}`;
if (!queuedCoordinatesSet.has(coordKeyBacktrack)) { if (!queuedCoordinatesSet.has(coordKeyBacktrack)) {
queue.push([cc, cr]); queue.push([cc, cr]);
queuedCoordinatesSet.add(coordKeyBacktrack); 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]],
});
} }
}
// THE FIX: Push the updated decision back onto the stack
if (backtrackStack.length < MAX_BACKTRACK_DEPTH_W) {
backtrackStack.push({
...lastDecision,
lastTriedOptionIndexInList: nextOptionIndexToTryInList,
});
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,
},
},
});
}
self.postMessage({
type: 'STATUS_UPDATE',
payload: {
message: `Backtracked. Retrying option ${
nextOptionIndexToTryInList + 1
}/${optionsAtCell.length} at (${cc},${cr}). Stack: ${
backtrackStack.length
}`,
},
});
solverPhase = 'propagate';
backtrackedSuccessfully = true;
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]) {
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 (allTilesUpdate.length > 0) {
self.postMessage({
type: 'TILE_UPDATE',
payload: { updates: allTilesUpdate },
});
}
break; // Exit backtrack loop
}
} }
if (!backtrackedSuccessfully) { if (!backtrackedSuccessfully) {
self.postMessage({ self.postMessage({
type: 'REQUEST_RESTART_FROM_SOLVER', type: 'REQUEST_RESTART_FROM_SOLVER',
@ -493,14 +486,18 @@ function runStepInWorker() {
} }
} }
if (allCollapsed()) { if (allCollapsed()) {
solverPhase = 'stop'; solverPhase = 'stop';
// Add the final grid to the payload. This is the definitive final state.
self.postMessage({ self.postMessage({
type: 'WFC_COMPLETE', type: 'WFC_COMPLETE',
payload: { finalStats: { wfcRun: solverRunStats } }, payload: {
finalStats: { wfcRun: solverRunStats },
finalGrid: grid
},
}); });
return; return;
} }
if (maxTriesInWorker-- <= 0 && solverPhase !== 'propagate') { if (maxTriesInWorker-- <= 0 && solverPhase !== 'propagate') {
self.postMessage({ self.postMessage({
@ -638,4 +635,4 @@ self.onerror = function (message, source, lineno, colno, error) {
}); });
solverPhase = 'stop'; // Stop processing on unhandled error solverPhase = 'stop'; // Stop processing on unhandled error
return true; // Prevents default error handling return true; // Prevents default error handling
}; };