/** * Optimal Recognition Point (ORP) Calculation for Spritz RSVP Reading. * Fixates the eye at ~30%-35% into the word where human ocular fixation naturally rests. * Uses unicode-safe code point segmentation (Array.from) to eliminate surrogate pair * and emoji splitting defects. */ 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. * Based on unicode code-point length (Array.from(cleanWord).length). */ export function calculateOrpIndex(cleanWord: string): number { const chars = Array.from(cleanWord); const len = chars.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. * Operates purely on unicode code points to prevent corrupting surrogate pairs. */ export function splitOrp( coreWord: string, leadingPunct: string = '', trailingPunct: string = '' ): SplitWord { if (!coreWord) { return { prefix: leadingPunct, pivot: '', suffix: trailingPunct, orpIndex: 0, }; } const chars = Array.from(coreWord); const orpIndex = calculateOrpIndex(coreWord); const prefix = leadingPunct + chars.slice(0, orpIndex).join(''); const pivot = chars[orpIndex] || ''; const suffix = chars.slice(orpIndex + 1).join('') + trailingPunct; return { prefix, pivot, suffix, orpIndex, }; }