import { createContext, type ReactNode } from 'react'; import type { ColorName } from './types'; import { toFa } from '@/lib/format'; /** * رنگ‌ها و قطعه‌های مشترکِ همه‌ی شکل‌ها. * * رنگ‌ها معنا دارند و در همه‌ی شکل‌ها یکی‌اند: آبی شکلِ اصلی، قرمز زاویه و چیزی که * باید دیده شود، سبز جواب، نارنجی/بنفش سری‌های دوم و سوم. مدل فقط نامِ رنگ را * می‌دهد، نه کدِ hex، تا شکل‌ها همیشه با رنگ‌های اپ بخوانند. */ export const COLORS: Record = { blue: '#3F5BD8', red: '#E5484D', green: '#1F9D55', orange: '#EE8A12', purple: '#8B5CF6', teal: '#0E9F9F', pink: '#D9468F', gray: '#8A8FA3', black: '#26263D', yellow: '#F5B400', }; /** ترتیبِ رنگِ سری‌ها وقتی مدل رنگ نداده (نمودارِ دوم، ستونِ سوم، …). */ export const SERIES: ColorName[] = ['blue', 'red', 'green', 'orange', 'purple', 'teal', 'pink', 'yellow']; /** رنگِ هر ستون یا برشِ نمودار: اگر برچسب نامِ یک رنگ باشد («قرمز»، «مهره‌ی آبی (۳)») همان رنگ، وگرنه رنگ‌های * پیش‌فرض به ترتیب، بدون رنگ‌هایی که برچسبی به نامشان هست؛ تا «قرمز» در چرخنده یا جدولِ فراوانی آبی کشیده نشود. */ const COLOR_WORDS: [string, ColorName][] = [ ['قرمز', 'red'], ['سرخ', 'red'], ['آبی', 'blue'], ['سبز', 'green'], ['نارنجی', 'orange'], ['بنفش', 'purple'], ['صورتی', 'pink'], ['زرد', 'yellow'], ['خاکستری', 'gray'], ['مشکی', 'black'], ['سیاه', 'black'], ]; const namedColor = (label: string): ColorName | undefined => COLOR_WORDS.find(([w]) => new RegExp(`(^|[\\s‌(«])${w}($|[\\s‌)»])`).test(label))?.[1]; export function seriesColors(labels: string[], n: number): string[] { const named = Array.from({ length: n }, (_, i) => namedColor(labels[i] ?? '')); const free = SERIES.filter((c) => !named.includes(c)); let k = 0; return named.map((c) => COLORS[c ?? free[k++ % free.length]]); } export const INK = COLORS.black; export const SOFT = '#9EA3B8'; export const GRID = '#E4E6F0'; export const PAPER = '#FBFBFF'; export const color = (name: ColorName | undefined, fallback: string) => (name ? COLORS[name] : fallback); /** عدد با رقمِ فارسی، منفیِ درست (−) و بدونِ صفرهای بی‌معنای اعشار. */ export function fa(n: number, digits = 2): string { if (!Number.isFinite(n)) return ''; const r = Math.round(n * 10 ** digits) / 10 ** digits; const s = (Object.is(r, -0) ? 0 : r).toString(); return toFa(s.replace('-', '−').replace('.', '٫')); } /** حرفِ فارسی/عربی — نه رقم: ۰–۹ِ فارسی (U+06F0…) عمداً بیرون است. */ export const PERSIAN_LETTER = /[\u0620-\u064A\u066E-\u06D3\u06FA-\u06FF]/; /** * متنِ ریاضی را چپ‌به‌راست قفل می‌کند. رقمِ فارسی در الگوریتمِ جهتِ یونیکد «عددِ * عربی» است و علامت‌های بینِ دو رقم را راست‌به‌چپ می‌کند: «۳ × ۴ = ۱۲» به شکلِ * «۱۲ = ۴ × ۳» دیده می‌شد، حتی با dir=ltr. نشانه‌ی نامرئیِ LRM بینِ نویسه‌ها * همه‌ی علامت‌ها را چپ‌به‌راست نگه می‌دارد. فقط برای متنی که حرفِ فارسی ندارد. */ export function ltr(s: string): string { if (!s || PERSIAN_LETTER.test(s)) return s; return '\u200E' + [...s].join('\u200E') + '\u200E'; } /** رقمِ لاتین به فارسی، منفیِ درست (−)، و قفلِ چپ‌به‌راست برای متنِ ریاضیِ خالص. */ export function mathText(s: string): string { const t = toFa(s.replace(/-/g, '−')); return ltr(t); } /** * جهتِ متنِ SVG: متنِ ریاضیِ خالص با bidi-override چپ‌به‌راست (در Chrome نه LRM و نه * direction=ltr رقمِ فارسی را از برعکس شدن نجات نداد: «۳ × ۴ = ۱۲» → «۱۲ = ۴ × ۳»)، * و متنِ فارسی جهتش را از خودش می‌گیرد. */ export function bidi(children: ReactNode): { direction?: 'ltr'; style: { unicodeBidi: 'plaintext' | 'bidi-override'; direction?: 'ltr' } } { const math = typeof children === 'string' && !PERSIAN_LETTER.test(children); // bidi-override: نویسه‌ها دقیقاً به ترتیبِ نوشتن، چپ به راست. برای ریاضیِ خالص درست // همان چیزی است که می‌خواهیم؛ برای متنِ فارسی هرگز، چون حروف را برعکس می‌کند. return math ? { direction: 'ltr', style: { unicodeBidi: 'bidi-override', direction: 'ltr' } } : { style: { unicodeBidi: 'plaintext' } }; } /** برچسب با هاله‌ی سفید تا روی خط‌ها خوانا بماند؛ جهتش را از متنِ خودش می‌گیرد. */ export function Label({ x, y, color: c = INK, size = 11, weight = 700, anchor = 'middle', children, }: { x: number; y: number; color?: string; size?: number; weight?: number; anchor?: 'start' | 'middle' | 'end'; children: ReactNode }) { return ( {children} ); } /** سرِ فلش؛ هر رنگ یک marker (SVG رنگِ marker را از خط به ارث نمی‌برد). */ export function Arrowheads({ ids }: { ids: string[] }) { return ( {[...new Set(ids)].map((c) => ( ))} ); } export const arrowId = (hex: string) => `dg-ah-${hex.replace('#', '')}`; /** کسرِ دوطبقه (صورت روی مخرج)؛ در کتابِ دبستان «/» ممیز است، پس کسر همیشه طبقه‌ای کشیده می‌شود. */ export function StackedFraction({ x, y, num, den, color: c = INK, size = 10 }: { x: number; y: number; num: string; den: string; color?: string; size?: number }) { const w = Math.max(num.length, den.length) * size * 0.62 + 4; return ( {num} {den} ); } export function niceStep(span: number, maxTicks: number): number { const raw = span / Math.max(1, maxTicks); const pow = 10 ** Math.floor(Math.log10(raw || 1)); const m = raw / pow; return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 2.5 ? 2.5 : m <= 5 ? 5 : 10) * pow; } export function ticksBetween(lo: number, hi: number, step: number): number[] { const out: number[] = []; for (let t = Math.ceil(lo / step - 1e-9) * step; t <= hi + 1e-9; t += step) out.push(Math.round(t * 1e9) / 1e9); return out; } /** بزرگ‌ترین مقسوم‌علیهِ مشترک، برای ساده کردنِ کسرِ برچسب‌ها. */ export function gcd(a: number, b: number): number { a = Math.abs(a); b = Math.abs(b); while (b) [a, b] = [b, a % b]; return a || 1; } /** برچسبِ مضربِ π برای محورِ مثلثاتی: π/2، π، 3π/2، −π، … */ export function piLabel(t: number): string { const k = Math.round(t / (Math.PI / 2)); if (k === 0) return '۰'; const g = gcd(k, 2); const num = k / g, den = 2 / g; const sign = num < 0 ? '−' : ''; const n = Math.abs(num); const head = n === 1 ? 'π' : `${toFa(n)}π`; return den === 1 ? `${sign}${head}` : `${sign}${head}/${toFa(den)}`; } /** * زیرِ شکلِ سوالِ آزمون، شمارشِ خودکار («۲ تکه از ۵ تکه»، «۶ خانه از ۱۲ خانه»، «۶ × ۴ = ۲۴»، «۹ تا») * جوابِ سوال را لو می‌داد؛ آزمون این را روشن می‌کند و شکل فقط خودِ داده را نشان می‌دهد. * در درس و جوابِ دستیار شمارش می‌ماند چون آنجا توضیح است، نه سوال. */ export const HideCounts = createContext(false);