From dffa940f8a1394877d536b93658aed646e8291a4 Mon Sep 17 00:00:00 2001 From: AliReza Date: Tue, 29 Sep 2026 02:37:50 +0330 Subject: [PATCH] diagram kit: lines from their written equation, regular polygons, exterior angles; hide unused points Found in a real chat with gpt-4.1-mini: the model writes a line's coefficients as ax + by + c = 0, so eq now means that and its own earlier outputs render right; it can also just copy the equation text (3x + 5y = 4, y = 12x - 16). regularPolygon builds the vertices (the model's own pentagon was not regular), exteriorAngle draws the extension of the side (the model always drew the interior angle). Named points no item refers to are no longer drawn, and a computed root or vertex skips its label when the model already put a point there. Co-Authored-By: Claude Opus 5.5 --- src/diagram/plane.tsx | 96 ++++++++++++++++++++++++++++++++++++++++--- src/diagram/types.ts | 4 +- 2 files changed, 93 insertions(+), 7 deletions(-) diff --git a/src/diagram/plane.tsx b/src/diagram/plane.tsx index 90cb50f..81f1f8e 100644 --- a/src/diagram/plane.tsx +++ b/src/diagram/plane.tsx @@ -166,7 +166,51 @@ function RichLabel({ x, y, text, fill, size = 11, anchor = 'middle' }: { x: numb /* ─── خودِ شکل ───────────────────────────────────────────────────────────── */ -export function Plane({ spec }: { spec: PlaneSpec }) { +/** + * قطعه‌هایی که به قطعه‌های ساده‌تر باز می‌شوند، پیش از هر حسابی: + * regularPolygon → نقطه‌های polar با زاویه‌های مساوی + polygon (مدل مختصاتِ رأسِ پنج‌ضلعیِ + * منتظم را خودش حدس زده بود و شکل منتظم نشد) + * exteriorAngle → نقطه‌ی پنهان روی امتدادِ ضلع + پاره‌خطِ خط‌چین + angle (مدل با angle همیشه + * زاویه‌ی داخلی را می‌کشید) + */ +function expand(spec: PlaneSpec): PlaneSpec { + if (!spec.items.some((i) => i.type === 'regularPolygon' || i.type === 'exteriorAngle')) return spec; + const points: Record = { ...spec.points }; + const items: PlaneItem[] = []; + spec.items.forEach((item, idx) => { + if (item.type === 'regularPolygon') { + const center = item.center ?? '_rc' + idx; + if (!item.center) points['_rc' + idx] = [0, 0]; + // پیش‌فرض: ضلعِ پایین افقی، مثلِ کتاب + const start = item.start ?? -90 - 180 / item.n; + const names = Array.from({ length: item.n }, (_, i) => item.names?.[i] ?? `_r${idx}_${i}`); + names.forEach((name, i) => { points[name] = { polar: [item.r, start + (360 * i) / item.n], from: center }; }); + items.push({ type: 'polygon', points: names, fill: item.fill, color: item.color, dashed: item.dashed, label: item.label }); + } else if (item.type === 'exteriorAngle') { + const ext = `_ext${idx}`; + points[ext] = { along: [item.from, item.vertex, 1.7] }; + items.push({ type: 'segment', from: item.vertex, to: ext, dashed: true, color: 'gray' }); + items.push({ type: 'angle', vertex: item.vertex, from: ext, to: item.to, label: item.label, color: item.color }); + } else items.push(item); + }); + return { ...spec, points, items }; +} + +/** نام‌هایی که یک item یا تعریفِ نقطه‌ی دیگری به آن‌ها ارجاع داده. */ +function referencedNames(spec: PlaneSpec): Set { + const out = new Set(); + const walk = (v: unknown) => { + if (typeof v === 'string') { if (v in spec.points) out.add(v); } + else if (Array.isArray(v)) v.forEach(walk); + else if (v && typeof v === 'object') Object.values(v).forEach(walk); + }; + spec.items.forEach(walk); + Object.values(spec.points).forEach(walk); + return out; +} + +export function Plane({ spec: raw }: { spec: PlaneSpec }) { + const spec = expand(raw); // شناسه‌ی یکتا: چند شکل در یک گفتگو نباید clipPathِ همدیگر را بگیرند. const clipId = `dg-plot-${useId().replace(/:/g, '')}`; const { named, ref } = resolvePoints(spec); @@ -248,7 +292,7 @@ export function Plane({ spec }: { spec: PlaneSpec }) { } case 'segment': case 'arrow': case 'vector': add(ref(item.from)); add(ref(item.to)); break; case 'ray': add(ref(item.from)); add(ref(item.through)); break; - case 'line': { const [a, b] = lineEnds(item, ref); if (!item.eq) { add(a); add(b); } else if (a) add(a); break; } + case 'line': { const [a, b] = lineEnds(item, ref); if (!item.eq && !item.equation) { add(a); add(b); } else if (a) add(a); break; } case 'polygon': item.points.forEach((p) => add(ref(p))); break; case 'point': add(ref(item.point)); break; case 'text': add(item.at); break; @@ -578,7 +622,10 @@ export function Plane({ spec }: { spec: PlaneSpec }) { // بیش از چهار نقطه از یک نوع (ریشه‌های سینوس) فقط نقطه می‌گیرند، نه برچسب؛ وگرنه روی هم می‌افتند. const kindCount = new Map(); markPts.forEach((m) => kindCount.set(m.kind === 'max' || m.kind === 'min' ? 'ext' : m.kind, (kindCount.get(m.kind === 'max' || m.kind === 'min' ? 'ext' : m.kind) ?? 0) + 1)); + // اگر مدل خودش روی همان ریشه یا رأس نقطه‌ی نام‌دار گذاشته، برچسبِ ما تکرار نشود + const namedXY = [...named.values()]; for (const m of markPts) { + if (namedXY.some((q) => Math.abs(q[0] - m.xy[0]) < 1e-3 && Math.abs(q[1] - m.xy[1]) < 1e-3)) continue; const [px, py] = P(m.xy); if (px < 0 || px > W || py < 0 || py > H) continue; layers.points.push(); @@ -609,8 +656,13 @@ export function Plane({ spec }: { spec: PlaneSpec }) { if (label) { const [lx, ly] = away(xy, 14); layers.labels.push(); } }; const imageNames = new Set(images.flatMap((im) => im.names)); + // نقطه‌ای که هیچ تکه‌ای به آن ارجاع نداده رسم نمی‌شود (مدل هفت نقطه‌ی امتداد تعریف کرده + // بود که هیچ‌جا به کار نرفتند و شکل را شلوغ کردند) — مگر شکل فقط از نقطه ساخته شده باشد. + const used = referencedNames(spec); + const onlyPoints = spec.items.length === 0; named.forEach((xy, name) => { if (name.startsWith('_') || imageNames.has(name)) return; + if (!onlyPoints && !used.has(name)) return; const it = explicit.get(name); // مبدأ روی محور خودش «۰» دارد؛ فقط اگر مدل صریحاً نقطه‌اش را خواسته باشد برچسب می‌گیرد. if (spec.axes && !it && Math.abs(xy[0]) < 1e-9 && Math.abs(xy[1]) < 1e-9) return; @@ -634,15 +686,47 @@ export function Plane({ spec }: { spec: PlaneSpec }) { * شیب را کد حساب می‌کند، چون مدل در بازنویسیِ 3x + 5y = 4 به y = … علامت را گم کرده بود). */ function lineEnds(item: Extract, ref: (r: Ref) => XY | null): [XY | null, XY | null] { - if (item.eq) { - const [a, b, c] = item.eq; - if (Math.abs(b) > 1e-12) return [[0, c / b], [1, (c - a) / b]]; - if (Math.abs(a) > 1e-12) return [[c / a, 0], [c / a, 1]]; + const coef = item.equation ? parseLinear(item.equation) : item.eq ?? null; + if (coef) { + // ax + by + c = 0 + const [a, b, c] = coef; + if (Math.abs(b) > 1e-12) return [[0, -c / b], [1, (-c - a) / b]]; + if (Math.abs(a) > 1e-12) return [[-c / a, 0], [-c / a, 1]]; return [null, null]; } return item.through ? [ref(item.through[0]), ref(item.through[1])] : [null, null]; } +/** + * «3x + 5y = 4»، «y = 12x − 16»، «2y = x + 1»، «x = 2.5» → [a, b, c] با ax + by + c = 0. + * هر چیزِ غیرخطی (x²، sin) null است و خط کشیده نمی‌شود. + */ +export function parseLinear(src: string): [number, number, number] | null { + const text = toLatin(src).replace(/\s+/g, '').replace(/[−–]/g, '-').replace(/\*/g, ''); + const sides = text.split('='); + if (sides.length !== 2) return null; + const side = (t: string): [number, number, number] | null => { + if (!t) return null; + const terms = t.replace(/-/g, '+-').split('+').filter(Boolean); + const acc: [number, number, number] = [0, 0, 0]; + for (const term of terms) { + const m = /^(-?)(\d*\.?\d*)(?:\/(\d+\.?\d*))?([xy]?)$/.exec(term); + if (!m || (!m[2] && !m[4])) return null; + let v = m[2] ? Number(m[2]) : 1; + if (m[3]) v /= Number(m[3]); + if (m[1]) v = -v; + acc[m[4] === 'x' ? 0 : m[4] === 'y' ? 1 : 2] += v; + } + return acc; + }; + const l = side(sides[0]), r = side(sides[1]); + if (!l || !r) return null; + const out: [number, number, number] = [l[0] - r[0], l[1] - r[1], l[2] - r[2]]; + return Math.abs(out[0]) + Math.abs(out[1]) > 1e-12 ? out : null; +} + +const toLatin = (s: string) => s.replace(/[۰-۹]/g, (d) => String('۰۱۲۳۴۵۶۷۸۹'.indexOf(d))).replace(/[٠-٩]/g, (d) => String('٠١٢٣٤٥٦٧٨٩'.indexOf(d))); + /** علامتِ تساویِ ضلع: یک، دو یا سه خطِ کوچک عمود بر وسطِ پاره‌خط. */ function Ticks({ a, b, n, col }: { a: XY; b: XY; n: number; col: string }) { const [mx, my] = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; diff --git a/src/diagram/types.ts b/src/diagram/types.ts index 6c7b08f..afe5a99 100644 --- a/src/diagram/types.ts +++ b/src/diagram/types.ts @@ -38,7 +38,7 @@ export type PlaneItem = | ({ type: 'point'; point: Ref; open?: boolean } & Style) | ({ type: 'segment'; from: Ref; to: Ref; ticks?: number } & Style) | ({ type: 'arrow' | 'vector'; from: Ref; to: Ref } & Style) - | ({ type: 'line'; through?: [Ref, Ref]; /** ax + by = c */ eq?: [number, number, number] } & Style) + | ({ type: 'line'; through?: [Ref, Ref]; /** «3x + 5y = 4»، همان‌طور که در مسئله نوشته شده */ equation?: string; /** ax + by + c = 0 */ eq?: [number, number, number] } & Style) | ({ type: 'ray'; from: Ref; through: Ref } & Style) | ({ type: 'vline'; x: number } & Style) | ({ type: 'hline'; y: number } & Style) @@ -47,6 +47,8 @@ export type PlaneItem = | ({ type: 'angle'; vertex: Ref; from: Ref; to: Ref; arcs?: number } & Style) | ({ type: 'rightAngle'; vertex: Ref; from: Ref; to: Ref } & Style) | ({ type: 'polygon'; points: Ref[]; fill?: boolean } & Style) + | ({ type: 'regularPolygon'; n: number; r: number; center?: Ref; start?: number; names?: string[]; fill?: boolean } & Style) + | ({ type: 'exteriorAngle'; vertex: Ref; from: Ref; to: Ref } & Style) | ({ type: 'function'; expr: string; from: number; to: number; marks?: FunctionMark[] } & Style) | ({ type: 'piecewise'; pieces: { expr: string; from: number; to: number; closedFrom?: boolean; closedTo?: boolean }[] } & Style) | ({ type: 'tangent'; expr: string; at: number } & Style)