From 0dbcbee96668c196ba37c71277a65803891bf612 Mon Sep 17 00:00:00 2001 From: kyuketski Date: Fri, 29 Aug 2025 22:08:25 +0300 Subject: [PATCH] feat: enhance color resolution, progress management, and pixel processing * Implemented `resolveColor` to improve color matching with exact matches, white threshold handling, and chroma penalty adjustments. * Enhanced `extractAvailableColors` to parse RGB values robustly and skip invalid color elements with warnings. * Refactored progress data management with `buildPaintedMapPacked`, `buildProgressData`, and `migrateProgress` for better organization and version handling. * Added migration logic to support progress data from versions 1.x, 2.0, and 2.1 to 2.2. * Improved error handling and logging during progress saving and loading operations. * Updated progress file structure to include additional state details like coordinate modes, block dimensions, and painted map packing. * Refactored progress migration to use a unified `Utils.migrateProgress`, reducing redundancy and improving maintainability. * Simplified color matching by moving inline functions to utilities for readability and reusability. * Enhanced `updateStats` with optional `isManualRefresh` for granular UI updates and alerts. * Added utility functions `getRemainingMsToThreshold`, `startChargesThresholdTicker`, and `updateChargesThresholdUI` to modularize charge-related logic. * Added coordinate generation logic supporting various modes, directions, and block configurations. * Fixed potential issues with color filtering and invalid cache management. * Ensured alerts and notifications are displayed correctly when colors or thresholds change. * Simplified coordinate generation logic, removing redundant code and improving readability. * Introduced `flushPixelBatch` for modular batch pixel processing. * Added detailed pixel eligibility checks in `processImage` to ensure valid pixels are processed. * Configurable transparency thresholds and strict color availability checks for flexibility. * Improved error handling for unsupported modes and directions. * Refactored pixel processing for readability and maintainability. * Consolidated batch flushing logic and simplified batch size calculations. * Enhanced logging with debug information for pixel eligibility, existing colors, and batch processing. * Added safeguards to clear intervals and prevent memory leaks during state cleanup. These changes improve color resolution accuracy, progress tracking, coordinate generation flexibility, and pixel processing robustness, while making the codebase more modular, maintainable, and easier to debug. --- Auto-Image.js | 1118 ++++++++++++++++++++++++------------------------- 1 file changed, 551 insertions(+), 567 deletions(-) diff --git a/Auto-Image.js b/Auto-Image.js index 83bd5b0..066a444 100644 --- a/Auto-Image.js +++ b/Auto-Image.js @@ -1775,6 +1775,111 @@ return r >= wt && g >= wt && b >= wt; }, + resolveColor(targetRgb, palette, exactMatch = false) { + if (!palette || palette.length === 0) return { id: null, rgb: targetRgb }; + + const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}|${ + state.colorMatchingAlgorithm + }|${state.enableChromaPenalty ? 'c' : 'nc'}|${state.chromaPenaltyWeight}|${exactMatch ? 'exact' : 'closest'}`; + + if (colorCache.has(cacheKey)) return colorCache.get(cacheKey); + + // точное совпадение + if (exactMatch) { + const match = palette.find( + (c) => c.rgb[0] === targetRgb[0] && c.rgb[1] === targetRgb[1] && c.rgb[2] === targetRgb[2] + ); + const result = match ? { id: match.id, rgb: [...match.rgb] } : { id: null, rgb: targetRgb }; + colorCache.set(cacheKey, result); + return result; + } + + // проверка на белый через порог + const whiteThreshold = state.customWhiteThreshold || CONFIG.WHITE_THRESHOLD; + if ( + targetRgb[0] >= whiteThreshold && + targetRgb[1] >= whiteThreshold && + targetRgb[2] >= whiteThreshold + ) { + const whiteEntry = palette.find( + (c) => + c.rgb[0] >= whiteThreshold && c.rgb[1] >= whiteThreshold && c.rgb[2] >= whiteThreshold + ); + if (whiteEntry) { + const result = { id: whiteEntry.id, rgb: [...whiteEntry.rgb] }; + colorCache.set(cacheKey, result); + return result; + } + } + + // поиск ближайшего цвета + let bestId = palette[0].id; + let bestRgb = [...palette[0].rgb]; + let bestScore = Infinity; + + if (state.colorMatchingAlgorithm === 'legacy') { + for (let i = 0; i < palette.length; i++) { + const c = palette[i]; + const [r, g, b] = c.rgb; + const rmean = (r + targetRgb[0]) / 2; + const rdiff = r - targetRgb[0]; + const gdiff = g - targetRgb[1]; + const bdiff = b - targetRgb[2]; + const dist = Math.sqrt( + (((512 + rmean) * rdiff * rdiff) >> 8) + + 4 * gdiff * gdiff + + (((767 - rmean) * bdiff * bdiff) >> 8) + ); + if (dist < bestScore) { + bestScore = dist; + bestId = c.id; + bestRgb = [...c.rgb]; + if (dist === 0) break; + } + } + } else { + const [Lt, at, bt] = Utils._lab(targetRgb[0], targetRgb[1], targetRgb[2]); + const targetChroma = Math.sqrt(at * at + bt * bt); + const penaltyWeight = state.enableChromaPenalty ? state.chromaPenaltyWeight || 0.15 : 0; + + for (let i = 0; i < palette.length; i++) { + const c = palette[i]; + const [r, g, b] = c.rgb; + const [L2, a2, b2] = Utils._lab(r, g, b); + const dL = Lt - L2, + da = at - a2, + db = bt - b2; + let dist = dL * dL + da * da + db * db; + + if (penaltyWeight > 0 && targetChroma > 20) { + const candChroma = Math.sqrt(a2 * a2 + b2 * b2); + if (candChroma < targetChroma) { + const cd = targetChroma - candChroma; + dist += cd * cd * penaltyWeight; + } + } + + if (dist < bestScore) { + bestScore = dist; + bestId = c.id; + bestRgb = [...c.rgb]; + if (dist === 0) break; + } + } + } + + const result = { id: bestId, rgb: bestRgb }; + colorCache.set(cacheKey, result); + + // ограничение кеша + if (colorCache.size > 15000) { + const firstKey = colorCache.keys().next().value; + colorCache.delete(firstKey); + } + + return result; + }, + createImageUploader: () => new Promise((resolve) => { const input = document.createElement('input'); @@ -1827,7 +1932,7 @@ }), extractAvailableColors: () => { - const colorElements = document.querySelectorAll('[id^="color-"]'); + const colorElements = document.querySelectorAll('.tooltip button[id^="color-"]'); // Separate available and unavailable colors const availableColors = []; @@ -1838,7 +1943,11 @@ if (id === 0) return; // Skip transparent color const rgbStr = el.style.backgroundColor.match(/\d+/g); - const rgb = rgbStr ? rgbStr.map(Number) : [0, 0, 0]; + if (!rgbStr || rgbStr.length < 3) { + console.warn(`Skipping color element ${el.id} — cannot parse RGB`); + return; + } + const rgb = rgbStr.map(Number); // Find color name from COLOR_MAP const colorInfo = Object.values(CONFIG.COLOR_MAP).find((color) => color.id === id); @@ -2114,54 +2223,83 @@ } }, - saveProgress: () => { - try { - // Pack painted map if available - let paintedMapPacked = null; - if (state.paintedMap && state.imageData) { - const data = Utils.packPaintedMapToBase64( - state.paintedMap, - state.imageData.width, - state.imageData.height - ); - if (data) { - paintedMapPacked = { + buildPaintedMapPacked() { + if (state.paintedMap && state.imageData) { + const data = Utils.packPaintedMapToBase64( + state.paintedMap, + state.imageData.width, + state.imageData.height + ); + if (data) { + return { + width: state.imageData.width, + height: state.imageData.height, + data: data, + }; + } + } + return null; + }, + + buildProgressData() { + return { + timestamp: Date.now(), + version: '2.2', + state: { + totalPixels: state.totalPixels, + paintedPixels: state.paintedPixels, + lastPosition: state.lastPosition, + startPosition: state.startPosition, + region: state.region, + imageLoaded: state.imageLoaded, + colorsChecked: state.colorsChecked, + coordinateMode: state.coordinateMode, + coordinateDirection: state.coordinateDirection, + coordinateSnake: state.coordinateSnake, + blockWidth: state.blockWidth, + blockHeight: state.blockHeight, + availableColors: state.availableColors, + }, + imageData: state.imageData + ? { width: state.imageData.width, height: state.imageData.height, - data: data, - }; - } - } + pixels: Array.from(state.imageData.pixels), + totalPixels: state.imageData.totalPixels, + } + : null, + paintedMapPacked: Utils.buildPaintedMapPacked(), + }; + }, - const progressData = { - timestamp: Date.now(), - version: '2.2', - state: { - totalPixels: state.totalPixels, - paintedPixels: state.paintedPixels, - lastPosition: state.lastPosition, - startPosition: state.startPosition, - region: state.region, - imageLoaded: state.imageLoaded, - colorsChecked: state.colorsChecked, - availableColors: state.availableColors, - // Добавляем настройки генерации координат - coordinateMode: state.coordinateMode, - coordinateDirection: state.coordinateDirection, - coordinateSnake: state.coordinateSnake, - blockWidth: state.blockWidth, - blockHeight: state.blockHeight, - }, - imageData: state.imageData - ? { - width: state.imageData.width, - height: state.imageData.height, - pixels: Array.from(state.imageData.pixels), - totalPixels: state.imageData.totalPixels, - } - : null, - paintedMapPacked: paintedMapPacked, - }; + migrateProgress(saved) { + if (!saved) return null; + + let data = saved; + const ver = data.version; + + // если версия не указана или <= 1.x → сначала в v2 + if (!ver || ver === '1' || ver === '1.0' || ver === '1.1') { + data = Utils.migrateProgressToV2(data); + } + + // если всё ещё старее v2.1 → мигрируем в 2.1 + if (data.version === '2' || data.version === '2.0') { + data = Utils.migrateProgressToV21(data); + } + + // если всё ещё старее v2.2 → мигрируем в 2.2 + if (data.version === '2.1') { + data = Utils.migrateProgressToV22(data); + } + + // теперь точно последняя версия + return data; + }, + + saveProgress: () => { + try { + const progressData = Utils.buildProgressData(state); localStorage.setItem('wplace-bot-progress', JSON.stringify(progressData)); return true; @@ -2176,27 +2314,14 @@ const saved = localStorage.getItem('wplace-bot-progress'); if (!saved) return null; let data = JSON.parse(saved); - const ver = data.version; - let migrated = data; - - // Миграция для новых версий - if (ver === '2.2') { - // already latest - } else if (ver === '2.1') { - migrated = Utils.migrateProgressToV22(data); - } else if (ver === '2' || ver === '2.0') { - migrated = Utils.migrateProgressToV22(Utils.migrateProgressToV21(data)); - } else { - migrated = Utils.migrateProgressToV22(Utils.migrateProgressToV21(data)); - } + const migrated = Utils.migrateProgress(data); if (migrated && migrated !== data) { try { localStorage.setItem('wplace-bot-progress', JSON.stringify(migrated)); } catch {} - data = migrated; } - return data; + return migrated; } catch (error) { console.error('Error loading progress:', error); return null; @@ -2289,47 +2414,7 @@ saveProgressToFile: () => { try { - // Pack painted map if available - let paintedMapPacked = null; - if (state.paintedMap && state.imageData) { - const data = Utils.packPaintedMapToBase64( - state.paintedMap, - state.imageData.width, - state.imageData.height - ); - if (data) { - paintedMapPacked = { - width: state.imageData.width, - height: state.imageData.height, - data: data, - }; - } - } - - const progressData = { - timestamp: Date.now(), - version: '2.1', - state: { - totalPixels: state.totalPixels, - paintedPixels: state.paintedPixels, - lastPosition: state.lastPosition, - startPosition: state.startPosition, - region: state.region, - imageLoaded: state.imageLoaded, - colorsChecked: state.colorsChecked, - availableColors: state.availableColors, - }, - imageData: state.imageData - ? { - width: state.imageData.width, - height: state.imageData.height, - pixels: Array.from(state.imageData.pixels), - totalPixels: state.imageData.totalPixels, - } - : null, - paintedMapPacked: paintedMapPacked, - }; - + const progressData = Utils.buildProgressData(); const filename = `wplace-bot-progress-${new Date() .toISOString() .slice(0, 19) @@ -2348,14 +2433,8 @@ if (!data || !data.state) { throw new Error('Invalid file format'); } - const ver = data.version; - let migrated = data; - if (ver === '2.1') { - } else if (ver === '2' || ver === '2.0') { - migrated = Utils.migrateProgressToV21(data) || data; - } else { - migrated = Utils.migrateProgressToV21(data) || data; - } + const migrated = Utils.migrateProgress(data); + const success = Utils.restoreProgress(migrated); return success; } catch (error) { @@ -2642,90 +2721,9 @@ // COLOR MATCHING FUNCTION - Optimized with caching const colorCache = new Map(); - function findClosestColor(targetRgb, availableColors) { - if (!availableColors || availableColors.length === 0) return 1; - const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}|${ - state.colorMatchingAlgorithm - }|${state.enableChromaPenalty ? 'c' : 'nc'}|${state.chromaPenaltyWeight}`; - if (colorCache.has(cacheKey)) return colorCache.get(cacheKey); - - const whiteThreshold = state.customWhiteThreshold || CONFIG.WHITE_THRESHOLD; - if ( - targetRgb[0] >= whiteThreshold && - targetRgb[1] >= whiteThreshold && - targetRgb[2] >= whiteThreshold - ) { - const whiteEntry = availableColors.find( - (c) => - c.rgb[0] >= whiteThreshold && c.rgb[1] >= whiteThreshold && c.rgb[2] >= whiteThreshold - ); - if (whiteEntry) { - colorCache.set(cacheKey, whiteEntry.id); - return whiteEntry.id; - } - } - - let bestId = availableColors[0].id; - let bestScore = Infinity; - - if (state.colorMatchingAlgorithm === 'legacy') { - for (let i = 0; i < availableColors.length; i++) { - const c = availableColors[i]; - const [r, g, b] = c.rgb; - const rmean = (r + targetRgb[0]) / 2; - const rdiff = r - targetRgb[0]; - const gdiff = g - targetRgb[1]; - const bdiff = b - targetRgb[2]; - const dist = Math.sqrt( - (((512 + rmean) * rdiff * rdiff) >> 8) + - 4 * gdiff * gdiff + - (((767 - rmean) * bdiff * bdiff) >> 8) - ); - if (dist < bestScore) { - bestScore = dist; - bestId = c.id; - if (dist === 0) break; - } - } - } else { - // lab - const [Lt, at, bt] = Utils._lab(targetRgb[0], targetRgb[1], targetRgb[2]); - const targetChroma = Math.sqrt(at * at + bt * bt); - const penaltyWeight = state.enableChromaPenalty ? state.chromaPenaltyWeight || 0.15 : 0; - for (let i = 0; i < availableColors.length; i++) { - const c = availableColors[i]; - const [r, g, b] = c.rgb; - const [L2, a2, b2] = Utils._lab(r, g, b); - const dL = Lt - L2, - da = at - a2, - db = bt - b2; - let dist = dL * dL + da * da + db * db; - if (penaltyWeight > 0 && targetChroma > 20) { - const candChroma = Math.sqrt(a2 * a2 + b2 * b2); - if (candChroma < targetChroma) { - const cd = targetChroma - candChroma; - dist += cd * cd * penaltyWeight; - } - } - if (dist < bestScore) { - bestScore = dist; - bestId = c.id; - if (dist === 0) break; - } - } - } - - colorCache.set(cacheKey, bestId); - if (colorCache.size > 15000) { - const firstKey = colorCache.keys().next().value; - colorCache.delete(firstKey); - } - return bestId; - } - // UI UPDATE FUNCTIONS (declared early to avoid reference errors) let updateUI = () => {}; - let updateStats = () => {}; + let updateStats = (isManualRefresh) => {}; let updateDataButtons = () => {}; function updateActiveColorPalette() { @@ -3271,6 +3269,7 @@ } `; + // noinspection CssInvalidFunction settingsContainer.innerHTML = `
@@ -3556,7 +3555,7 @@

- +