fixed non null assertion + chains

removed some non null assetions + changed to optional chains
This commit is contained in:
Decrypt 2025-08-22 16:06:12 +02:00
parent 812181c204
commit 6653fc6674
2 changed files with 478 additions and 413 deletions

View file

@ -12,13 +12,7 @@ import { EV_ANCHOR_SET, EV_AUTOCAP_CHANGED } from '../core/events';
let panelEl: HTMLDivElement | null = null;
function $(id: string): HTMLElement {
const el = document.getElementById(id);
if (!el) {
throw new Error(`Element with id "${id}" not found.`);
}
return el;
}
function $(id: string): HTMLElement | null { return document.getElementById(id); }
export function createUI() {
if (document.getElementById('overlay-pro-panel')) return;
@ -29,8 +23,10 @@ export function createUI() {
const panelW = 340;
const defaultLeft = Math.max(12, window.innerWidth - panelW - 80);
panel.style.left = `${Number.isFinite(config.panelX as any) ? (config.panelX as any) : defaultLeft}px`;
panel.style.top = `${Number.isFinite(config.panelY as any) ? (config.panelY as any) : 120}px`;
const initLeft = (typeof config.panelX === 'number' && Number.isFinite(config.panelX)) ? config.panelX : defaultLeft;
const initTop = (typeof config.panelY === 'number' && Number.isFinite(config.panelY)) ? config.panelY : 120;
panel.style.left = `${initLeft}px`;
panel.style.top = `${initTop}px`;
panel.innerHTML = `
<div class="op-header" id="op-header">
@ -179,7 +175,8 @@ export function createUI() {
}
function rebuildOverlayListUI() {
const list = $('op-overlay-list');
const list = $('op-overlay-list') as HTMLDivElement | null;
if (!list) return;
list.innerHTML = '';
for (const ov of config.overlays) {
const item = document.createElement('div');
@ -213,7 +210,7 @@ function rebuildOverlayListUI() {
async function addBlankOverlay() {
const name = uniqueName('Overlay', config.overlays.map(o => o.name || ''));
const ov = { id: uid(), name, enabled: true, imageUrl: null, imageBase64: null, isLocal: false, pixelUrl: null, offsetX: 0, offsetY: 0, opacity: 0.7 };
const ov = { id: uid(), name, enabled: true, imageUrl: null as string | null, imageBase64: null as string | null, isLocal: false, pixelUrl: null as string | null, offsetX: 0, offsetY: 0, opacity: 0.7 };
config.overlays.push(ov);
config.activeOverlayId = ov.id;
await saveConfig(['overlays', 'activeOverlayId']);
@ -240,7 +237,7 @@ async function setOverlayImageFromFile(ov: any, file: File) {
showToast('Local image loaded. Placement mode ON -- click once to set anchor.');
}
type ImportedOverlay = {
type ImportOverlay = {
name?: string;
imageUrl?: string;
pixelUrl?: string | null;
@ -249,18 +246,31 @@ type ImportedOverlay = {
opacity?: number;
};
function isImportOverlay(val: unknown): val is ImportOverlay {
return typeof val === 'object' && val !== null;
}
async function importOverlayFromJSON(jsonText: string) {
let obj: unknown;
try { obj = JSON.parse(jsonText) as unknown; } catch { alert('Invalid JSON'); return; }
const arr: ImportedOverlay[] = Array.isArray(obj) ? (obj as ImportedOverlay[]) : [obj as ImportedOverlay];
let parsed: unknown;
try {
parsed = JSON.parse(jsonText) as unknown;
} catch {
alert('Invalid JSON');
return;
}
const arr: ImportOverlay[] = Array.isArray(parsed)
? parsed.filter(isImportOverlay)
: (isImportOverlay(parsed) ? [parsed] : []);
let imported = 0, failed = 0;
for (const item of arr) {
const name = uniqueName(item?.name || 'Imported Overlay', config.overlays.map(o => o.name || ''));
const imageUrl = item?.imageUrl;
const pixelUrl = item?.pixelUrl ?? null;
const offsetX = Number.isFinite(item?.offsetX) ? (item?.offsetX as number) : 0;
const offsetY = Number.isFinite(item?.offsetY) ? (item?.offsetY as number) : 0;
const opacity = Number.isFinite(item?.opacity) ? (item?.opacity as number) : 0.7;
const pixelUrl = (item?.pixelUrl ?? null) as string | null;
const offsetX = (typeof item?.offsetX === 'number' && Number.isFinite(item.offsetX)) ? item.offsetX : 0;
const offsetY = (typeof item?.offsetY === 'number' && Number.isFinite(item.offsetY)) ? item.offsetY : 0;
const opacity = (typeof item?.opacity === 'number' && Number.isFinite(item.opacity)) ? item.opacity : 0.7;
if (!imageUrl) { failed++; continue; }
try {
const base64 = await urlToDataURL(imageUrl);
@ -269,7 +279,7 @@ async function importOverlayFromJSON(jsonText: string) {
} catch (e) { console.error('Import failed for', imageUrl, e); failed++; }
}
if (imported > 0) {
config.activeOverlayId = config.overlays[config.overlays.length - 1]?.id || null;
config.activeOverlayId = config.overlays[config.overlays.length - 1].id;
await saveConfig(['overlays', 'activeOverlayId']); clearOverlayCache(); ensureHook(); updateUI();
}
alert(`Import finished. Imported: ${imported}${failed ? `, Failed: ${failed}` : ''}`);
@ -289,9 +299,9 @@ function copyText(text: string) {
}
function addEventListeners(panel: HTMLDivElement) {
$('op-theme-toggle').addEventListener('click', async (e) => { e.stopPropagation(); config.theme = config.theme === 'light' ? 'dark' : 'light'; await saveConfig(['theme']); applyTheme(); });
$('op-refresh-btn').addEventListener('click', (e) => { e.stopPropagation(); location.reload(); });
$('op-panel-toggle').addEventListener('click', (e) => { e.stopPropagation(); config.isPanelCollapsed = !config.isPanelCollapsed; saveConfig(['isPanelCollapsed']); updateUI(); });
$('op-theme-toggle')?.addEventListener('click', async (e) => { e.stopPropagation(); config.theme = config.theme === 'light' ? 'dark' : 'light'; await saveConfig(['theme']); applyTheme(); });
$('op-refresh-btn')?.addEventListener('click', (e) => { e.stopPropagation(); location.reload(); });
$('op-panel-toggle')?.addEventListener('click', (e) => { e.stopPropagation(); config.isPanelCollapsed = !config.isPanelCollapsed; saveConfig(['isPanelCollapsed']); updateUI(); });
panel.querySelectorAll('.op-tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
@ -306,21 +316,27 @@ function addEventListeners(panel: HTMLDivElement) {
updateUI();
});
});
$('op-style-dots').addEventListener('change', () => { if (($('op-style-dots') as HTMLInputElement).checked) { config.minifyStyle = 'dots'; saveConfig(['minifyStyle']); clearOverlayCache(); ensureHook(); }});
$('op-style-symbols').addEventListener('change', () => { if (($('op-style-symbols') as HTMLInputElement).checked) { config.minifyStyle = 'symbols'; saveConfig(['minifyStyle']); clearOverlayCache(); ensureHook(); }});
$('op-autocap-toggle').addEventListener('click', () => { config.autoCapturePixelUrl = !config.autoCapturePixelUrl; saveConfig(['autoCapturePixelUrl']); ensureHook(); updateUI(); });
const styleDotsEl = $('op-style-dots') as HTMLInputElement | null;
styleDotsEl?.addEventListener('change', () => { if (styleDotsEl.checked) { config.minifyStyle = 'dots'; saveConfig(['minifyStyle']); clearOverlayCache(); ensureHook(); } });
$('op-add-overlay').addEventListener('click', async () => { try { await addBlankOverlay(); } catch (e) { console.error(e); } });
$('op-import-overlay').addEventListener('click', async () => { const text = prompt('Paste overlay JSON (single or array):'); if (!text) return; await importOverlayFromJSON(text); });
$('op-export-overlay').addEventListener('click', () => exportActiveOverlayToClipboard());
$('op-collapse-list').addEventListener('click', () => { config.collapseList = !config.collapseList; saveConfig(['collapseList']); updateUI(); });
$('op-collapse-editor').addEventListener('click', () => { config.collapseEditor = !config.collapseEditor; saveConfig(['collapseEditor']); updateUI(); });
$('op-collapse-positioning').addEventListener('click', () => { config.collapsePositioning = !config.collapsePositioning; saveConfig(['collapsePositioning']); updateUI(); });
const styleSymbolsEl = $('op-style-symbols') as HTMLInputElement | null;
styleSymbolsEl?.addEventListener('change', () => { if (styleSymbolsEl.checked) { config.minifyStyle = 'symbols'; saveConfig(['minifyStyle']); clearOverlayCache(); ensureHook(); } });
$('op-name').addEventListener('change', async (e: any) => {
$('op-autocap-toggle')?.addEventListener('click', () => { config.autoCapturePixelUrl = !config.autoCapturePixelUrl; saveConfig(['autoCapturePixelUrl']); ensureHook(); updateUI(); });
$('op-add-overlay')?.addEventListener('click', async () => { try { await addBlankOverlay(); } catch (e) { console.error(e); } });
$('op-import-overlay')?.addEventListener('click', async () => { const text = prompt('Paste overlay JSON (single or array):'); if (!text) return; await importOverlayFromJSON(text); });
$('op-export-overlay')?.addEventListener('click', () => exportActiveOverlayToClipboard());
$('op-collapse-list')?.addEventListener('click', () => { config.collapseList = !config.collapseList; saveConfig(['collapseList']); updateUI(); });
$('op-collapse-editor')?.addEventListener('click', () => { config.collapseEditor = !config.collapseEditor; saveConfig(['collapseEditor']); updateUI(); });
$('op-collapse-positioning')?.addEventListener('click', () => { config.collapsePositioning = !config.collapsePositioning; saveConfig(['collapsePositioning']); updateUI(); });
const nameInput = $('op-name') as HTMLInputElement | null;
nameInput?.addEventListener('change', async (e: Event) => {
const ov = getActiveOverlay(); if (!ov) return;
const desired = (e.target.value || '').trim() || 'Overlay';
const target = e.target as HTMLInputElement;
const desired = (target.value || '').trim() || 'Overlay';
if (config.overlays.some(o => o.id !== ov.id && (o.name || '').toLowerCase() === desired.toLowerCase())) {
ov.name = uniqueName(desired, config.overlays.map(o => o.name || ''));
showToast(`Name in use. Renamed to "${ov.name}".`);
@ -328,48 +344,56 @@ function addEventListeners(panel: HTMLDivElement) {
await saveConfig(['overlays']); rebuildOverlayListUI();
});
$('op-fetch').addEventListener('click', async () => {
$('op-fetch')?.addEventListener('click', async () => {
const ov = getActiveOverlay(); if (!ov) { alert('No active overlay selected.'); return; }
if (ov.imageBase64) { alert('This overlay already has an image. Create a new overlay to change the image.'); return; }
const url = ( $('op-image-url') as HTMLInputElement ).value.trim(); if (!url) { alert('Enter an image link first.'); return; }
const urlEl = $('op-image-url') as HTMLInputElement | null;
const url = urlEl?.value.trim(); if (!url) { alert('Enter an image link first.'); return; }
try { await setOverlayImageFromURL(ov, url); } catch (e) { console.error(e); alert('Failed to fetch image.'); }
});
const dropzone = $('op-dropzone');
dropzone.addEventListener('click', () => $('op-file-input').click());
$('op-file-input').addEventListener('change', async (e: any) => {
const file = e.target.files?.[0]; e.target.value=''; if (!file) return;
const dropzone = $('op-dropzone') as HTMLDivElement | null;
dropzone?.addEventListener('click', () => ( $('op-file-input') as HTMLInputElement | null )?.click());
const fileInput = $('op-file-input') as HTMLInputElement | null;
fileInput?.addEventListener('change', async (e: Event) => {
const target = e.target as HTMLInputElement;
const file = target?.files?.[0]; if (target) target.value = ''; if (!file) return;
const ov = getActiveOverlay(); if (!ov) return;
if (ov.imageBase64) { alert('This overlay already has an image. Create a new overlay to change the image.'); return; }
try { await setOverlayImageFromFile(ov, file); } catch (err) { console.error(err); alert('Failed to load local image.'); }
});
['dragenter', 'dragover'].forEach(evt => dropzone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drop-highlight'); }));
['dragleave', 'drop'].forEach(evt => dropzone.addEventListener(evt, (e: any) => { e.preventDefault(); e.stopPropagation(); if (evt === 'dragleave' && e.target !== dropzone) return; dropzone.classList.remove('drop-highlight'); }));
dropzone.addEventListener('drop', async (e: any) => {
const dt = e.dataTransfer; if (!dt) return; const file = dt.files?.[0]; if (!file) return;
const ov = getActiveOverlay(); if (!ov) return;
if (ov.imageBase64) { alert('This overlay already has an image. Create a new overlay to change the image.'); return; }
try { await setOverlayImageFromFile(ov, file); } catch (err) { console.error(err); alert('Failed to load dropped image.'); }
});
if (dropzone) {
['dragenter', 'dragover'].forEach(evt => dropzone.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drop-highlight'); }));
['dragleave', 'drop'].forEach(evt => dropzone.addEventListener(evt, (e: any) => { e.preventDefault(); e.stopPropagation(); if (evt === 'dragleave' && e.target !== dropzone) return; dropzone.classList.remove('drop-highlight'); }));
dropzone.addEventListener('drop', async (e: DragEvent) => {
const dt = e.dataTransfer; if (!dt) return; const file = dt?.files?.[0]; if (!file) return;
const ov = getActiveOverlay(); if (!ov) return;
if (ov.imageBase64) { alert('This overlay already has an image. Create a new overlay to change the image.'); return; }
try { await setOverlayImageFromFile(ov, file); } catch (err) { console.error(err); alert('Failed to load dropped image.'); }
});
}
const nudge = async (dx: number, dy: number) => {
const ov = getActiveOverlay(); if (!ov) return;
ov.offsetX += dx; ov.offsetY += dy;
await saveConfig(['overlays']); clearOverlayCache(); updateUI();
};
$('op-nudge-up').addEventListener('click', () => nudge(0, -1));
$('op-nudge-down').addEventListener('click', () => nudge(0, 1));
$('op-nudge-left').addEventListener('click', () => nudge(-1, 0));
$('op-nudge-right').addEventListener('click', () => nudge(1, 0));
$('op-nudge-up')?.addEventListener('click', () => nudge(0, -1));
$('op-nudge-down')?.addEventListener('click', () => nudge(0, 1));
$('op-nudge-left')?.addEventListener('click', () => nudge(-1, 0));
$('op-nudge-right')?.addEventListener('click', () => nudge(1, 0));
$('op-opacity-slider').addEventListener('input', (e: any) => {
const opacitySlider = $('op-opacity-slider') as HTMLInputElement | null;
opacitySlider?.addEventListener('input', (e: Event) => {
const ov = getActiveOverlay(); if (!ov) return;
ov.opacity = parseFloat(e.target.value);
$('op-opacity-value').textContent = `${Math.round(ov.opacity * 100)}%`;
const target = e.target as HTMLInputElement;
ov.opacity = parseFloat(target.value);
const valEl = $('op-opacity-value');
if (valEl) valEl.textContent = `${Math.round(ov.opacity * 100)}%`;
});
$('op-opacity-slider').addEventListener('change', async () => { await saveConfig(['overlays']); clearOverlayCache(); });
opacitySlider?.addEventListener('change', async () => { await saveConfig(['overlays']); clearOverlayCache(); });
$('op-download-overlay').addEventListener('click', () => {
$('op-download-overlay')?.addEventListener('click', () => {
const ov = getActiveOverlay();
if (!ov || !ov.imageBase64) { showToast('No overlay image to download.'); return; }
const a = document.createElement('a');
@ -380,7 +404,7 @@ function addEventListeners(panel: HTMLDivElement) {
a.remove();
});
$('op-open-cc').addEventListener('click', () => {
$('op-open-cc')?.addEventListener('click', () => {
const ov = getActiveOverlay(); if (!ov || !ov.imageBase64) { showToast('No overlay image to edit.'); return; }
openCCModal(ov);
});
@ -395,7 +419,7 @@ function addEventListeners(panel: HTMLDivElement) {
}
function enableDrag(panel: HTMLDivElement) {
const header = panel.querySelector('#op-header') as HTMLDivElement;
const header = panel.querySelector('#op-header') as HTMLDivElement | null;
if (!header) return;
let isDragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0, moved = false;
@ -413,7 +437,7 @@ function enableDrag(panel: HTMLDivElement) {
const maxLeft = Math.max(8, window.innerWidth - panel.offsetWidth - 8);
const maxTop = Math.max(8, window.innerHeight - panel.offsetHeight - 8);
panel.style.left = `${clamp(startLeft + dx, 8, maxLeft)}px`;
panel.style.top = `${clamp(startTop + dy, 8, maxTop)}px`;
panel.style.top = `${clamp(startTop + dy, 8, maxTop)}px`;
moved = true;
};
const onPointerUp = (e: PointerEvent) => {
@ -441,31 +465,36 @@ function enableDrag(panel: HTMLDivElement) {
});
}
export function updateEditorUI(): void {
const editorSect = $('op-editor-section');
const editorBody = $('op-editor-body');
function updateEditorUI() {
const editorSect = $('op-editor-section') as HTMLDivElement | null;
const editorBody = $('op-editor-body') as HTMLDivElement | null;
const ov = getActiveOverlay();
if (!editorSect) return;
editorSect.style.display = ov ? 'flex' : 'none';
if (!ov) return;
if (!ov || !editorBody) return;
( $('op-name') as HTMLInputElement ).value = ov.name || '';
const nameEl = $('op-name') as HTMLInputElement | null;
if (nameEl) {
nameEl.value = ov.name || '';
}
const srcWrap = $('op-image-source');
const previewWrap = $('op-preview-wrap');
const previewImg = $('op-image-preview') as HTMLImageElement;
const ccRow = $('op-cc-btn-row');
const srcWrap = $('op-image-source') as HTMLDivElement | null;
const previewWrap = $('op-preview-wrap') as HTMLDivElement | null;
const previewImg = $('op-image-preview') as HTMLImageElement | null;
const ccRow = $('op-cc-btn-row') as HTMLDivElement | null;
if (ov.imageBase64) {
srcWrap.style.display = 'none';
previewWrap.style.display = 'flex';
previewImg.src = ov.imageBase64;
ccRow.style.display = 'flex';
if (srcWrap) srcWrap.style.display = 'none';
if (previewWrap) previewWrap.style.display = 'flex';
if (previewImg) previewImg.src = ov.imageBase64;
if (ccRow) ccRow.style.display = 'flex';
} else {
srcWrap.style.display = 'block';
previewWrap.style.display = 'none';
ccRow.style.display = 'none';
( $('op-image-url') as HTMLInputElement ).value = ov.imageUrl || '';
if (srcWrap) srcWrap.style.display = 'block';
if (previewWrap) previewWrap.style.display = 'none';
if (ccRow) ccRow.style.display = 'none';
const urlInput = $('op-image-url') as HTMLInputElement | null;
if (urlInput) urlInput.value = ov.imageUrl || '';
}
const coords = ov.pixelUrl ? extractPixelCoords(ov.pixelUrl) : { chunk1: '-', chunk2: '-', posX: '-', posY: '-' } as any;
@ -489,86 +518,97 @@ export function updateUI() {
applyTheme();
const content = $('op-content');
const toggle = $('op-panel-toggle');
const content = $('op-content') as HTMLDivElement | null;
const toggle = $('op-panel-toggle') as HTMLButtonElement | null;
const collapsed = !!config.isPanelCollapsed;
content.style.display = collapsed ? 'none' : 'flex';
toggle.textContent = collapsed ? '▸' : '▾';
toggle.title = collapsed ? 'Expand' : 'Collapse';
if (content) content.style.display = collapsed ? 'none' : 'flex';
if (toggle) {
toggle.textContent = collapsed ? '▸' : '▾';
toggle.title = collapsed ? 'Expand' : 'Collapse';
}
// --- Mode Tabs ---
if (panelEl) {
panelEl.querySelectorAll('.op-tab-btn').forEach(btn => {
const mode = btn.getAttribute('data-mode');
let isActive = false;
if (mode === 'above' && (config.overlayMode === 'above' || config.overlayMode === 'behind')) {
panelEl.querySelectorAll('.op-tab-btn').forEach(btn => {
const mode = btn.getAttribute('data-mode');
let isActive = false;
if (mode === 'above' && (config.overlayMode === 'above' || config.overlayMode === 'behind')) {
isActive = true;
} else {
} else {
isActive = mode === config.overlayMode;
}
btn.classList.toggle('active', isActive);
});
}
}
(btn as HTMLButtonElement).classList.toggle('active', isActive);
});
// --- Mode Settings ---
const fullOverlaySettings = $('op-mode-settings').querySelector('[data-setting="above"]') as HTMLDivElement;
const minifySettings = $('op-mode-settings').querySelector('[data-setting="minify"]') as HTMLDivElement;
const settingsRoot = $('op-mode-settings') as HTMLDivElement | null;
const fullOverlaySettings = settingsRoot?.querySelector('[data-setting="above"]') as HTMLDivElement | null;
const minifySettings = settingsRoot?.querySelector('[data-setting="minify"]') as HTMLDivElement | null;
if (config.overlayMode === 'above' || config.overlayMode === 'behind') {
fullOverlaySettings.classList.add('active');
minifySettings.classList.remove('active');
fullOverlaySettings?.classList.add('active');
minifySettings?.classList.remove('active');
const ov = getActiveOverlay();
if (ov) {
( $('op-opacity-slider') as HTMLInputElement ).value = String(ov.opacity);
$('op-opacity-value').textContent = `${Math.round(ov.opacity * 100)}%`;
const opacitySlider = $('op-opacity-slider') as HTMLInputElement | null;
const opacityVal = $('op-opacity-value');
if (opacitySlider) opacitySlider.value = String(ov.opacity);
if (opacityVal) opacityVal.textContent = `${Math.round(ov.opacity * 100)}%`;
}
} else if (config.overlayMode === 'minify') {
fullOverlaySettings.classList.remove('active');
minifySettings.classList.add('active');
fullOverlaySettings?.classList.remove('active');
minifySettings?.classList.add('active');
} else {
fullOverlaySettings.classList.remove('active');
minifySettings.classList.remove('active');
fullOverlaySettings?.classList.remove('active');
minifySettings?.classList.remove('active');
}
($('op-style-dots') as HTMLInputElement).checked = config.minifyStyle === 'dots';
($('op-style-symbols') as HTMLInputElement).checked = config.minifyStyle === 'symbols';
const layeringBtns = $('op-layering-btns');
layeringBtns.innerHTML = '';
const behindBtn = document.createElement('button');
behindBtn.textContent = 'Behind';
behindBtn.className = `op-button${config.overlayMode === 'behind' ? ' active' : ''}`;
behindBtn.addEventListener('click', () => { config.overlayMode = 'behind'; saveConfig(['overlayMode']); ensureHook(); updateUI(); });
const aboveBtn = document.createElement('button');
aboveBtn.textContent = 'Above';
aboveBtn.className = `op-button${config.overlayMode === 'above' ? ' active' : ''}`;
aboveBtn.addEventListener('click', () => { config.overlayMode = 'above'; saveConfig(['overlayMode']); ensureHook(); updateUI(); });
layeringBtns.appendChild(behindBtn);
layeringBtns.appendChild(aboveBtn);
const dotsEl = $('op-style-dots') as HTMLInputElement | null;
if (dotsEl) dotsEl.checked = config.minifyStyle === 'dots';
const symbolsEl = $('op-style-symbols') as HTMLInputElement | null;
if (symbolsEl) symbolsEl.checked = config.minifyStyle === 'symbols';
const layeringBtns = $('op-layering-btns') as HTMLDivElement | null;
if (layeringBtns) {
layeringBtns.innerHTML = '';
const behindBtn = document.createElement('button');
behindBtn.textContent = 'Behind';
behindBtn.className = `op-button${config.overlayMode === 'behind' ? ' active' : ''}`;
behindBtn.addEventListener('click', () => { config.overlayMode = 'behind'; saveConfig(['overlayMode']); ensureHook(); updateUI(); });
const aboveBtn = document.createElement('button');
aboveBtn.textContent = 'Above';
aboveBtn.className = `op-button${config.overlayMode === 'above' ? ' active' : ''}`;
aboveBtn.addEventListener('click', () => { config.overlayMode = 'above'; saveConfig(['overlayMode']); ensureHook(); updateUI(); });
layeringBtns.appendChild(behindBtn);
layeringBtns.appendChild(aboveBtn);
}
// --- Positioning Section ---
const autoBtn = $('op-autocap-toggle');
const placeLabel = $('op-place-label');
autoBtn.textContent = config.autoCapturePixelUrl ? 'Enabled' : 'Disabled';
autoBtn.classList.toggle('op-danger', !!config.autoCapturePixelUrl);
placeLabel?.classList.toggle('op-danger-text', !!config.autoCapturePixelUrl);
if (autoBtn) {
autoBtn.textContent = config.autoCapturePixelUrl ? 'Enabled' : 'Disabled';
autoBtn.classList.toggle('op-danger', !!config.autoCapturePixelUrl);
}
if (placeLabel) placeLabel.classList.toggle('op-danger-text', !!config.autoCapturePixelUrl);
const positioningBody: HTMLElement = $('op-positioning-body');
const positioningBody = $('op-positioning-body');
const positioningCz = $('op-collapse-positioning');
positioningBody.style.display = config.collapsePositioning ? 'none' : 'block';
if (positioningCz) positioningCz.textContent = config.collapsePositioning ? '▸' : '▾';
if (positioningBody) positioningBody.style.display = config.collapsePositioning ? 'none' : 'block';
if (positioningCz) (positioningCz as HTMLButtonElement).textContent = config.collapsePositioning ? '▸' : '▾';
const listWrap = $('op-list-wrap');
const listWrap = $('op-list-wrap') as HTMLDivElement | null;
const listCz = $('op-collapse-list');
listWrap.style.display = config.collapseList ? 'none' : 'block';
if (listCz) listCz.textContent = config.collapseList ? '▸' : '▾';
if (listWrap) listWrap.style.display = config.collapseList ? 'none' : 'block';
if (listCz) (listCz as HTMLButtonElement).textContent = config.collapseList ? '▸' : '▾';
rebuildOverlayListUI();
updateEditorUI(); // <- now exported, so noUnusedLocals won't flag it
updateEditorUI();
const exportBtn = $('op-export-overlay') as HTMLButtonElement;
const exportBtn = $('op-export-overlay') as HTMLButtonElement | null;
const ov = getActiveOverlay();
const canExport = !!(ov?.imageUrl && !ov?.isLocal);
exportBtn.disabled = !canExport;
exportBtn.title = canExport ? 'Export active overlay JSON' : 'Export disabled for local images';
if (exportBtn) {
exportBtn.disabled = !canExport;
exportBtn.title = canExport ? 'Export active overlay JSON' : 'Export disabled for local images';
}
}

View file

@ -291,14 +291,26 @@ export function buildRSModal() {
closeBtn: modal.querySelector('#op-rs-close') as HTMLButtonElement,
};
const ctxPrev = refs.preview.getContext('2d', { willReadFrequently: true });
if (!ctxPrev) throw new Error('Failed to get 2d context for preview canvas.');
const ctxSimOrig = refs.simOrig.getContext('2d', { willReadFrequently: true });
if (!ctxSimOrig) throw new Error('Failed to get 2d context for simOrig canvas.');
const ctxSimNew = refs.simNew.getContext('2d', { willReadFrequently: true });
if (!ctxSimNew) throw new Error('Failed to get 2d context for simNew canvas.');
const ctxRes = refs.resCanvas.getContext('2d', { willReadFrequently: true });
if (!ctxRes) throw new Error('Failed to get 2d context for resCanvas.');
const ctxPrev: CanvasRenderingContext2D = (() => {
const ctx = refs.preview.getContext('2d', { willReadFrequently: true });
if (!ctx) throw new Error('Failed to get 2d context for preview canvas.');
return ctx;
})();
const ctxSimOrig: CanvasRenderingContext2D = (() => {
const ctx = refs.simOrig.getContext('2d', { willReadFrequently: true });
if (!ctx) throw new Error('Failed to get 2d context for simOrig canvas.');
return ctx;
})();
const ctxSimNew: CanvasRenderingContext2D = (() => {
const ctx = refs.simNew.getContext('2d', { willReadFrequently: true });
if (!ctx) throw new Error('Failed to get 2d context for simNew canvas.');
return ctx;
})();
const ctxRes: CanvasRenderingContext2D = (() => {
const ctx = refs.resCanvas.getContext('2d', { willReadFrequently: true });
if (!ctx) throw new Error('Failed to get 2d context for resCanvas.');
return ctx;
})();
rs = {
...refs,
@ -325,65 +337,70 @@ export function buildRSModal() {
calcReady: false,
};
const s = rs; // local non-null alias within this scope
function computeSimpleFooterText() {
const W = parseInt(rs!.w.value||'0',10);
const H = parseInt(rs!.h.value||'0',10);
const ok = Number.isFinite(W) && Number.isFinite(H) && W>0 && H>0;
const W = parseInt(s.w.value || '0', 10);
const H = parseInt(s.h.value || '0', 10);
const ok = Number.isFinite(W) && Number.isFinite(H) && W > 0 && H > 0;
const limit = (W >= MAX_OVERLAY_DIM || H >= MAX_OVERLAY_DIM);
return ok ? (limit ? `Target: ${W}×${H} (exceeds limit: must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})`
: `Target: ${W}×${H} (OK)`)
: 'Enter positive width and height.';
}
function sampleDims() {
const cols = Math.floor((rs!.origW - rs!.offx) / rs!.gapX);
const rows = Math.floor((rs!.origH - rs!.offy) / rs!.gapY);
const cols = Math.floor((s.origW - s.offx) / s.gapX);
const rows = Math.floor((s.origH - s.offy) / s.gapY);
return { cols: Math.max(0, cols), rows: Math.max(0, rows) };
}
function computeAdvancedFooterText() {
const { cols, rows } = sampleDims();
const limit = (cols >= MAX_OVERLAY_DIM || rows >= MAX_OVERLAY_DIM);
return (cols>0 && rows>0)
return (cols > 0 && rows > 0)
? `Samples: ${cols} × ${rows} | Output: ${cols}×${rows}${limit ? ` (exceeds limit: < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})` : ''}`
: 'Adjust multiplier/offset until dots sit at centers.';
}
const updateFooterMeta = () => {
rs!.meta.textContent = (rs!.mode === 'advanced') ? computeAdvancedFooterText() : computeSimpleFooterText();
s.meta.textContent = (s.mode === 'advanced') ? computeAdvancedFooterText() : computeSimpleFooterText();
};
function drawSimplePreview() {
if (!rs!.img) return;
const leftLabelH = rs!.colLeft.querySelector('.pad-top')!.clientHeight;
const rightLabelH = rs!.colRight.querySelector('.pad-top')!.clientHeight;
const leftW = rs!.colLeft.clientWidth;
const rightW = rs!.colRight.clientWidth;
const leftH = rs!.colLeft.clientHeight - leftLabelH;
const rightH = rs!.colRight.clientHeight - rightLabelH;
if (!s.img) return;
const padTopL = s.colLeft.querySelector('.pad-top') as HTMLElement | null;
const padTopR = s.colRight.querySelector('.pad-top') as HTMLElement | null;
const leftLabelH = padTopL ? padTopL.clientHeight : 0;
const rightLabelH = padTopR ? padTopR.clientHeight : 0;
const leftW = s.colLeft.clientWidth;
const rightW = s.colRight.clientWidth;
const leftH = s.colLeft.clientHeight - leftLabelH;
const rightH = s.colRight.clientHeight - rightLabelH;
rs!.simOrig.width = leftW; rs!.simOrig.height = leftH;
rs!.simNew.width = rightW; rs!.simNew.height = rightH;
s.simOrig.width = leftW; s.simOrig.height = leftH;
s.simNew.width = rightW; s.simNew.height = rightH;
ctxSimOrig.save();
ctxSimOrig.imageSmoothingEnabled = false;
ctxSimOrig.clearRect(0,0,leftW,leftH);
const sFit = Math.min(leftW / rs!.origW, leftH / rs!.origH);
const dW = Math.max(1, Math.floor(rs!.origW * sFit));
const dH = Math.max(1, Math.floor(rs!.origH * sFit));
const sFit = Math.min(leftW / s.origW, leftH / s.origH);
const dW = Math.max(1, Math.floor(s.origW * sFit));
const dH = Math.max(1, Math.floor(s.origH * sFit));
const dx0 = Math.floor((leftW - dW) / 2);
const dy0 = Math.floor((leftH - dH) / 2);
ctxSimOrig.drawImage(rs!.img!, 0,0, rs!.origW,rs!.origH, dx0,dy0, dW,dH);
ctxSimOrig.drawImage(s.img, 0,0, s.origW,s.origH, dx0,dy0, dW,dH);
ctxSimOrig.restore();
const W = parseInt(rs!.w.value||'0',10);
const H = parseInt(rs!.h.value||'0',10);
const W = parseInt(s.w.value || '0', 10);
const H = parseInt(s.h.value || '0', 10);
ctxSimNew.save();
ctxSimNew.imageSmoothingEnabled = false;
ctxSimNew.clearRect(0,0,rightW,rightH);
if (Number.isFinite(W) && Number.isFinite(H) && W>0 && H>0) {
const tiny = createCanvas(W, H) as any;
const tctx = tiny.getContext('2d', { willReadFrequently: true })!;
const tiny = createCanvas(W, H) as HTMLCanvasElement;
const tctx = tiny.getContext('2d', { willReadFrequently: true });
if (!tctx) throw new Error('Failed to get 2d context for tiny canvas.');
tctx.imageSmoothingEnabled = false;
tctx.clearRect(0,0,W,H);
tctx.drawImage(rs!.img!, 0,0, rs!.origW,rs!.origH, 0,0, W,H);
tctx.drawImage(s.img, 0,0, s.origW,s.origH, 0,0, W,H);
const id = tctx.getImageData(0,0,W,H);
const data = id.data;
for (let i=0;i<data.length;i+=4) { if (data[i+3] !== 0) data[i+3]=255; }
@ -396,63 +413,63 @@ export function buildRSModal() {
const dy2 = Math.floor((rightH - dH2)/2);
ctxSimNew.drawImage(tiny, 0,0, W,H, dx2,dy2, dW2,dH2);
} else {
ctxSimNew.drawImage(rs!.img!, 0,0, rs!.origW,rs!.origH, dx0,dy0, dW,dH);
ctxSimNew.drawImage(s.img, 0,0, s.origW,s.origH, dx0,dy0, dW,dH);
}
ctxSimNew.restore();
}
function syncAdvancedMeta() {
sampleDims();
if (rs!.mode === 'advanced') {
rs!.applyBtn.disabled = !rs!.calcReady;
if (s.mode === 'advanced') {
s.applyBtn.disabled = !s.calcReady;
} else {
const W = parseInt(rs!.w.value||'0',10), H = parseInt(rs!.h.value||'0',10);
const W = parseInt(s.w.value||'0',10), H = parseInt(s.h.value||'0',10);
const ok = Number.isFinite(W)&&Number.isFinite(H)&&W>0&&H>0&&W<MAX_OVERLAY_DIM&&H<MAX_OVERLAY_DIM;
rs!.applyBtn.disabled = !ok;
s.applyBtn.disabled = !ok;
}
updateFooterMeta();
}
function drawAdvancedPreview() {
if (rs!.mode !== 'advanced' || !rs!.img) return;
const w = rs!.origW, h = rs!.origH;
if (s.mode !== 'advanced' || !s.img) return;
const w = s.origW, h = s.origH;
const destW = Math.max(50, Math.floor(rs!.advWrap.clientWidth));
const destH = Math.max(50, Math.floor(rs!.advWrap.clientHeight));
rs!.preview.width = destW;
rs!.preview.height = destH;
const destW = Math.max(50, Math.floor(s.advWrap.clientWidth));
const destH = Math.max(50, Math.floor(s.advWrap.clientHeight));
s.preview.width = destW;
s.preview.height = destH;
const sw = Math.max(1, Math.floor(destW / rs!.zoom));
const sh = Math.max(1, Math.floor(destH / rs!.zoom));
const sw = Math.max(1, Math.floor(destW / s.zoom));
const sh = Math.max(1, Math.floor(destH / s.zoom));
const maxX = Math.max(0, w - sw);
const maxY = Math.max(0, h - sh);
rs!.viewX = Math.min(Math.max(0, rs!.viewX), maxX);
rs!.viewY = Math.min(Math.max(0, rs!.viewY), maxY);
s.viewX = Math.min(Math.max(0, s.viewX), maxX);
s.viewY = Math.min(Math.max(0, s.viewY), maxY);
ctxPrev.save();
ctxPrev.imageSmoothingEnabled = false;
ctxPrev.clearRect(0,0,destW,destH);
ctxPrev.drawImage(rs!.img!, rs!.viewX, rs!.viewY, sw, sh, 0, 0, destW, destH);
ctxPrev.drawImage(s.img, s.viewX, s.viewY, sw, sh, 0, 0, destW, destH);
if (rs!.gridToggle.checked && rs!.gapX >= 1 && rs!.gapY >= 1) {
if (s.gridToggle.checked && s.gapX >= 1 && s.gapY >= 1) {
ctxPrev.strokeStyle = 'rgba(255,59,48,0.45)';
ctxPrev.lineWidth = 1;
const startGX = Math.ceil((rs!.viewX - rs!.offx) / rs!.gapX);
const endGX = Math.floor((rs!.viewX + sw - rs!.offx) / rs!.gapX);
const startGY = Math.ceil((rs!.viewY - rs!.offy) / rs!.gapY);
const endGY = Math.floor((rs!.viewY + sh - rs!.offy) / rs!.gapY);
const startGX = Math.ceil((s.viewX - s.offx) / s.gapX);
const endGX = Math.floor((s.viewX + sw - s.offx) / s.gapX);
const startGY = Math.ceil((s.viewY - s.offy) / s.gapY);
const endGY = Math.floor((s.viewY + sh - s.offy) / s.gapY);
const linesX = Math.max(0, endGX - startGX + 1);
const linesY = Math.max(0, endGY - startGY + 1);
if (linesX <= 4000 && linesY <= 4000) {
ctxPrev.beginPath();
for (let gx = startGX; gx <= endGX; gx++) {
const x = rs!.offx + gx * rs!.gapX;
const px = Math.round((x - rs!.viewX) * rs!.zoom);
const x = s.offx + gx * s.gapX;
const px = Math.round((x - s.viewX) * s.zoom);
ctxPrev.moveTo(px + 0.5, 0);
ctxPrev.lineTo(px + 0.5, destH);
}
for (let gy = startGY; gy <= endGY; gy++) {
const y = rs!.offy + gy * rs!.gapY;
const py = Math.round((y - rs!.viewY) * rs!.zoom);
const y = s.offy + gy * s.gapY;
const py = Math.round((y - s.viewY) * s.zoom);
ctxPrev.moveTo(0, py + 0.5);
ctxPrev.lineTo(destW, py + 0.5);
}
@ -460,26 +477,26 @@ export function buildRSModal() {
}
}
if (rs!.gapX >= 1 && rs!.gapY >= 1) {
if (s.gapX >= 1 && s.gapY >= 1) {
ctxPrev.fillStyle = '#ff3b30';
const cx0 = rs!.offx + Math.floor(rs!.gapX/2);
const cy0 = rs!.offy + Math.floor(rs!.gapY/2);
const cx0 = s.offx + Math.floor(s.gapX/2);
const cy0 = s.offy + Math.floor(s.gapY/2);
if (cx0 >= 0 && cy0 >= 0) {
const startX = Math.ceil((rs!.viewX - cx0) / rs!.gapX);
const startY = Math.ceil((rs!.viewY - cy0) / rs!.gapY);
const endY = Math.floor((rs!.viewY + sh - 1 - cy0) / rs!.gapY);
const endX2 = Math.floor((rs!.viewX + sw - 1 - cx0) / rs!.gapX);
const r = rs!.dotr;
const startX = Math.ceil((s.viewX - cx0) / s.gapX);
const startY = Math.ceil((s.viewY - cy0) / s.gapY);
const endY = Math.floor((s.viewY + sh - 1 - cy0) / s.gapY);
const endX2 = Math.floor((s.viewX + sw - 1 - cx0) / s.gapX);
const r = s.dotr;
const dotsX = Math.max(0, endX2 - startX + 1);
const dotsY = Math.max(0, endY - startY + 1);
const maxDots = 300000;
if (dotsX * dotsY <= maxDots) {
for (let gy = startY; gy <= endY; gy++) {
const y = cy0 + gy * rs!.gapY;
const y = cy0 + gy * s.gapY;
for (let gx = startX; gx <= endX2; gx++) {
const x = cx0 + gx * rs!.gapX;
const px = Math.round((x - rs!.viewX) * rs!.zoom);
const py = Math.round((y - rs!.viewY) * rs!.zoom);
const x = cx0 + gx * s.gapX;
const px = Math.round((x - s.viewX) * s.zoom);
const py = Math.round((y - s.viewY) * s.zoom);
ctxPrev.beginPath();
ctxPrev.arc(px, py, r, 0, Math.PI*2);
ctxPrev.fill();
@ -492,44 +509,44 @@ export function buildRSModal() {
}
function drawAdvancedResultPreview() {
const canvas = rs!.calcCanvas;
const wrap = rs!.resWrap;
const canvas = s.calcCanvas;
const wrap = s.resWrap;
if (!wrap || !canvas) {
ctxRes.clearRect(0,0, rs!.resCanvas.width, rs!.resCanvas.height);
rs!.resMeta.textContent = 'No result. Click Calculate.';
ctxRes.clearRect(0,0, s.resCanvas.width, s.resCanvas.height);
s.resMeta.textContent = 'No result. Click Calculate.';
return;
}
const W = canvas.width, H = canvas.height;
const availW = Math.max(50, Math.floor(wrap.clientWidth - 16));
const availH = Math.max(50, Math.floor(wrap.clientHeight - 16));
const s = Math.min(availW / W, availH / H);
const dW = Math.max(1, Math.floor(W * s));
const dH = Math.max(1, Math.floor(H * s));
rs!.resCanvas.width = dW;
rs!.resCanvas.height = dH;
const scale = Math.min(availW / W, availH / H);
const dW = Math.max(1, Math.floor(W * scale));
const dH = Math.max(1, Math.floor(H * scale));
s.resCanvas.width = dW;
s.resCanvas.height = dH;
ctxRes.save();
ctxRes.imageSmoothingEnabled = false;
ctxRes.clearRect(0,0,dW,dH);
ctxRes.drawImage(canvas, 0,0, W,H, 0,0, dW,dH);
ctxRes.restore();
rs!.resMeta.textContent = `Output: ${W}×${H}${(W>=MAX_OVERLAY_DIM||H>=MAX_OVERLAY_DIM) ? ` (exceeds limit: < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})` : ''}`;
s.resMeta.textContent = `Output: ${W}×${H}${(W>=MAX_OVERLAY_DIM||H>=MAX_OVERLAY_DIM) ? ` (exceeds limit: < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})` : ''}`;
}
rs._drawSimplePreview = drawSimplePreview;
rs._drawAdvancedPreview = drawAdvancedPreview;
rs._drawAdvancedResultPreview = drawAdvancedResultPreview;
s._drawSimplePreview = drawSimplePreview;
s._drawAdvancedPreview = drawAdvancedPreview;
s._drawAdvancedResultPreview = drawAdvancedResultPreview;
const setMode = (m: 'simple'|'advanced') => {
rs!.mode = m;
rs!.tabSimple.classList.toggle('active', m === 'simple');
rs!.tabAdvanced.classList.toggle('active', m === 'advanced');
rs!.paneSimple.classList.toggle('show', m === 'simple');
rs!.paneAdvanced.classList.toggle('show', m === 'advanced');
s.mode = m;
s.tabSimple.classList.toggle('active', m === 'simple');
s.tabAdvanced.classList.toggle('active', m === 'advanced');
s.paneSimple.classList.toggle('show', m === 'simple');
s.paneAdvanced.classList.toggle('show', m === 'advanced');
updateFooterMeta();
rs!.calcBtn.style.display = (m === 'advanced') ? 'inline-block' : 'none';
s.calcBtn.style.display = (m === 'advanced') ? 'inline-block' : 'none';
if (m === 'advanced') {
rs!.applyBtn.disabled = !rs!.calcReady;
s.applyBtn.disabled = !s.calcReady;
} else {
syncSimpleNote();
}
@ -542,144 +559,144 @@ export function buildRSModal() {
drawSimplePreview();
}
};
rs._setMode = (m) => {
s._setMode = (m) => {
const evt = new Event('click');
(m === 'simple' ? rs!.tabSimple : rs!.tabAdvanced).dispatchEvent(evt);
(m === 'simple' ? s.tabSimple : s.tabAdvanced).dispatchEvent(evt);
};
rs.tabSimple.addEventListener('click', () => setMode('simple'));
rs.tabAdvanced.addEventListener('click', () => setMode('advanced'));
s.tabSimple.addEventListener('click', () => setMode('simple'));
s.tabAdvanced.addEventListener('click', () => setMode('advanced'));
function onWidthInput() {
if (rs!.updating) return;
rs!.updating = true;
const W = parseInt(rs!.w.value||'0',10);
if (rs!.lock.checked && rs!.origW>0 && rs!.origH>0 && W>0) {
rs!.h.value = String(Math.max(1, Math.round(W * rs!.origH / rs!.origW)));
if (s.updating) return;
s.updating = true;
const W = parseInt(s.w.value||'0',10);
if (s.lock.checked && s.origW>0 && s.origH>0 && W>0) {
s.h.value = String(Math.max(1, Math.round(W * s.origH / s.origW)));
}
rs!.updating = false;
s.updating = false;
syncSimpleNote();
if (rs!.mode === 'simple') drawSimplePreview();
if (s.mode === 'simple') drawSimplePreview();
}
function onHeightInput() {
if (rs!.updating) return;
rs!.updating = true;
const H = parseInt(rs!.h.value||'0',10);
if (rs!.lock.checked && rs!.origW>0 && rs!.origH>0 && H>0) {
rs!.w.value = String(Math.max(1, Math.round(H * rs!.origW / rs!.origH)));
if (s.updating) return;
s.updating = true;
const H = parseInt(s.h.value||'0',10);
if (s.lock.checked && s.origW>0 && s.origH>0 && H>0) {
s.w.value = String(Math.max(1, Math.round(H * s.origW / s.origH)));
}
rs!.updating = false;
s.updating = false;
syncSimpleNote();
if (rs!.mode === 'simple') drawSimplePreview();
if (s.mode === 'simple') drawSimplePreview();
}
rs.w.addEventListener('input', onWidthInput);
rs.h.addEventListener('input', onHeightInput);
rs.onex.addEventListener('click', () => { applyScaleToFields(1); drawSimplePreview(); });
rs.half.addEventListener('click', () => { applyScaleToFields(0.5); drawSimplePreview(); });
rs.third.addEventListener('click', () => { applyScaleToFields(1/3); drawSimplePreview(); });
rs.quarter.addEventListener('click', () => { applyScaleToFields(1/4); drawSimplePreview(); });
rs.double.addEventListener('click', () => { applyScaleToFields(2); drawSimplePreview(); });
rs.applyScale.addEventListener('click', () => {
const s = parseFloat(rs!.scale.value||'');
if (!Number.isFinite(s) || s<=0) { showToast('Enter a valid scale factor > 0'); return; }
applyScaleToFields(s);
s.w.addEventListener('input', onWidthInput);
s.h.addEventListener('input', onHeightInput);
s.onex.addEventListener('click', () => { applyScaleToFields(1); drawSimplePreview(); });
s.half.addEventListener('click', () => { applyScaleToFields(0.5); drawSimplePreview(); });
s.third.addEventListener('click', () => { applyScaleToFields(1/3); drawSimplePreview(); });
s.quarter.addEventListener('click', () => { applyScaleToFields(1/4); drawSimplePreview(); });
s.double.addEventListener('click', () => { applyScaleToFields(2); drawSimplePreview(); });
s.applyScale.addEventListener('click', () => {
const scaleVal = parseFloat(s.scale.value||'');
if (!Number.isFinite(scaleVal) || scaleVal<=0) { showToast('Enter a valid scale factor > 0'); return; }
applyScaleToFields(scaleVal);
drawSimplePreview();
});
const markCalcStale = () => {
if (rs!.mode === 'advanced') {
rs!.calcReady = false;
rs!.applyBtn.disabled = true;
if (s.mode === 'advanced') {
s.calcReady = false;
s.applyBtn.disabled = true;
drawAdvancedResultPreview();
updateFooterMeta();
}
};
const onMultChange = (v: string) => {
if (rs!.updating) return;
if (s.updating) return;
const parsed = parseFloat(v);
if (!Number.isFinite(parsed)) return;
const clamped = Math.min(Math.max(parsed, 1), 128);
rs!.mult = clamped;
if (rs!.bind.checked) { rs!.gapX = clamped; rs!.gapY = clamped; }
s.mult = clamped;
if (s.bind.checked) { s.gapX = clamped; s.gapY = clamped; }
syncAdvFieldsToState();
syncAdvancedMeta();
drawAdvancedPreview();
markCalcStale();
};
rs.multRange.addEventListener('input', (e) => onMultChange((e.target as HTMLInputElement).value));
rs.multInput.addEventListener('input', (e) => {
s.multRange.addEventListener('input', (e) => onMultChange((e.target as HTMLInputElement).value));
s.multInput.addEventListener('input', (e) => {
const v = (e.target as HTMLInputElement).value;
if (!Number.isFinite(parseFloat(v))) return;
onMultChange(v);
});
rs.bind.addEventListener('change', () => {
if (rs!.bind.checked) { rs!.gapX = rs!.mult; rs!.gapY = rs!.mult; syncAdvFieldsToState(); }
s.bind.addEventListener('change', () => {
if (s.bind.checked) { s.gapX = s.mult; s.gapY = s.mult; syncAdvFieldsToState(); }
syncAdvancedMeta();
drawAdvancedPreview();
markCalcStale();
});
rs.blockW.addEventListener('input', (e) => {
s.blockW.addEventListener('input', (e) => {
const val = parseFloat((e.target as HTMLInputElement).value);
if (!Number.isFinite(val)) return;
rs!.gapX = Math.min(Math.max(val, 1), 4096);
if (rs!.bind.checked) { rs!.mult = rs!.gapX; rs!.gapY = rs!.gapX; }
s.gapX = Math.min(Math.max(val, 1), 4096);
if (s.bind.checked) { s.mult = s.gapX; s.gapY = s.gapX; }
syncAdvFieldsToState();
syncAdvancedMeta();
drawAdvancedPreview();
markCalcStale();
});
rs.blockH.addEventListener('input', (e) => {
s.blockH.addEventListener('input', (e) => {
const val = parseFloat((e.target as HTMLInputElement).value);
if (!Number.isFinite(val)) return;
rs!.gapY = Math.min(Math.max(val, 1), 4096);
if (rs!.bind.checked) { rs!.mult = rs!.gapY; rs!.gapX = rs!.gapY; }
s.gapY = Math.min(Math.max(val, 1), 4096);
if (s.bind.checked) { s.mult = s.gapY; s.gapX = s.gapY; }
syncAdvFieldsToState();
syncAdvancedMeta();
drawAdvancedPreview();
markCalcStale();
});
rs.offX.addEventListener('input', (e) => {
s.offX.addEventListener('input', (e) => {
const val = parseFloat((e.target as HTMLInputElement).value);
if (!Number.isFinite(val)) return;
rs!.offx = Math.min(Math.max(val, 0), Math.max(0, rs!.origH-0.0001));
rs!.viewX = Math.min(rs!.viewX, Math.max(0, rs!.origW - 1));
s.offx = Math.min(Math.max(val, 0), Math.max(0, s.origH-0.0001));
s.viewX = Math.min(s.viewX, Math.max(0, s.origW - 1));
syncAdvancedMeta();
drawAdvancedPreview();
markCalcStale();
});
rs.offY.addEventListener('input', (e) => {
s.offY.addEventListener('input', (e) => {
const val = parseFloat((e.target as HTMLInputElement).value);
if (!Number.isFinite(val)) return;
rs!.offy = Math.min(Math.max(val, 0), Math.max(0, rs!.origH-0.0001));
rs!.viewY = Math.min(rs!.viewY, Math.max(0, rs!.origH - 1));
s.offy = Math.min(Math.max(val, 0), Math.max(0, s.origH-0.0001));
s.viewY = Math.min(s.viewY, Math.max(0, s.origH - 1));
syncAdvancedMeta();
drawAdvancedPreview();
markCalcStale();
});
rs.dotR.addEventListener('input', (e) => {
rs!.dotr = Math.max(1, Math.round(Number((e.target as HTMLInputElement).value)||1));
rs!.dotRVal.textContent = String(rs!.dotr);
s.dotR.addEventListener('input', (e) => {
s.dotr = Math.max(1, Math.round(Number((e.target as HTMLInputElement).value)||1));
s.dotRVal.textContent = String(s.dotr);
drawAdvancedPreview();
});
rs.gridToggle.addEventListener('change', drawAdvancedPreview);
s.gridToggle.addEventListener('change', drawAdvancedPreview);
function applyZoom(factor: number) {
const destW = Math.max(50, Math.floor(rs!.advWrap.clientWidth));
const destH = Math.max(50, Math.floor(rs!.advWrap.clientHeight));
const sw = Math.max(1, Math.floor(destW / rs!.zoom));
const sh = Math.max(1, Math.floor(destH / rs!.zoom));
const cx = rs!.viewX + sw / 2;
const cy = rs!.viewY + sh / 2;
rs!.zoom = Math.min(32, Math.max(0.1, rs!.zoom * factor));
const sw2 = Math.max(1, Math.floor(destW / rs!.zoom));
const sh2 = Math.max(1, Math.floor(destH / rs!.zoom));
rs!.viewX = Math.min(Math.max(0, Math.round(cx - sw2 / 2)), Math.max(0, rs!.origW - sw2));
rs!.viewY = Math.min(Math.max(0, Math.round(cy - sh2 / 2)), Math.max(0, rs!.origH - sh2));
const destW = Math.max(50, Math.floor(s.advWrap.clientWidth));
const destH = Math.max(50, Math.floor(s.advWrap.clientHeight));
const sw = Math.max(1, Math.floor(destW / s.zoom));
const sh = Math.max(1, Math.floor(destH / s.zoom));
const cx = s.viewX + sw / 2;
const cy = s.viewY + sh / 2;
s.zoom = Math.min(32, Math.max(0.1, s.zoom * factor));
const sw2 = Math.max(1, Math.floor(destW / s.zoom));
const sh2 = Math.max(1, Math.floor(destH / s.zoom));
s.viewX = Math.min(Math.max(0, Math.round(cx - sw2 / 2)), Math.max(0, s.origW - sw2));
s.viewY = Math.min(Math.max(0, Math.round(cy - sh2 / 2)), Math.max(0, s.origH - sh2));
drawAdvancedPreview();
}
rs.zoomIn.addEventListener('click', () => applyZoom(1.25));
rs.zoomOut.addEventListener('click', () => applyZoom(1/1.25));
rs.advWrap.addEventListener('wheel', (e) => {
s.zoomIn.addEventListener('click', () => applyZoom(1.25));
s.zoomOut.addEventListener('click', () => applyZoom(1/1.25));
s.advWrap.addEventListener('wheel', (e) => {
if (!(e as WheelEvent).ctrlKey) return;
e.preventDefault();
const delta = (e as WheelEvent).deltaY || 0;
@ -688,59 +705,59 @@ export function buildRSModal() {
const onPanDown = (e: PointerEvent) => {
if ((e.target as HTMLElement).closest('.op-rs-zoom')) return;
rs!.panning = true;
rs!.panStart = { x: e.clientX, y: e.clientY, viewX: rs!.viewX, viewY: rs!.viewY };
rs!.advWrap.classList.remove('op-pan-grab');
rs!.advWrap.classList.add('op-pan-grabbing');
(rs!.advWrap as any).setPointerCapture?.(e.pointerId);
s.panning = true;
s.panStart = { x: e.clientX, y: e.clientY, viewX: s.viewX, viewY: s.viewY };
s.advWrap.classList.remove('op-pan-grab');
s.advWrap.classList.add('op-pan-grabbing');
(s.advWrap as any).setPointerCapture?.(e.pointerId);
};
const onPanMove = (e: PointerEvent) => {
if (!rs!.panning) return;
const dx = e.clientX - rs!.panStart!.x;
const dy = e.clientY - rs!.panStart!.y;
const wrapW = rs!.advWrap.clientWidth;
const wrapH = rs!.advWrap.clientHeight;
const sw = Math.max(1, Math.floor(wrapW / rs!.zoom));
const sh = Math.max(1, Math.floor(wrapH / rs!.zoom));
let nx = rs!.panStart!.viewX - Math.round(dx / rs!.zoom);
let ny = rs!.panStart!.viewY - Math.round(dy / rs!.zoom);
nx = Math.min(Math.max(0, nx), Math.max(0, rs!.origW - sw));
ny = Math.min(Math.max(0, ny), Math.max(0, rs!.origH - sh));
rs!.viewX = nx;
rs!.viewY = ny;
if (!s.panning || !s.panStart) return;
const dx = e.clientX - s.panStart.x;
const dy = e.clientY - s.panStart.y;
const wrapW = s.advWrap.clientWidth;
const wrapH = s.advWrap.clientHeight;
const sw = Math.max(1, Math.floor(wrapW / s.zoom));
const sh = Math.max(1, Math.floor(wrapH / s.zoom));
let nx = s.panStart.viewX - Math.round(dx / s.zoom);
let ny = s.panStart.viewY - Math.round(dy / s.zoom);
nx = Math.min(Math.max(0, nx), Math.max(0, s.origW - sw));
ny = Math.min(Math.max(0, ny), Math.max(0, s.origH - sh));
s.viewX = nx;
s.viewY = ny;
drawAdvancedPreview();
};
const onPanUp = (e: PointerEvent) => {
if (!rs!.panning) return;
rs!.panning = false;
rs!.panStart = null;
rs!.advWrap.classList.remove('op-pan-grabbing');
rs!.advWrap.classList.add('op-pan-grab');
(rs!.advWrap as any).releasePointerCapture?.(e.pointerId);
if (!s.panning) return;
s.panning = false;
s.panStart = null;
s.advWrap.classList.remove('op-pan-grabbing');
s.advWrap.classList.add('op-pan-grab');
(s.advWrap as any).releasePointerCapture?.(e.pointerId);
};
rs.advWrap.addEventListener('pointerdown', onPanDown);
rs.advWrap.addEventListener('pointermove', onPanMove);
rs.advWrap.addEventListener('pointerup', onPanUp);
rs.advWrap.addEventListener('pointercancel', onPanUp);
rs.advWrap.addEventListener('pointerleave', onPanUp);
s.advWrap.addEventListener('pointerdown', onPanDown);
s.advWrap.addEventListener('pointermove', onPanMove);
s.advWrap.addEventListener('pointerup', onPanUp);
s.advWrap.addEventListener('pointercancel', onPanUp);
s.advWrap.addEventListener('pointerleave', onPanUp);
const close = () => closeRSModal();
rs.cancelBtn.addEventListener('click', close);
rs.closeBtn.addEventListener('click', close);
s.cancelBtn.addEventListener('click', close);
s.closeBtn.addEventListener('click', close);
backdrop.addEventListener('click', close);
rs.calcBtn.addEventListener('click', async () => {
if (rs!.mode !== 'advanced' || !rs!.img) return;
s.calcBtn.addEventListener('click', async () => {
if (s.mode !== 'advanced' || !s.img) return;
try {
const { cols, rows } = sampleDims();
if (cols<=0 || rows<=0) { showToast('No samples. Adjust multiplier/offset.'); return; }
if (cols >= MAX_OVERLAY_DIM || rows >= MAX_OVERLAY_DIM) { showToast(`Output too large. Must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM}.`); return; }
const canvas = await reconstructViaGrid(rs!.img, rs!.origW, rs!.origH, rs!.offx, rs!.offy, rs!.gapX, rs!.gapY);
rs!.calcCanvas = canvas;
rs!.calcCols = cols;
rs!.calcRows = rows;
rs!.calcReady = true;
rs!.applyBtn.disabled = false;
const canvas = await reconstructViaGrid(s.img, s.origW, s.origH, s.offx, s.offy, s.gapX, s.gapY);
s.calcCanvas = canvas;
s.calcCols = cols;
s.calcRows = rows;
s.calcReady = true;
s.applyBtn.disabled = false;
drawAdvancedResultPreview();
updateFooterMeta();
showToast(`Calculated ${cols}×${rows}. Review preview, then Apply.`);
@ -750,29 +767,29 @@ export function buildRSModal() {
}
});
rs.applyBtn.addEventListener('click', async () => {
if (!rs!.ov) return;
s.applyBtn.addEventListener('click', async () => {
if (!s.ov) return;
try {
if (rs!.mode === 'simple') {
const W = parseInt(rs!.w.value||'0',10);
const H = parseInt(rs!.h.value||'0',10);
if (s.mode === 'simple') {
const W = parseInt(s.w.value||'0',10);
const H = parseInt(s.h.value||'0',10);
if (!Number.isFinite(W) || !Number.isFinite(H) || W<=0 || H<=0) { showToast('Invalid dimensions'); return; }
if (W >= MAX_OVERLAY_DIM || H >= MAX_OVERLAY_DIM) { showToast(`Too large. Must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM}.`); return; }
await resizeOverlayImage(rs!.ov, W, H);
await resizeOverlayImage(s.ov, W, H);
closeRSModal();
showToast(`Resized to ${W}×${H}.`);
} else {
if (!rs!.calcReady || !rs!.calcCanvas) { showToast('Calculate first.'); return; }
const dataUrl = await canvasToDataURLSafe(rs!.calcCanvas);
rs!.ov.imageBase64 = dataUrl;
rs!.ov.imageUrl = null;
rs!.ov.isLocal = true;
if (!s.calcReady || !s.calcCanvas) { showToast('Calculate first.'); return; }
const dataUrl = await canvasToDataURLSafe(s.calcCanvas);
s.ov.imageBase64 = dataUrl;
s.ov.imageUrl = null;
s.ov.isLocal = true;
await saveConfig(['overlays']);
clearOverlayCache();
ensureHook();
emitOverlayChanged();
closeRSModal();
showToast(`Applied ${rs!.calcCols}×${rs!.calcRows}.`);
showToast(`Applied ${s.calcCols}×${s.calcRows}.`);
}
} catch (e) {
console.error(e);
@ -781,51 +798,52 @@ export function buildRSModal() {
});
function syncSimpleNote() {
const W = parseInt(rs!.w.value||'0',10);
const H = parseInt(rs!.h.value||'0',10);
const W = parseInt(s.w.value||'0',10);
const H = parseInt(s.h.value||'0',10);
const ok = Number.isFinite(W) && Number.isFinite(H) && W>0 && H>0;
const limit = (W >= MAX_OVERLAY_DIM || H >= MAX_OVERLAY_DIM);
const simpleText = ok
? (limit ? `Target: ${W}×${H} (exceeds limit: must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})`
: `Target: ${W}×${H} (OK)`)
: 'Enter positive width and height.';
if (rs!.note) rs!.note.textContent = simpleText;
if (rs!.mode === 'simple') rs!.applyBtn.disabled = (!ok || limit);
if (rs!.mode === 'simple') rs!.meta.textContent = simpleText;
if (s.note) s.note.textContent = simpleText;
if (s.mode === 'simple') s.applyBtn.disabled = (!ok || limit);
if (s.mode === 'simple') s.meta.textContent = simpleText;
}
function applyScaleToFields(scale: number) {
const W = Math.max(1, Math.round(rs!.origW * scale));
const H = Math.max(1, Math.round(rs!.origH * scale));
rs!.updating = true;
rs!.w.value = String(W);
rs!.h.value = rs!.lock.checked ? String(Math.max(1, Math.round(W * rs!.origH / rs!.origW))) : String(H);
rs!.updating = false;
const W = Math.max(1, Math.round(s.origW * scale));
const H = Math.max(1, Math.round(s.origH * scale));
s.updating = true;
s.w.value = String(W);
s.h.value = s.lock.checked ? String(Math.max(1, Math.round(W * s.origH / s.origW))) : String(H);
s.updating = false;
syncSimpleNote();
}
function syncAdvFieldsToState() {
rs!.updating = true;
rs!.multRange.value = String(rs!.mult);
rs!.multInput.value = String(rs!.mult);
rs!.blockW.value = String(rs!.gapX);
rs!.blockH.value = String(rs!.gapY);
rs!.offX.value = String(rs!.offx);
rs!.offY.value = String(rs!.offy);
rs!.dotR.value = String(rs!.dotr);
rs!.dotRVal.textContent = String(rs!.dotr);
rs!.updating = false;
s.updating = true;
s.multRange.value = String(s.mult);
s.multInput.value = String(s.mult);
s.blockW.value = String(s.gapX);
s.blockH.value = String(s.gapY);
s.offX.value = String(s.offx);
s.offY.value = String(s.offy);
s.dotR.value = String(s.dotr);
s.dotRVal.textContent = String(s.dotr);
s.updating = false;
}
rs._syncAdvancedMeta = syncAdvancedMeta;
rs._syncSimpleNote = syncSimpleNote;
s._syncAdvancedMeta = syncAdvancedMeta;
s._syncSimpleNote = syncSimpleNote;
rs._resizeHandler = () => {
if (rs!.mode === 'simple') rs!._drawSimplePreview?.();
s._resizeHandler = () => {
if (!rs) return;
if (rs.mode === 'simple') rs._drawSimplePreview?.();
else {
rs!._drawAdvancedPreview?.();
rs!._drawAdvancedResultPreview?.();
rs._drawAdvancedPreview?.();
rs._drawAdvancedResultPreview?.();
}
};
window.addEventListener('resize', rs._resizeHandler);
window.addEventListener('resize', s._resizeHandler);
}
export function openRSModal(overlay: any) {
@ -834,60 +852,64 @@ export function openRSModal(overlay: any) {
const img = new Image();
img.onload = () => {
rs!.img = img;
rs!.origW = img.width; rs!.origH = img.height;
const s = rs;
if (!s) return;
rs!.orig.value = `${rs!.origW}×${rs!.origH}`;
rs!.w.value = String(rs!.origW);
rs!.h.value = String(rs!.origH);
rs!.lock.checked = true;
s.img = img;
s.origW = img.width; s.origH = img.height;
rs!.zoom = 1.0;
rs!.mult = 4;
rs!.gapX = 4; rs!.gapY = 4;
rs!.offx = 0; rs!.offy = 0;
rs!.dotr = 1;
rs!.viewX = 0; rs!.viewY = 0;
s.orig.value = `${s.origW}×${s.origH}`;
s.w.value = String(s.origW);
s.h.value = String(s.origH);
s.lock.checked = true;
rs!.bind.checked = true;
rs!.multRange.value = '4';
rs!.multInput.value = '4';
rs!.blockW.value = '4';
rs!.blockH.value = '4';
rs!.offX.value = '0';
rs!.offY.value = '0';
rs!.dotR.value = '1';
rs!.dotRVal.textContent = '1';
rs!.gridToggle.checked = true;
s.zoom = 1.0;
s.mult = 4;
s.gapX = 4; s.gapY = 4;
s.offx = 0; s.offy = 0;
s.dotr = 1;
s.viewX = 0; s.viewY = 0;
rs!.calcCanvas = null;
rs!.calcCols = 0;
rs!.calcRows = 0;
rs!.calcReady = false;
rs!.applyBtn.disabled = (rs!.mode === 'advanced');
s.bind.checked = true;
s.multRange.value = '4';
s.multInput.value = '4';
s.blockW.value = '4';
s.blockH.value = '4';
s.offX.value = '0';
s.offY.value = '0';
s.dotR.value = '1';
s.dotRVal.textContent = '1';
s.gridToggle.checked = true;
rs!._setMode!('simple');
s.calcCanvas = null;
s.calcCols = 0;
s.calcRows = 0;
s.calcReady = false;
s.applyBtn.disabled = (s.mode === 'advanced');
if (s._setMode) s._setMode('simple');
document.body.classList.add('op-scroll-lock');
rs!.backdrop.classList.add('show');
rs!.modal.style.display = 'flex';
s.backdrop.classList.add('show');
s.modal.style.display = 'flex';
rs!._drawSimplePreview?.();
rs!._drawAdvancedPreview?.();
rs!._drawAdvancedResultPreview?.();
rs!._syncAdvancedMeta?.();
rs!._syncSimpleNote?.();
s._drawSimplePreview?.();
s._drawAdvancedPreview?.();
s._drawAdvancedResultPreview?.();
s._syncAdvancedMeta?.();
s._syncSimpleNote?.();
const setFooterNow = () => {
if (rs!.mode === 'advanced') {
const cols = Math.floor((rs!.origW - rs!.offx) / rs!.gapX);
const rows = Math.floor((rs!.origH - rs!.offy) / rs!.gapY);
rs!.meta.textContent = (cols>0&&rows>0) ? `Samples: ${cols} × ${rows} | Output: ${cols}×${rows}${(cols>=MAX_OVERLAY_DIM||rows>=MAX_OVERLAY_DIM)?` (exceeds limit: < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})`:''}` : 'Adjust multiplier/offset until dots sit at centers.';
if (!rs) return;
if (rs.mode === 'advanced') {
const cols = Math.floor((rs.origW - rs.offx) / rs.gapX);
const rows = Math.floor((rs.origH - rs.offy) / rs.gapY);
rs.meta.textContent = (cols>0&&rows>0) ? `Samples: ${cols} × ${rows} | Output: ${cols}×${rows}${(cols>=MAX_OVERLAY_DIM||rows>=MAX_OVERLAY_DIM)?` (exceeds limit: < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})`:''}` : 'Adjust multiplier/offset until dots sit at centers.';
} else {
const W = parseInt(rs!.w.value||'0',10); const H = parseInt(rs!.h.value||'0',10);
const W = parseInt(rs.w.value||'0',10); const H = parseInt(rs.h.value||'0',10);
const ok = Number.isFinite(W)&&Number.isFinite(H)&&W>0&&H>0;
const limit = (W>=MAX_OVERLAY_DIM||H>=MAX_OVERLAY_DIM);
rs!.meta.textContent = ok ? (limit ? `Target: ${W}×${H} (exceeds limit: must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})` : `Target: ${W}×${H} (OK)`) : 'Enter positive width and height.';
rs.meta.textContent = ok ? (limit ? `Target: ${W}×${H} (exceeds limit: must be < ${MAX_OVERLAY_DIM}×${MAX_OVERLAY_DIM})` : `Target: ${W}×${H} (OK)`) : 'Enter positive width and height.';
}
};
setFooterNow();
@ -906,8 +928,9 @@ function closeRSModal() {
}
async function reconstructViaGrid(img: HTMLImageElement, origW: number, origH: number, offx: number, offy: number, gapX: number, gapY: number) {
const srcCanvas = createCanvas(origW, origH) as any;
const sctx = srcCanvas.getContext('2d', { willReadFrequently: true })!;
const srcCanvas = createCanvas(origW, origH) as HTMLCanvasElement;
const sctx = srcCanvas.getContext('2d', { willReadFrequently: true });
if (!sctx) throw new Error('Failed to get 2d context for srcCanvas.');
sctx.imageSmoothingEnabled = false;
sctx.drawImage(img, 0, 0);
const srcData = sctx.getImageData(0,0,origW,origH).data;
@ -917,7 +940,8 @@ async function reconstructViaGrid(img: HTMLImageElement, origW: number, origH: n
if (cols <= 0 || rows <= 0) throw new Error('No samples available with current offset/gap');
const outCanvas = createHTMLCanvas(cols, rows);
const octx = outCanvas.getContext('2d')!;
const octx = outCanvas.getContext('2d');
if (!octx) throw new Error('Failed to get 2d context for outCanvas.');
const out = octx.createImageData(cols, rows);
const odata = out.data;
@ -951,7 +975,8 @@ async function reconstructViaGrid(img: HTMLImageElement, origW: number, origH: n
async function resizeOverlayImage(ov: any, targetW: number, targetH: number) {
const img = await loadImage(ov.imageBase64);
const canvas = createHTMLCanvas(targetW, targetH);
const ctx = canvas.getContext('2d', { willReadFrequently: true })!;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (!ctx) throw new Error('Failed to get 2d context for resize canvas.');
ctx.imageSmoothingEnabled = false;
ctx.clearRect(0,0,targetW,targetH);
ctx.drawImage(img, 0,0, img.width,img.height, 0,0, targetW,targetH);