import type { ClockSpec, FractionSpec, GridSpec, GroupsSpec, PatternSpec, TableSpec } from './types';
import { useContext } from 'react';
import { COLORS, HideCounts, INK, bidi, color, mathText } from './svg';
import { toFa } from '@/lib/format';
const W = 320;
const SHADE = COLORS.yellow;
/* ─── کسر: دایره یا نوارِ تکهتکه ─────────────────────────────────────────── */
export function Fraction({ spec }: { spec: FractionSpec }) {
const hide = useContext(HideCounts);
if (spec.compare?.length) return ;
if (spec.wholes) return ;
const { parts, shaded } = spec;
const fill = (i: number) => (i < shaded ? SHADE : '#fff');
const caption = hide ? null : {`${toFa(shaded)} تکه از ${toFa(parts)} تکه`};
if (spec.shape === 'rect') {
const w = (W - 40) / parts;
return (
);
}
const cx = W / 2, cy = 80, r = 66;
return (
);
}
/** چند کسر روی نوارهای هماندازه، برای مقایسه (یکپنجم و نُهدهم). */
function FractionCompare({ items, hide }: { items: { parts: number; shaded: number; label?: string }[]; hide: boolean }) {
const barW = W - 110, h = 30, gap = 16;
const H = items.length * (h + gap) + 8;
const cols = [COLORS.yellow, COLORS.teal, COLORS.orange, COLORS.purple];
return (
);
}
/** عددِ مخلوط: چند واحدِ کامل و یک کسر (دو و هفتدهم). */
function MixedNumber({ wholes, parts, shaded, shape, hide }: { wholes: number; parts: number; shaded: number; shape: 'circle' | 'rect'; hide: boolean }) {
const n = wholes + 1, r = Math.min(40, (W - 20) / n / 2 - 6), cy = 52;
const cx = (i: number) => W - 10 - r - 6 - i * (2 * r + 12);
const slice = (x: number, i: number, full: boolean) => {
const a0 = -Math.PI / 2 + (2 * Math.PI * i) / parts, a1 = -Math.PI / 2 + (2 * Math.PI * (i + 1)) / parts;
return Math.PI ? 1 : 0} 1 ${x + r * Math.cos(a1)},${cy + r * Math.sin(a1)} Z`} fill={full || i < shaded ? SHADE : '#fff'} stroke={INK} strokeWidth={1.6} />;
};
return (
);
}
/* ─── شبکهی خانهها: درصد، مساحت با شمردن، کسر روی جدول ─────────────────── */
export function Grid({ spec }: { spec: GridSpec }) {
const hide = useContext(HideCounts);
const rows = spec.rows, cols = spec.cols;
const cell = Math.min(26, (W - 30) / cols, 220 / rows);
const gw = cols * cell, gh = rows * cell;
const x0 = (W - gw) / 2, y0 = 12;
const on = new Set();
if (typeof spec.shaded === 'number') {
// سطر به سطر، از بالا و از راست (جهتِ خواندنِ فارسی)
for (let i = 0; i < Math.min(spec.shaded, rows * cols); i++) on.add(`${Math.floor(i / cols)},${cols - 1 - (i % cols)}`);
} else spec.shaded.forEach(([r, c]) => on.add(`${r},${c}`));
const col = color(spec.color, SHADE);
const count = on.size;
return (
);
}
/* ─── گروهها: مدلِ ضرب و تقسیم برای دبستان ───────────────────────────────── */
export function Groups({ spec }: { spec: GroupsSpec }) {
const hide = useContext(HideCounts);
const g = spec.groups, each = spec.each;
const perRow = Math.min(g, g <= 4 ? g : Math.ceil(g / 2));
const rowsG = Math.ceil(g / perRow);
const inner = Math.ceil(Math.sqrt(each));
const dot = Math.min(14, 64 / inner);
const boxW = inner * dot + 16, boxH = Math.ceil(each / inner) * dot + 16;
const gap = 12;
const totalW = perRow * boxW + (perRow - 1) * gap;
const scale = Math.min(1, (W - 20) / totalW);
const H = (rowsG * (boxH + gap) + 30) * scale + 10;
const shape = spec.item ?? 'circle';
const cols = [COLORS.red, COLORS.blue, COLORS.green, COLORS.orange, COLORS.purple];
return (
);
}
function star(cx: number, cy: number, r: number): string {
const pts = Array.from({ length: 10 }, (_, i) => {
const t = -Math.PI / 2 + (i * Math.PI) / 5, rr = i % 2 ? r * 0.45 : r;
return `${cx + rr * Math.cos(t)},${cy + rr * Math.sin(t)}`;
});
return `M${pts.join(' L')} Z`;
}
/* ─── ساعت ────────────────────────────────────────────────────────────────── */
export function Clock({ spec }: { spec: ClockSpec }) {
const cx = W / 2, cy = 100, r = 84;
const h = ((spec.hour % 12) + spec.minute / 60) * 30, m = spec.minute * 6;
const hand = (deg: number, len: number, w: number, c: string) => {
const t = ((deg - 90) * Math.PI) / 180;
return ;
};
return (
);
}
/* ─── جدول (مقدارها، تعیینِ علامت) ───────────────────────────────────────── */
export function Table({ spec }: { spec: TableSpec }) {
const cellStyle = (v: string) => {
const t = v.trim();
if (t === '+') return { color: COLORS.green, fontWeight: 900 };
if (t === '-' || t === '−') return { color: COLORS.red, fontWeight: 900 };
if (t === '0' || t === '۰') return { color: INK, fontWeight: 900 };
return undefined;
};
return (
// جدولِ ریاضی (تعیینِ علامت، مقدارها) چپبهراست است، مثلِ محورِ x.
{spec.headers.length > 0 && (
{spec.headers.map((h, i) => | {mathText(h)} | )}
)}
{spec.rows.map((row, r) => (
{row.map((c, i) => (
|
{mathText(c)}
|
))}
))}
);
}
/* ─── الگوی شکلی: شکلِ ۱، ۲، ۳، … و «شکلِ بعدی؟» (فصلِ الگو و دنباله) ────────── */
/** خانههای هر مرحله در واحدِ شبکه؛ اولین مرحله راست است، مثلِ کتاب. */
function patternCells(layout: PatternSpec['layout'], n: number): [number, number][] {
const out: [number, number][] = [];
if (layout === 'square') for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) out.push([c, r]);
else if (layout === 'rect') for (let r = 0; r < n; r++) for (let c = 0; c <= n; c++) out.push([c, r]);
else if (layout === 'triangle') for (let r = 0; r < n; r++) for (let c = 0; c <= r; c++) out.push([c + (n - 1 - r) / 2, r]);
else if (layout === 'stair') for (let c = 0; c < n; c++) for (let r = 0; r <= c; r++) out.push([c, n - 1 - r]);
else if (layout === 'L') { for (let r = 0; r < n; r++) out.push([0, r]); for (let c = 1; c < n; c++) out.push([c, n - 1]); }
else for (let c = 0; c < n; c++) out.push([c, 0]);
return out;
}
export function Pattern({ spec }: { spec: PatternSpec }) {
const hide = useContext(HideCounts);
const stages = spec.stages;
const sticks = spec.unit === 'stick';
const sizes = stages.map((n) => {
const cells = sticks ? [] : patternCells(spec.layout, n);
const w = sticks ? n : Math.max(1, ...cells.map(([c]) => c + 1));
const h = sticks ? 1 : Math.max(1, ...cells.map(([, r]) => r + 1));
return { n, cells, w, h };
});
const gapU = 1.2;
const totalU = sizes.reduce((t, s) => t + s.w, 0) + gapU * (sizes.length - 1 + (spec.next ? 1 : 0)) + (spec.next ? 1.4 : 0);
const maxH = Math.max(...sizes.map((s) => s.h));
const u = Math.min(22, (W - 24) / totalU, 150 / maxH);
const H = maxH * u + 46;
const base = 10 + maxH * u;
const col = color(spec.color, COLORS.blue);
let x = W - 12;
const groups = sizes.map((s, i) => {
x -= s.w * u;
const x0 = x;
x -= gapU * u;
const count = sticks ? 3 * s.n + 1 : s.cells.length;
return (
{sticks
? Array.from({ length: s.n + 1 }, (_, k) => )
.concat(Array.from({ length: s.n }, (_, k) => [
,
,
]).flat())
: s.cells.map(([c, r], k) => {
const cx = x0 + (s.w - 1 - c) * u, cy = base - (s.h - r) * u;
return spec.unit === 'dot'
?
: ;
})}
{`شکل ${toFa(i + 1)}`}
{spec.counts !== false && !hide && {`${toFa(count)} ${sticks ? 'چوب' : 'تا'}`}}
);
});
const nx = x - 1.4 * u + gapU * u;
return (
);
}