diff --git a/script.js b/script.js index 828caf6..a7c8947 100644 --- a/script.js +++ b/script.js @@ -599,9 +599,11 @@ const pCols = OUTPUT_SIZE; const pRows = OUTPUT_SIZE; - outputCanvas.width = pCols * currentPatternSize; - outputCanvas.height = pRows * currentPatternSize; - outputCtx.clearRect(0, 0, outputCanvas.width, outputCanvas.height); + 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; @@ -743,19 +745,34 @@ Object.assign(stats.memory, payload.memory || {}); break; case 'WFC_COMPLETE': - phase = 'done'; - statusEl.textContent = 'WFC complete!'; - stats.wfcRun.status = 'WFC Complete!'; - if (payload.finalStats) + 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) { + } + 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; + 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.'; @@ -854,8 +871,8 @@ function drawTile(c, r, cellOptions, patternArrayForDrawing, pSize) { if (!cellOptions || !patternArrayForDrawing) return; const numOptions = cellOptions.length; - const pixelX = c * pSize; - const pixelY = r * pSize; + const pixelX = c; + const pixelY = r; if (numOptions === 1) { drawCollapsedPatternForCell( diff --git a/wfc_setup_worker.js b/wfc_setup_worker.js index 4c4c197..83776b6 100644 --- a/wfc_setup_worker.js +++ b/wfc_setup_worker.js @@ -215,6 +215,9 @@ function extractPatterns() { }); } +// --- CORRECTED `buildAdjacency` for the OVERLAPPING MODEL --- +// --- Paste this into wfc_setup_worker.js --- + function buildAdjacency() { const t1 = performance.now(); adjacency = {}; @@ -223,7 +226,6 @@ function buildAdjacency() { let ruleCount = 0; for (const pat of patterns) { - // patterns is an array of pattern objects adjacency[pat.id] = { up: [], down: [], left: [], right: [] }; } @@ -240,15 +242,18 @@ function buildAdjacency() { ruleCount++; 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; if (pSize > 1) { - for (let r = 0; r < pSize; r++) { - for (let c = 0; c < pSize - 1; c++) { + for (let r = 0; r < pSize; r++) { // For each row + for (let c = 0; c < pSize - 1; c++) { // For each overlapping column for (let ch = 0; ch < 4; ch++) { - if ( - p.cells[(r * pSize + (c + 1)) * 4 + ch] !== - q.cells[(r * pSize + c) * 4 + ch] - ) { + // Compare p's pixel at [r][c+1] with q's pixel at [r][c] + const pPixel = p.cells[(r * pSize + (c + 1)) * 4 + ch]; + const qPixel = q.cells[(r * pSize + c) * 4 + ch]; + if (pPixel !== qPixel) { canSitRight = false; break; } @@ -258,7 +263,7 @@ function buildAdjacency() { if (!canSitRight) break; } } else { - canSitRight = true; + canSitRight = true; // 1x1 patterns are always adjacent } if (canSitRight) { @@ -268,15 +273,17 @@ function buildAdjacency() { 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; 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 r = 0; r < pSize - 1; r++) { // For each overlapping row + for (let c = 0; c < pSize; c++) { // For each column in that 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] - ) { + // Compare p's pixel at [r+1][c] with q's pixel at [r][c] + const pPixel = p.cells[((r + 1) * pSize + c) * 4 + ch]; + const qPixel = q.cells[(r * pSize + c) * 4 + ch]; + if (pPixel !== qPixel) { canSitDown = false; break; } @@ -286,7 +293,7 @@ function buildAdjacency() { if (!canSitDown) break; } } else { - canSitDown = true; + canSitDown = true; // 1x1 patterns are always adjacent } if (canSitDown) { @@ -395,4 +402,4 @@ self.onerror = function (message, source, lineno, colno, error) { ); } return true; // Prevents default error handling if possible -}; +}; \ No newline at end of file diff --git a/wfc_solver_worker.js b/wfc_solver_worker.js index 49e6d26..e204e76 100644 --- a/wfc_solver_worker.js +++ b/wfc_solver_worker.js @@ -372,112 +372,105 @@ function runStepInWorker() { payload: { wfcRun: { totalBacktracks: solverRunStats.totalBacktracks } }, }); + // THIS IS THE CORRECTED BACKTRACKING LOOP while (backtrackStack.length > 0) { - const lastDecision = backtrackStack.pop(); - const { - gridBeforeChoice, - queueBeforeChoice, - queuedCoordinatesSetBeforeChoice, - cellCoords, - optionsAtCell, - lastTriedOptionIndexInList, - } = lastDecision; + const lastDecision = backtrackStack.pop(); + const { + gridBeforeChoice, + queueBeforeChoice, + queuedCoordinatesSetBeforeChoice, + cellCoords, + optionsAtCell, + lastTriedOptionIndexInList, + } = lastDecision; - const [cc, cr] = cellCoords; - let nextOptionIndexToTryInList = lastTriedOptionIndexInList + 1; + const [cc, cr] = cellCoords; + let nextOptionIndexToTryInList = lastTriedOptionIndexInList + 1; - if (nextOptionIndexToTryInList < optionsAtCell.length) { - grid = deepCopyGrid(gridBeforeChoice); // Restore state - queue = deepCopyQueue(queueBeforeChoice); - queuedCoordinatesSet = new Set(queuedCoordinatesSetBeforeChoice); + 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 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]], - }); + const coordKeyBacktrack = `${cc},${cr}`; + if (!queuedCoordinatesSet.has(coordKeyBacktrack)) { + queue.push([cc, cr]); + queuedCoordinatesSet.add(coordKeyBacktrack); } - } + + // 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) { self.postMessage({ type: 'REQUEST_RESTART_FROM_SOLVER', @@ -493,14 +486,18 @@ function runStepInWorker() { } } - if (allCollapsed()) { +if (allCollapsed()) { solverPhase = 'stop'; + // Add the final grid to the payload. This is the definitive final state. self.postMessage({ type: 'WFC_COMPLETE', - payload: { finalStats: { wfcRun: solverRunStats } }, + payload: { + finalStats: { wfcRun: solverRunStats }, + finalGrid: grid + }, }); return; - } +} if (maxTriesInWorker-- <= 0 && solverPhase !== 'propagate') { self.postMessage({ @@ -638,4 +635,4 @@ self.onerror = function (message, source, lineno, colno, error) { }); solverPhase = 'stop'; // Stop processing on unhandled error return true; // Prevents default error handling -}; +}; \ No newline at end of file