Fix Catti reaction mouths rendering left-shifted instead of centered

Catti's dual cat-mouth (two mirrored Q-curve paths sharing a center
start point) caused extractMouthPositionFromElements to return only
the left half's coordinates. Every mouth generator then computed
cx=(100+82)/2=91 instead of the true center x=100.

The fix scans all Q-curve paths in the mouth section and computes the
full horizontal extent. For Catti: startX=82, endX=118, center=100.
Single-path mouths are unaffected (the extra-path loop simply doesn't
execute). Froggi's dual-path mouth already had symmetric bounds so its
result is unchanged.
This commit is contained in:
filemon
2026-04-18 23:38:41 -03:00
parent 27736c7047
commit d1ae988024
+36 -15
View File
@@ -82,24 +82,45 @@ function detectMouthByMarker(svgText: string): MouthDetectionResult | null {
/**
* Extract mouth position from mouth SVG elements.
*
* When the mouth section contains multiple Q-curve paths (e.g. Catti's
* dual cat-mouth), computes the full horizontal extent across all paths
* so the detected position represents the whole mouth, not just the
* first half. Single-path mouths are unaffected.
*/
function extractMouthPositionFromElements(elements: string): MouthPosition | null {
const pathMatch = elements.match(/d="M\s*([\d.]+)\s+([\d.]+)\s*Q\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)"/);
if (pathMatch) {
const strokeWidthMatch = elements.match(/stroke-width="([^"]*)"/);
const strokeWidth = strokeWidthMatch ? strokeWidthMatch[1] : '2.5';
return {
startX: parseFloat(pathMatch[1]),
startY: parseFloat(pathMatch[2]),
controlX: parseFloat(pathMatch[3]),
controlY: parseFloat(pathMatch[4]),
endX: parseFloat(pathMatch[5]),
endY: parseFloat(pathMatch[6]),
strokeAttrs: `stroke="#1f2937" stroke-width="${strokeWidth}"`,
};
const qCurveRegex = /d="M\s*([\d.]+)\s+([\d.]+)\s*Q\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)"/g;
const firstMatch = qCurveRegex.exec(elements);
if (!firstMatch) {
return null;
}
return null;
const strokeWidthMatch = elements.match(/stroke-width="([^"]*)"/);
const strokeWidth = strokeWidthMatch ? strokeWidthMatch[1] : '2.5';
// Start from the first path's coordinates
let minX = Math.min(parseFloat(firstMatch[1]), parseFloat(firstMatch[5]));
let maxX = Math.max(parseFloat(firstMatch[1]), parseFloat(firstMatch[5]));
const startY = parseFloat(firstMatch[2]);
const controlY = parseFloat(firstMatch[4]);
const endY = parseFloat(firstMatch[6]);
// Widen bounds with any additional Q-curve paths in the section
let extra: RegExpExecArray | null;
while ((extra = qCurveRegex.exec(elements)) !== null) {
minX = Math.min(minX, parseFloat(extra[1]), parseFloat(extra[5]));
maxX = Math.max(maxX, parseFloat(extra[1]), parseFloat(extra[5]));
}
return {
startX: minX,
startY,
controlX: (minX + maxX) / 2,
controlY,
endX: maxX,
endY,
strokeAttrs: `stroke="#1f2937" stroke-width="${strokeWidth}"`,
};
}
// ─── Regex-Based Detection ────────────────────────────────────────────────────