import type { NumberLineSpec } from './types'; import { Arrowheads, COLORS, INK, Label, SERIES, SOFT, StackedFraction, arrowId, color, fa, gcd, mathText, niceStep, ticksBetween } from './svg'; import { toFa } from '@/lib/format'; const W = 330; const PAD = 26; /** * محورِ اعداد: نقطه (توپُر یا توخالی)، پرش (فلشِ خمیده)، و بازه (پاره‌خطِ رنگی با * سرِ باز/بسته، یا تا بی‌نهایت). با `fractions` تیک‌ها کسرِ دوطبقه‌اند (۱/۴، ۲/۴)، * چون در کتابِ دبستان «/» ممیز است. */ export function NumberLine({ spec }: { spec: NumberLineSpec }) { const { min, max } = spec; const intervals = spec.intervals ?? []; const lanes = intervals.length; const jumpH = spec.jumps.length ? 52 : 0; const LANE = intervals.some((iv) => iv.label) ? 24 : 14; const base = 22 + jumpH + lanes * LANE + 10; const H = base + (spec.fractions ? 58 : 44); const X = (v: number) => PAD + ((v - min) / (max - min)) * (W - 2 * PAD); // محورِ عددهای صحیح گامِ صحیحِ «گرد» می‌گیرد (۱، ۲، ۵، ۱۰، …)، نه ۲٫۵ const intStep = (span: number) => [1, 2, 5, 10, 20, 25, 50, 100, 200, 500, 1000].find((c) => span / c <= 12) ?? niceStep(span, 12); const integers = Number.isInteger(min) && Number.isInteger(max); const step = spec.step && spec.step > 0 && (max - min) / spec.step <= 40 ? spec.step : integers ? intStep(max - min) : niceStep(max - min, 12); const ticks = ticksBetween(min, max, step); const labelled = new Set(spec.points.map((p) => p.at)); // بیش از ۱۴ تیک: فقط بعضی عدد می‌گیرند (صفر، مضرب‌های ۵ یا ۱۰، نقطه‌های خواسته‌شده)، // وگرنه عددها روی هم می‌افتند. const every = ticks.length > 14 ? Math.ceil(ticks.length / 10) : 1; const showLabel = (t: number, i: number) => every === 1 || labelled.has(t) || Math.abs(t) < 1e-9 || i % every === 0; const arrows = new Set([INK, COLORS.red]); const tickLabel = (t: number) => { if (spec.fractions && step < 1) { const den = Math.round(1 / step); const num = Math.round(t * den); if (num % den === 0) return null; // عددِ صحیح با رقمِ معمولی const g = gcd(num, den); return { num: toFa(Math.abs(num / g)), den: toFa(den / g), neg: num < 0 }; } return null; }; return ( color(iv.color, COLORS[SERIES[i % SERIES.length]]))]} /> {ticks.map((t, ti) => { const fr = showLabel(t, ti) ? tickLabel(t) : null; const hideNumber = !showLabel(t, ti); const on = labelled.has(t); const whole = Math.abs(t - Math.round(t)) < 1e-9; return ( {fr ? ( ) : hideNumber ? null : ( {fa(t)} )} ); })} {intervals.map((iv, i) => { const col = color(iv.color, COLORS[SERIES[i % SERIES.length]]); const y = base - 10 - i * LANE; const a = iv.from === null ? PAD - 12 : X(iv.from), b = iv.to === null ? W - PAD + 12 : X(iv.to); return ( {iv.from !== null && } {iv.to !== null && } {iv.label && } ); })} {spec.jumps.map((j, i) => { const a = X(j.from), b = X(j.to); const lift = Math.min(42, Math.abs(b - a) * 0.45 + 12); const y = base - 6 - lanes * LANE; return ( {j.label && } ); })} {spec.points.map((p, i) => { const col = color(p.color, i === spec.points.length - 1 && spec.points.length > 1 ? COLORS.red : COLORS.blue); const numeric = /^[-−+]?[\d۰-۹.,٫/]+$/.test(p.label.trim()); return ( {p.label && !numeric && } ); })} ); }