/** * Optimal Recognition Point (ORP) Calculation for Spritz RSVP Reading. * Fixates the eye at ~30%-35% into the word where human ocular fixation naturally rests. */ const CJK_REGEX = /[一-龥぀-ヿ가-힯]/; export function isCJKChar(char: string): boolean { return CJK_REGEX.test(char); } export function isCJKWord(word: string): boolean { return CJK_REGEX.test(word); } /** * Calculates the 0-based index of the ORP character in cleanWord. */ export function calculateOrpIndex(cleanWord: string): number { const len = cleanWord.length; if (len <= 1) return 0; if (isCJKWord(cleanWord)) { // CJK characters have high semantic density per glyph if (len === 2) return 0; if (len <= 4) return 1; if (len <= 6) return 2; return Math.min(len - 1, Math.floor((len - 1) * 0.35)); } // Standard Spritz ORP Table for alphabetic words if (len <= 5) return 1; if (len <= 9) return 2; if (len <= 13) return 3; return 4; } export interface SplitWord { prefix: string; pivot: string; suffix: string; orpIndex: number; } /** * Splits a token into prefix, pivot (ORP character), and suffix. * Handles leading punctuation (e.g. "“", "《", "(") and trailing punctuation. */ export function splitOrp( coreWord: string, leadingPunct: string = '', trailingPunct: string = '' ): SplitWord { if (!coreWord) { return { prefix: leadingPunct, pivot: '', suffix: trailingPunct, orpIndex: 0, }; } const orpIndex = calculateOrpIndex(coreWord); const prefix = leadingPunct + coreWord.slice(0, orpIndex); const pivot = coreWord[orpIndex] || ''; const suffix = coreWord.slice(orpIndex + 1) + trailingPunct; return { prefix, pivot, suffix, orpIndex, }; }