import type { ColorName, PlaneItem, PointDef, Ref } from './types'; /** * مثلث و چهارضلعی از روی داده‌های مسئله، نه از روی مختصاتِ حدسیِ مدل. * * مدل می‌گوید «مثلثی با AB = 6، AC = 8 و زاویه‌ی A = 90°» یا «ذوزنقه با قاعده‌های ۸ و ۵ و * ارتفاعِ ۳»؛ این‌جا با قانونِ کسینوس‌ها و سینوس‌ها (مثلث) یا فرمولِ هر چهارضلعی رأس‌ها * حساب می‌شوند و به نقطه + polygon + برچسب باز می‌شوند. سنجشِ ۲۰۲۶-۰۹-۲۸: مربعی که مدل * مختصاتش را خودش نوشت کج درآمد؛ این ابزار آن خطا را از دستِ مدل بیرون می‌آورد. * داده‌ی ناقص با پیش‌فرضِ ساده پر می‌شود (ضلعِ نامعلوم = میانگینِ ضلع‌های معلوم)، پس * شکل همیشه کشیده می‌شود؛ داده‌ی متناقض (۳، ۴، ۱۰) مثلثی نمی‌سازد و item حذف می‌شود. */ type XY = [number, number]; const RAD = Math.PI / 180; export interface TriangleItem { type: 'triangle'; names: [string, string, string]; /** کلید: دو نامِ رأس («AB» یا «BA») */ sides?: Record; /** کلید: نامِ رأس، درجه */ angles?: Record; /** برچسبِ دلخواه برای ضلع («BC»: «x»)، رأس («A»: «?») یا ارتفاع («h») */ labels?: Record; /** رأسی که ارتفاع از آن بر ضلعِ روبه‌رو کشیده شود */ altitude?: string; /** جای رأسِ اول */ at?: XY; color?: ColorName; fill?: boolean; } export interface QuadItem { type: 'quad'; shape: 'rectangle' | 'square' | 'parallelogram' | 'rhombus' | 'trapezoid'; names: [string, string, string, string]; w?: number; h?: number; side?: number; angle?: number; top?: number; d1?: number; d2?: number; right?: boolean; at?: XY; labels?: Record; color?: ColorName; fill?: boolean; } interface Solved { a: number; b: number; c: number; A: number; B: number; C: number } /** a روبه‌روی A و ...؛ زاویه‌ها به درجه. null یعنی داده‌ها مثلث نمی‌سازند. */ export function solveTriangle(sides: Partial>, angles: Partial>): Solved | null { const s: Record = { ...sides }; const g: Record = { ...angles }; const opp = { a: 'A', b: 'B', c: 'C' } as const; const known = (o: Record, k: string) => o[k] !== undefined && Number.isFinite(o[k]!); const step = () => { const ks = ['A', 'B', 'C'].filter((k) => known(g, k)); if (ks.length === 2) { const rest = 180 - ks.reduce((t, k) => t + g[k]!, 0); if (rest <= 0) return false; g[['A', 'B', 'C'].find((k) => !known(g, k))!] = rest; } const ss = (['a', 'b', 'c'] as const).filter((k) => known(s, k)); if (ss.length === 3 && ['A', 'B', 'C'].some((k) => !known(g, k))) { const { a, b, c } = s as Record; if (a + b <= c || a + c <= b || b + c <= a) return false; g.A = Math.acos((b * b + c * c - a * a) / (2 * b * c)) / RAD; g.B = Math.acos((a * a + c * c - b * b) / (2 * a * c)) / RAD; g.C = 180 - g.A - g.B; } // دو ضلع و زاویه‌ی بینشان for (const [x, y, z] of [['b', 'c', 'a'], ['a', 'c', 'b'], ['a', 'b', 'c']] as const) { if (known(s, x) && known(s, y) && !known(s, z) && known(g, opp[z])) { s[z] = Math.sqrt(s[x]! ** 2 + s[y]! ** 2 - 2 * s[x]! * s[y]! * Math.cos(g[opp[z]]! * RAD)); } } // قانونِ سینوس‌ها const pair = (['a', 'b', 'c'] as const).find((k) => known(s, k) && known(g, opp[k])); if (pair) { const k = s[pair]! / Math.sin(g[opp[pair]]! * RAD); for (const side of ['a', 'b', 'c'] as const) { const ang = opp[side]; if (!known(s, side) && known(g, ang)) s[side] = k * Math.sin(g[ang]! * RAD); else if (known(s, side) && !known(g, ang)) { const r = s[side]! / k; if (r > 1 + 1e-9) return false; g[ang] = Math.asin(Math.min(1, r)) / RAD; } } } return true; }; const done = () => ['a', 'b', 'c'].every((k) => known(s, k)) && ['A', 'B', 'C'].every((k) => known(g, k)); const count = () => ['a', 'b', 'c'].filter((k) => known(s, k)).length + ['A', 'B', 'C'].filter((k) => known(g, k)).length; for (let round = 0; round < 12 && !done(); round++) { const before = count(); if (!step()) return null; if (done()) break; if (count() > before) continue; // گیر کرد: داده‌ی ناقص. ضلعِ نامعلومِ اول = میانگینِ ضلع‌های معلوم (متساوی‌الساقین/متساوی‌الاضلاع) const ss = (['a', 'b', 'c'] as const).filter((k) => known(s, k)); if (!ss.length) { s.c = 4; continue; } const missing = (['a', 'b', 'c'] as const).find((k) => !known(s, k)); if (missing) s[missing] = ss.reduce((t, k) => t + s[k]!, 0) / ss.length; } if (!done()) return null; const out = { a: s.a!, b: s.b!, c: s.c!, A: g.A!, B: g.B!, C: g.C! }; if (Object.values(out).some((v) => !Number.isFinite(v) || v <= 0) || Math.abs(out.A + out.B + out.C - 180) > 0.5) return null; return out; } const num = (v: number) => String(Math.round(v * 100) / 100); /** رأس‌های شکل‌های مرکبی که تا این‌جا جا گرفته‌اند (برای چیدنِ چند شکل کنارِ هم). */ export interface Layout { placed: XY[] } /** * جای شکل: رأس‌ها در دستگاهِ محلی (رأسِ اول در مبدأ، ضلعِ اول روی محورِ x) ساخته شده‌اند. * دو رأسِ اول از قبل مختصات دارند → شکل روی همان ضلع ساخته می‌شود (ذوزنقه روی ضلعِ مستطیل) * فقط رأسِ اول → همان‌جا؛ "at" → همان‌جا * هیچ‌کدام، ولی شکلِ دیگری قبلاً کشیده شده → کنارِ آن، نه روی آن (دو مربعِ کنارِ هم) */ function placement(names: string[], local: XY[], points: Record, layout: Layout, at?: XY): (p: XY) => XY { const plain = (n: string) => (Array.isArray(points[n]) ? (points[n] as XY) : null); const p0 = plain(names[0]), p1 = plain(names[1]); let T: (p: XY) => XY = (p) => p; if (p0 && p1 && Math.hypot(p1[0] - p0[0], p1[1] - p0[1]) > 1e-9) { const len = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]); const u: XY = [(p1[0] - p0[0]) / len, (p1[1] - p0[1]) / len]; T = (p) => [p0[0] + p[0] * u[0] - p[1] * u[1], p0[1] + p[0] * u[1] + p[1] * u[0]]; } else if (p0) T = (p) => [p0[0] + p[0], p0[1] + p[1]]; else if (at) T = (p) => [at[0] + p[0], at[1] + p[1]]; else if (layout.placed.length) { const right = Math.max(...layout.placed.map((q) => q[0])); const bottom = Math.min(...layout.placed.map((q) => q[1])); const minX = Math.min(...local.map((q) => q[0])), minY = Math.min(...local.map((q) => q[1])); const gap = Math.max(1, 0.2 * (Math.max(...local.map((q) => q[0])) - minX)); T = (p) => [p[0] - minX + right + gap, p[1] - minY + bottom]; } layout.placed.push(...local.map(T)); return T; } function sideKey(key: string, names: string[]): [number, number] | null { for (let i = 0; i < names.length; i++) for (let j = 0; j < names.length; j++) { if (i !== j && key === names[i] + names[j]) return [i, j]; } return null; } /** item‌های ساده (نقطه، polygon، پاره‌خطِ برچسب‌دار، زاویه) به‌جای triangle. */ export function expandTriangle(item: TriangleItem, idx: number, points: Record, taken: (n: string) => boolean, layout: Layout): PlaneItem[] | null { const [P, Q, R] = item.names; const sides: Partial> = {}; const oppOf = (i: number, j: number) => (['a', 'b', 'c'] as const)[3 - i - j]; Object.entries(item.sides ?? {}).forEach(([k, v]) => { const p = sideKey(k, item.names); if (p) sides[oppOf(p[0], p[1])] = v; }); const angles: Partial> = {}; Object.entries(item.angles ?? {}).forEach(([k, v]) => { const i = item.names.indexOf(k); if (i >= 0) angles[(['A', 'B', 'C'] as const)[i]] = v; }); // مثلثی روی نقطه‌هایی که مدل خودش تعریف کرده (مختصات، polar، تبدیل) و بی‌داده‌ی ضلع و زاویه: // همان نقطه‌ها، دست‌نخورده (سنجشِ ۲۰۲۶-۰۹-۲۹: جابه‌جا کردنشان تصویرِ تقارن را خراب کرد) const hasData = Object.keys(sides).length + Object.keys(angles).length > 0; if (!(hasData || !item.names.every((n) => n in points))) { layout.placed.push(...item.names.map((n) => points[n]).filter((v): v is XY => Array.isArray(v))); } else { const t = solveTriangle(sides, angles); if (!t) return null; const local: XY[] = [[0, 0], [t.c, 0], [t.b * Math.cos(t.A * RAD), t.b * Math.sin(t.A * RAD)]]; const T = placement(item.names, local, points, layout, item.at); item.names.forEach((n, i) => { points[n] = T(local[i]); }); } const style = { color: item.color }; const out: PlaneItem[] = [{ type: 'polygon', points: [P, Q, R], fill: item.fill ?? true, ...style }]; const labels = item.labels ?? {}; const sideLabel = new Map(); Object.entries(item.sides ?? {}).forEach(([k, v]) => { const p = sideKey(k, item.names); if (p) sideLabel.set([p[0], p[1]].sort().join(), num(v)); }); Object.entries(labels).forEach(([k, v]) => { const p = sideKey(k, item.names); if (p) sideLabel.set([p[0], p[1]].sort().join(), v); }); // همه‌ی ضلع‌ها پاره‌خط‌اند (نه فقط برچسب‌دارها) تا ضخامتِ ضلع‌ها یکی باشد [[0, 1], [1, 2], [0, 2]].forEach(([i, j]) => { out.push({ type: 'segment', from: item.names[i], to: item.names[j], label: sideLabel.get(`${i},${j}`), ...style }); }); item.names.forEach((n, i) => { const given = item.angles?.[n]; const label = labels[n] ?? (given !== undefined ? `${num(given)}°` : undefined); if (label === undefined) return; const [from, to] = [item.names[(i + 1) % 3], item.names[(i + 2) % 3]]; if (given !== undefined && Math.abs(given - 90) < 1e-6 && labels[n] === undefined) out.push({ type: 'rightAngle', vertex: n, from, to }); else out.push({ type: 'angle', vertex: n, from, to, label, color: 'red' }); }); const alt = item.altitude; if (alt && item.names.includes(alt)) { const [u, v] = item.names.filter((n) => n !== alt); const foot = taken('H') ? `_h${idx}` : 'H'; points[foot] = { foot: [alt, u, v] }; // امتدادِ ضلع فقط وقتی پای ارتفاع بیرونِ مثلث می‌افتد (مثلثِ منفرجه) const [pu, pv, pa] = [u, v, alt].map((n) => (Array.isArray(points[n]) ? (points[n] as XY) : ([0, 0] as XY))); const d: XY = [pv[0] - pu[0], pv[1] - pu[1]]; const tt = ((pa[0] - pu[0]) * d[0] + (pa[1] - pu[1]) * d[1]) / (d[0] * d[0] + d[1] * d[1] || 1); if (tt < 0) out.push({ type: 'segment', from: foot, to: u, dashed: true, color: 'gray' }); if (tt > 1) out.push({ type: 'segment', from: v, to: foot, dashed: true, color: 'gray' }); out.push({ type: 'segment', from: alt, to: foot, dashed: true, color: 'green', label: labels.h }); out.push({ type: 'rightAngle', vertex: foot, from: alt, to: v }); } return out; } export function expandQuad(item: QuadItem, idx: number, points: Record, layout: Layout): PlaneItem[] { const [A, B, C, D] = item.names; const labels = item.labels ?? {}; const style = { color: item.color }; const lab = (key: string, value?: number) => labels[key] ?? (value !== undefined ? num(value) : undefined); const out: PlaneItem[] = []; const seg = (from: Ref, to: Ref, label?: string, extra: Partial> = {}) => { out.push({ type: 'segment', from, to, label, ...style, ...extra }); }; // ضلع‌هایی که برچسب یا علامت نگرفتند، بعداً با همان ضخامت کشیده می‌شوند const edges = () => { const drawn = new Set(out.filter((i) => i.type === 'segment' && !i.dashed).map((i) => { const s = i as { from: Ref; to: Ref }; return [s.from, s.to].sort().join(); })); [[A, B], [B, C], [C, D], [D, A]].forEach(([p, q]) => { if (!drawn.has([p, q].sort().join())) seg(p, q); }); }; // بی‌هیچ اندازه و روی چهار نقطه‌ای که مدل خودش تعریف کرده: همان نقطه‌ها const keep = ['w', 'h', 'side', 'top', 'd1', 'd2', 'angle'].every((k) => item[k as keyof QuadItem] === undefined) && item.names.every((n) => n in points); let T: (p: XY) => XY = (p) => p; const put = (xy: XY[]) => { if (keep) { layout.placed.push(...item.names.map((n) => points[n]).filter((v): v is XY => Array.isArray(v))); return; } T = placement(item.names, xy, points, layout, item.at); [A, B, C, D].forEach((n, i) => { points[n] = T(xy[i]); }); }; const height = (label?: string) => { const foot = `_q${idx}`; points[foot] = { foot: [D, A, B] }; out.push({ type: 'segment', from: D, to: foot, dashed: true, color: 'green', label }); out.push({ type: 'rightAngle', vertex: foot, from: D, to: B }); }; const θ = (item.angle ?? 60) * RAD; switch (item.shape) { case 'rectangle': { const w = item.w ?? 6, h = item.h ?? 4; put([[0, 0], [w, 0], [w, h], [0, h]]); out.push({ type: 'polygon', points: [A, B, C, D], fill: item.fill ?? true, ...style }, { type: 'rightAngle', vertex: A, from: B, to: D }); seg(A, B, lab(A + B, item.w)); seg(B, C, lab(B + C, item.h)); break; } case 'square': { const s = item.side ?? item.w ?? 4; put([[0, 0], [s, 0], [s, s], [0, s]]); out.push({ type: 'polygon', points: [A, B, C, D], fill: item.fill ?? true, ...style }, { type: 'rightAngle', vertex: A, from: B, to: D }); seg(A, B, lab(A + B, item.side ?? item.w), { ticks: 1 }); seg(B, C, lab(B + C), { ticks: 1 }); seg(C, D, lab(C + D), { ticks: 1 }); seg(D, A, lab(D + A), { ticks: 1 }); break; } case 'parallelogram': { const w = item.w ?? 6; const s = item.side ?? (item.h ? item.h / Math.sin(θ) : 3.5); const d: XY = [s * Math.cos(θ), s * Math.sin(θ)]; put([[0, 0], [w, 0], [w + d[0], d[1]], d]); out.push({ type: 'polygon', points: [A, B, C, D], fill: item.fill ?? true, ...style }); seg(A, B, lab(A + B, item.w)); seg(A, D, lab(A + D, item.side)); if (item.h !== undefined || labels.h) height(lab('h', item.h)); if (item.angle !== undefined) out.push({ type: 'angle', vertex: A, from: B, to: D, label: `${num(item.angle)}°`, color: 'red' }); break; } case 'rhombus': { if (item.d1 !== undefined || item.d2 !== undefined) { const d1 = item.d1 ?? 6, d2 = item.d2 ?? 4, o = `_o${idx}`; put([[-d1 / 2, 0], [0, -d2 / 2], [d1 / 2, 0], [0, d2 / 2]]); points[o] = T([0, 0]); out.push({ type: 'polygon', points: [A, B, C, D], fill: item.fill ?? true, ...style }); // برچسبِ قطرها کنارِ هم نمی‌نشیند (هر دو وسطشان در O است): روی یک‌چهارمِ هر قطر seg(A, C, undefined, { dashed: true }); seg(B, D, undefined, { dashed: true }); const l1 = lab('d1', item.d1), l2 = lab('d2', item.d2); if (l1) out.push({ type: 'text', at: T([d1 / 4, d2 * 0.07]), text: l1, color: 'blue' }); if (l2) out.push({ type: 'text', at: T([d1 * 0.07, d2 / 4]), text: l2, color: 'blue' }); out.push({ type: 'rightAngle', vertex: o, from: C, to: D }); [[A, B], [B, C], [C, D], [D, A]].forEach(([p, q]) => seg(p, q, lab(p + q), { ticks: 1 })); } else { const s = item.side ?? item.w ?? 4; put([[0, 0], [s, 0], [s + s * Math.cos(θ), s * Math.sin(θ)], [s * Math.cos(θ), s * Math.sin(θ)]]); out.push({ type: 'polygon', points: [A, B, C, D], fill: item.fill ?? true, ...style }); seg(A, B, lab(A + B, item.side ?? item.w), { ticks: 1 }); [[B, C], [C, D], [D, A]].forEach(([p, q]) => seg(p, q, lab(p + q), { ticks: 1 })); if (item.h !== undefined || labels.h) height(lab('h', item.h)); if (item.angle !== undefined) out.push({ type: 'angle', vertex: A, from: B, to: D, label: `${num(item.angle)}°`, color: 'red' }); } break; } case 'trapezoid': { const w = item.w ?? 6, t = item.top ?? w * 0.6, h = item.h ?? 3; const x0 = item.right ? 0 : (w - t) / 2; put([[0, 0], [w, 0], [x0 + t, h], [x0, h]]); out.push({ type: 'polygon', points: [A, B, C, D], fill: item.fill ?? true, ...style }); seg(A, B, lab(A + B, item.w)); seg(D, C, lab(D + C, item.top)); if (item.right) { out.push({ type: 'rightAngle', vertex: A, from: B, to: D }); seg(A, D, lab(A + D, item.h) ?? lab('h')); } else if (item.h !== undefined || labels.h) height(lab('h', item.h)); break; } } edges(); return out; }