GEN-3e: C-locus colourpoint naming (CP-<base> for A-+cchm; Marder/Siam/Zobel/Zobel-Hell for aa+cchm); chch kept as Hermelin/Himalaya via base-match

This commit is contained in:
2026-06-06 11:04:57 +02:00
parent 76fcad6168
commit 5fd8cf4395
2 changed files with 74 additions and 8 deletions

View File

@@ -191,25 +191,61 @@ function eFamily(g: Genotype): string | null {
}
/** Resolve a genotype to its German Farbschlag (with Schecke/Rex modifiers). */
export function farbschlagFor(g: Genotype): FarbschlagMatch {
/** Resolve an allele pair to concrete alleles, defaulting unknown to wild-type. */
function resolvedPair(g: Genotype, locus: LocusKey): [string, string] {
const alleles = LOCI[locus].alleles
const isMarker = locus === 'Sp' || locus === 'Re' || locus === 'Sls'
const fallback = isMarker ? alleles[alleles.length - 1] : alleles[0]
const [x, y] = g[locus].map((a) => (a === WILDCARD ? fallback : a))
return [x, y]
}
/** Base colour name (no modifiers, no colourpoint prefix), via E-family + matches. */
function baseColourFor(g: Genotype): string | null {
const family = eFamily(g)
// When the animal is a Fuchs/Schimmel-family genotype, only entries that
// explicitly pin the E locus may name it (so e.g. eef can't masquerade as a
// C-series white via wildcard C); otherwise fall back to the family name.
const base = family
? (BASE_COLORS.find((e) => e.tokens.E !== undefined && matches(g, e)) ?? null)
: (BASE_COLORS.find((e) => matches(g, e)) ?? null)
return base?.name ?? family
}
/**
* GEN-3e: the C-locus colourpoint NAMING transform (breeder-authoritative).
* Returns the colourpoint name, or null when it doesn't apply (full C present,
* or chch — which the base matcher names Hermelin/Himalaya, preserving both).
* aa cchm/cchm -> Marder | aa cchm/ch -> Siam
* aa cchm/cchm gg -> Zobel | aa cchm/ch gg -> Zobel-Hell
* A- cchm/cchm | cchm/ch -> CP-<base colour> (base computed as if C were full)
*/
function colourpointName(g: Genotype): string | null {
const c = resolvedPair(g, 'C')
if (c.includes('C')) return null // a full C allele => full colour, no CP
if (c[0] === 'ch' && c[1] === 'ch') return null // chch -> base matcher (Hermelin/Himalaya)
// Remaining: cchm/cchm or cchm/ch (colourpoint, no full C, not chch).
const bothCchm = c[0] === 'cchm' && c[1] === 'cchm'
const agouti = resolvedPair(g, 'A').includes('A')
if (!agouti) {
const [g1, g2] = resolvedPair(g, 'G')
const grey = g1 === 'g' && g2 === 'g'
if (grey) return bothCchm ? 'Zobel' : 'Zobel-Hell'
return bothCchm ? 'Marder' : 'Siam'
}
// A- colourpoint -> CP-<base colour>, base as if C were full.
const base = baseColourFor(makeGenotype({ ...g, C: ['C', 'C'] }))
return base ? `CP-${base}` : null
}
export function farbschlagFor(g: Genotype): FarbschlagMatch {
const modifiers: string[] = []
if (locusToken(g, 'Sp') === 'Sp') modifiers.push('Schecke')
if (locusToken(g, 'Re') === 'Re') modifiers.push('Rex')
const baseName = base?.name ?? family
const baseName = colourpointName(g) ?? baseColourFor(g)
if (!baseName) {
return { name: UNKNOWN_FARBSCHLAG, base: null, unknown: true }
}
const name = [baseName, ...modifiers].join(' ')
return { name, base, unknown: false }
return { name, base: null, unknown: false }
}
/**