export function rectFromPoints(start, end) { return { x: Math.round(Math.min(start.x, end.x)), y: Math.round(Math.min(start.y, end.y)), width: Math.round(Math.abs(end.x - start.x)), height: Math.round(Math.abs(end.y - start.y)), }; } export function resolveFieldRect(anchorPosition, field) { return { x: Math.round(anchorPosition.x + field.offsetX), y: Math.round(anchorPosition.y + field.offsetY), width: Math.round(field.width), height: Math.round(field.height), }; } export function combineRects(first, second) { const x = Math.min(first.x, second.x); const y = Math.min(first.y, second.y); return { x, y, width: Math.max(first.x + first.width, second.x + second.width) - x, height: Math.max(first.y + first.height, second.y + second.height) - y, }; } export function normalizeOcrText(text) { return text.replace(/\s+/g, " ").trim(); } export function parseFieldValue(type, text) { const normalized = normalizeOcrText(text); if (type !== "price" && type !== "quantity") { return normalized; } const digits = normalized.replace(/\D/g, ""); return digits ? Number(digits) : null; } export function parseStoreHeaderText(text) { const normalized = normalizeOcrText(text); const match = normalized.match( /private\s*store.*?\b(buy|sell)\b.*?[-–—]\s*(.+)$/i, ); if (!match?.[2]?.trim()) return null; return { side: match[1].toLowerCase(), merchant: match[2].trim(), rawText: normalized, }; } export function parseTooltipText(text) { const lines = text .split(/\r?\n/) .map(normalizeOcrText) .filter(Boolean); const priceIndex = lines.findIndex( (line) => /\d[\d\s,.'`]*\s*adena/i.test(line) || /pr[i1l]ce\s*[:;]?\s*\d/i.test(line) || /^\s*\d[\d\s,.'`]*\s*$/.test(line), ); if (priceIndex < 0) return null; const priceLine = lines[priceIndex]; const priceText = priceLine.match(/([\d][\d\s,.'`]*)\s*adena/i)?.[1] ?? priceLine.match(/pr[i1l]ce\s*[:;]?\s*([\d][\d\s,.'`]*)/i)?.[1] ?? priceLine.match(/^\s*([\d][\d\s,.'`]*)\s*$/)?.[1] ?? ""; const digits = priceText.replace(/\D/g, ""); let name = lines.slice(0, priceIndex).find((line) => !/^pr[i1l]ce\s*[:;]?$/i.test(line)) ?? ""; if (!name) { name = priceLine.slice(0, priceLine.search(/pr[i1l]ce/i)).trim(); } if (!name || !digits) return null; const quantityMatches = [...name.matchAll(/\(([\d][\d\s,.]*)\)/g)]; const quantityMatch = quantityMatches.at(-1); const quantityText = quantityMatch?.[1].replace(/\D/g, "") ?? ""; if (quantityMatch) { name = `${name.slice(0, quantityMatch.index)} ${name.slice(quantityMatch.index + quantityMatch[0].length)}` .replace(/\s+/g, " ") .trim(); } return { name, quantity: quantityText ? Number(quantityText) : 1, priceAdena: Number(digits), rawText: lines.join("\n"), }; } export function isCalibrationReady(calibration) { return Boolean( calibration?.saleAnchor?.image && calibration.saleAnchor.width >= 4 && calibration.saleAnchor.height >= 4 && calibration.storeHeaderRegion && calibration.tooltipSearchRegion && calibration.tooltipAnchor?.image && calibration.itemNameRegion && calibration.itemPriceRegion, ); } export function isRectInside(rect, width, height) { return ( rect.x >= 0 && rect.y >= 0 && rect.width > 0 && rect.height > 0 && rect.x + rect.width <= width && rect.y + rect.height <= height ); }