/** * reference.js — Выбор референса, канвас и выделение области */ import { getState, setReference, clearReferenceState } from "./state.js"; import { isHeicFile, setJobStatus, MAX_FILE_SIZE } from "./ui.js"; const REFERENCE_MAX_DIMENSION = 1600; let referenceState = null; let regionDragStart = null; export function getReferenceState() { return referenceState; } export function clearReference() { if (referenceState?.source?.type === "file" && referenceState.source.objectUrl) { URL.revokeObjectURL(referenceState.source.objectUrl); } referenceState = null; clearReferenceState(); const preview = document.querySelector("#reference-preview"); const canvas = document.querySelector("#reference-canvas"); const note = document.querySelector("#reference-note"); const regionInfo = document.querySelector("#reference-region-info"); const historySelect = document.querySelector("#reference-history-select"); if (preview) preview.hidden = true; if (canvas) { canvas.width = 0; canvas.height = 0; } if (note) note.hidden = true; if (regionInfo) regionInfo.textContent = "Область не выбрана — потяните по превью"; if (historySelect) historySelect.value = ""; } function drawRegion(ctx) { const region = referenceState?.region; if (!region) return; ctx.strokeStyle = "rgba(220,38,38,0.95)"; ctx.lineWidth = 3; ctx.fillStyle = "rgba(220,38,38,0.18)"; ctx.fillRect(region.x, region.y, region.w, region.h); ctx.strokeRect(region.x, region.y, region.w, region.h); } function drawReferenceToCanvas(image) { const canvas = document.querySelector("#reference-canvas"); if (!canvas) return; const naturalWidth = image.naturalWidth || image.width; const naturalHeight = image.naturalHeight || image.height; if (!naturalWidth || !naturalHeight) return; const scale = Math.min( 1, REFERENCE_MAX_DIMENSION / Math.max(naturalWidth, naturalHeight) ); canvas.width = Math.round(naturalWidth * scale); canvas.height = Math.round(naturalHeight * scale); const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(image, 0, 0, canvas.width, canvas.height); drawRegion(ctx); } function redrawReferenceCanvas() { const canvas = document.querySelector("#reference-canvas"); const image = referenceState?.source?.image; if (!canvas || !image || !canvas.width) return; const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(image, 0, 0, canvas.width, canvas.height); drawRegion(ctx); updateReferenceNote(); } function updateReferenceNote() { const note = document.querySelector("#reference-note"); const regionInfo = document.querySelector("#reference-region-info"); if (!referenceState) { if (note) note.hidden = true; if (regionInfo) regionInfo.textContent = "Область не выбрана — потяните по превью"; return; } if (referenceState.region) { const r = referenceState.region; if (note) note.hidden = true; if (regionInfo) { regionInfo.textContent = `Область: ${Math.round(r.x)},${Math.round(r.y)} ${Math.round(r.w)}×${Math.round(r.h)}`; } } else { if (note) { note.textContent = "Область не выбрана — Codex использует референс как ориентир"; note.hidden = false; } if (regionInfo) regionInfo.textContent = "Область не выбрана — потяните по превью"; } } function canvasPointFromEvent(canvas, event) { const rect = canvas.getBoundingClientRect(); const x = (event.clientX - rect.left) * (canvas.width / rect.width); const y = (event.clientY - rect.top) * (canvas.height / rect.height); return { x: Math.min(Math.max(0, x), canvas.width), y: Math.min(Math.max(0, y), canvas.height), }; } export function loadReferenceFromFile(file) { if (!file) return; const note = document.querySelector("#reference-note"); const preview = document.querySelector("#reference-preview"); const fileType = String(file.type || "").toLowerCase(); if (!["image/jpeg", "image/jpg", "image/png", "image/webp"].includes(fileType)) { if (isHeicFile(file)) { if (note) { note.textContent = "HEIC не поддерживается как референс — выберите JPEG, PNG или WebP"; note.hidden = false; } } else { setJobStatus("error", "Референс должен быть JPEG, PNG или WebP."); } return; } if (file.size > MAX_FILE_SIZE) { setJobStatus("error", "Файл референса слишком большой. Максимальный размер — 25 МБ."); return; } if (referenceState?.source?.type === "file" && referenceState.source.objectUrl) { URL.revokeObjectURL(referenceState.source.objectUrl); } const objectUrl = URL.createObjectURL(file); referenceState = { source: { type: "file", file, objectUrl }, region: null }; const image = new Image(); image.onload = () => { referenceState.source.image = image; drawReferenceToCanvas(image); if (preview) preview.hidden = false; updateReferenceNote(); setReference({ file, versionId: null, image, region: null }); }; image.onerror = () => { URL.revokeObjectURL(objectUrl); referenceState = null; clearReferenceState(); setJobStatus("error", "Не удалось прочитать изображение референса."); }; image.src = objectUrl; } export function loadReferenceFromHistory(version) { if (!version) return; if (referenceState?.source?.type === "file" && referenceState.source.objectUrl) { URL.revokeObjectURL(referenceState.source.objectUrl); } const preview = document.querySelector("#reference-preview"); referenceState = { source: { type: "history", versionId: version.id, url: version.url }, region: null, }; const image = new Image(); image.onload = () => { referenceState.source.image = image; drawReferenceToCanvas(image); if (preview) preview.hidden = false; updateReferenceNote(); setReference({ file: null, versionId: version.id, image, region: null }); }; image.onerror = () => { referenceState = null; clearReferenceState(); setJobStatus("error", "Не удалось загрузить референс из истории."); }; image.src = version.url; } export function rebuildReferenceSelect() { const historySelect = document.querySelector("#reference-history-select"); if (!historySelect) return; const currentVal = historySelect.value; historySelect.innerHTML = ''; const state = getState(); for (const v of state.historyVersions) { const opt = document.createElement("option"); opt.value = v.id; opt.textContent = `${v.label} (${v.id.slice(0, 8)})`; historySelect.appendChild(opt); } if (currentVal && state.historyVersions.some((v) => v.id === currentVal)) { historySelect.value = currentVal; } } export function initReference() { const uploadBtn = document.querySelector("#reference-upload-button"); const fileInput = document.querySelector("#reference-file-input"); const clearBtn = document.querySelector("#reference-clear-button"); const historySelect = document.querySelector("#reference-history-select"); const canvas = document.querySelector("#reference-canvas"); const regionReset = document.querySelector("#reference-region-reset"); if (uploadBtn && fileInput) { uploadBtn.addEventListener("click", () => fileInput.click()); fileInput.addEventListener("change", (e) => { const file = e.target.files?.[0]; if (file) { if (historySelect) historySelect.value = ""; loadReferenceFromFile(file); } fileInput.value = ""; }); } if (clearBtn) { clearBtn.addEventListener("click", () => clearReference()); } if (historySelect) { historySelect.addEventListener("change", () => { const vid = historySelect.value; if (!vid) { clearReference(); return; } const state = getState(); const version = state.historyVersions.find((v) => v.id === vid); if (version) { loadReferenceFromHistory(version); } }); } if (regionReset) { regionReset.addEventListener("click", () => { if (referenceState) { referenceState.region = null; redrawReferenceCanvas(); updateReferenceNote(); setReference({ region: null }); } }); } if (canvas) { canvas.addEventListener("pointerdown", (event) => { const state = getState(); if (state.isBusy || !referenceState) return; event.preventDefault(); canvas.setPointerCapture(event.pointerId); const pos = canvasPointFromEvent(canvas, event); regionDragStart = pos; referenceState.region = { x: pos.x, y: pos.y, w: 0, h: 0 }; redrawReferenceCanvas(); updateReferenceNote(); }); canvas.addEventListener("pointermove", (event) => { const state = getState(); if (state.isBusy || !referenceState || !regionDragStart) return; const pos = canvasPointFromEvent(canvas, event); const x = Math.min(regionDragStart.x, pos.x); const y = Math.min(regionDragStart.y, pos.y); const w = Math.max(regionDragStart.x, pos.x) - x; const h = Math.max(regionDragStart.y, pos.y) - y; referenceState.region = { x, y, w, h }; redrawReferenceCanvas(); updateReferenceNote(); }); canvas.addEventListener("pointerup", () => { if (regionDragStart) { regionDragStart = null; if ( referenceState?.region && (referenceState.region.w < 3 || referenceState.region.h < 3) ) { referenceState.region = null; redrawReferenceCanvas(); updateReferenceNote(); } setReference({ region: referenceState?.region || null }); } }); canvas.addEventListener("pointercancel", () => { regionDragStart = null; }); } }