mirror of
https://github.com/tiennm99/mathmax.git
synced 2026-09-17 16:20:41 +00:00
fix(goc-noi-tiep): report the subtended arc, not the unsigned central angle
The readout showed the angle AOB, which does not change as M moves, so the page contradicted its own theorem whenever M sat on the minor arc: the inscribed angle read 120 degrees against a central angle of 120. It now shows the measure of the arc that M does not lie on, so the inscribed angle is half of it wherever M is. The inscribed angle is also marked undefined when M reaches either end of the chord instead of reading zero.
This commit is contained in:
@@ -42,3 +42,32 @@ export function angleAtVertex(a, vertex, b) {
|
||||
const cosTheta = Math.max(-1, Math.min(1, dot(va, vb) / (lenA * lenB)));
|
||||
return (Math.acos(cosTheta) * 180) / Math.PI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Angle of `p` as seen from the circle's center, in degrees [0, 360).
|
||||
* @param {Circle} c @param {Vec2} p @returns {number}
|
||||
*/
|
||||
export function angleOnCircle(c, p) {
|
||||
const deg = (Math.atan2(p.y - c.center.y, p.x - c.center.x) * 180) / Math.PI;
|
||||
return ((deg % 360) + 360) % 360;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure in degrees of the arc AB that does **not** contain M — the arc an
|
||||
* inscribed angle at M subtends. Ranges over (0, 360): it exceeds 180° when M
|
||||
* sits on the minor arc, which is exactly the case where the inscribed angle
|
||||
* is not half of the unsigned central angle ∠AOB.
|
||||
*
|
||||
* All three points are assumed to lie on `c`.
|
||||
*
|
||||
* @param {Circle} c @param {Vec2} a @param {Vec2} b @param {Vec2} m
|
||||
* @returns {number}
|
||||
*/
|
||||
export function subtendedArc(c, a, b, m) {
|
||||
const norm = (/** @type {number} */ d) => ((d % 360) + 360) % 360;
|
||||
const angA = angleOnCircle(c, a);
|
||||
const sweepToB = norm(angleOnCircle(c, b) - angA);
|
||||
const sweepToM = norm(angleOnCircle(c, m) - angA);
|
||||
// M inside the A→B sweep means the subtended arc is the complementary one.
|
||||
return sweepToM > 0 && sweepToM < sweepToB ? 360 - sweepToB : sweepToB;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { vec } from './vec.js';
|
||||
import { angleAtVertex, circle, pointOnCircle, projectToCircle } from './circle.js';
|
||||
import { angleAtVertex, circle, pointOnCircle, projectToCircle, subtendedArc } from './circle.js';
|
||||
|
||||
describe('pointOnCircle', () => {
|
||||
it('places 0° at +x', () => {
|
||||
@@ -59,3 +59,46 @@ describe('angleAtVertex', () => {
|
||||
expect(inscribed).toBeCloseTo(central / 2, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subtendedArc', () => {
|
||||
const c = circle(0, 0, 10);
|
||||
const at = (/** @type {number} */ deg) => pointOnCircle(c, deg);
|
||||
|
||||
it('M on the major arc subtends the minor arc', () => {
|
||||
// A at 150°, B at 30° — the minor arc AB measures 120°.
|
||||
expect(subtendedArc(c, at(150), at(30), at(270))).toBeCloseTo(120);
|
||||
});
|
||||
|
||||
it('M on the minor arc subtends the major arc', () => {
|
||||
for (const mDeg of [45, 90, 120]) {
|
||||
expect(subtendedArc(c, at(150), at(30), at(mDeg))).toBeCloseTo(240);
|
||||
}
|
||||
});
|
||||
|
||||
it('swapping A and B gives the same arc', () => {
|
||||
const arcAB = subtendedArc(c, at(150), at(30), at(270));
|
||||
const arcBA = subtendedArc(c, at(30), at(150), at(270));
|
||||
expect(arcBA).toBeCloseTo(arcAB);
|
||||
});
|
||||
|
||||
it('the inscribed angle is half the subtended arc, everywhere on the circle', () => {
|
||||
const a = at(150);
|
||||
const b = at(30);
|
||||
for (let mDeg = 0; mDeg < 360; mDeg += 7) {
|
||||
const m = at(mDeg);
|
||||
// Skip the two positions where the inscribed angle is undefined.
|
||||
if (Math.abs(mDeg - 150) < 1e-9 || Math.abs(mDeg - 30) < 1e-9) continue;
|
||||
expect(angleAtVertex(a, m, b)).toBeCloseTo(subtendedArc(c, a, b, m) / 2, 6);
|
||||
}
|
||||
});
|
||||
|
||||
it('holds for a second, asymmetric chord', () => {
|
||||
const a = at(10);
|
||||
const b = at(200);
|
||||
for (let mDeg = 0; mDeg < 360; mDeg += 11) {
|
||||
const m = at(mDeg);
|
||||
if (Math.abs(mDeg - 10) < 1e-9 || Math.abs(mDeg - 200) < 1e-9) continue;
|
||||
expect(angleAtVertex(a, m, b)).toBeCloseTo(subtendedArc(c, a, b, m) / 2, 6);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,13 +5,15 @@ export const vi = {
|
||||
title: 'Góc nội tiếp',
|
||||
gradeLabel: 'Lớp 9',
|
||||
intro:
|
||||
'Định lý góc nội tiếp nói rằng: khi M chạy trên cùng một cung của đường tròn, góc nội tiếp ∠AMB không đổi và bằng nửa góc ở tâm cùng chắn cung. Hãy kéo điểm M trên đường tròn để tự kiểm chứng.',
|
||||
'Định lý góc nội tiếp nói rằng: khi M chạy trên cùng một cung của đường tròn, góc nội tiếp ∠AMB không đổi và bằng nửa số đo cung bị chắn. Hãy kéo điểm M trên đường tròn để tự kiểm chứng — chú ý số đo cung đổi khi M vượt qua A hoặc B.',
|
||||
instruction: 'Kéo điểm M (đỏ) quanh đường tròn — hoặc dùng phím mũi tên',
|
||||
inscribedLabel: 'Góc nội tiếp ∠AMB',
|
||||
centralLabel: 'Góc ở tâm ∠AOB',
|
||||
arcLabel: 'Cung bị chắn AB',
|
||||
relationOk: 'Góc nội tiếp = nửa cung bị chắn',
|
||||
undefinedLabel: 'Chưa xác định — M đang trùng với A hoặc B',
|
||||
theoremTitle: 'Định lý',
|
||||
theoremStatement:
|
||||
'Trong một đường tròn, số đo của góc nội tiếp bằng nửa số đo của góc ở tâm cùng chắn một cung.',
|
||||
'Trong một đường tròn, số đo của góc nội tiếp bằng nửa số đo của cung bị chắn. Khi M nằm trên cung lớn, cung bị chắn là cung nhỏ AB và góc nội tiếp bằng nửa góc ở tâm ∠AOB; khi M nằm trên cung nhỏ, cung bị chắn là cung lớn (> 180°).',
|
||||
exampleTitle: 'Ví dụ',
|
||||
exampleBody:
|
||||
'Cho đường tròn (O) với hai điểm A, B cố định sao cho góc ở tâm ∠AOB = 120°. Theo định lý, mọi điểm M nằm trên cung lớn AB đều cho ∠AMB = 60° (= 120° / 2). Khi M chuyển sang cung nhỏ, ∠AMB = 120° vì khi đó góc nội tiếp chắn cung lớn (240°), một nửa của nó là 120°. Hãy kéo điểm M để kiểm tra.',
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
import { base } from '$app/paths';
|
||||
import { t } from '$lib/i18n/index.js';
|
||||
import { vi as m } from '$lib/lessons/goc-noi-tiep/copy.vi.js';
|
||||
import { circle, pointOnCircle, projectToCircle, angleAtVertex } from '$lib/geom-engine/circle.js';
|
||||
import {
|
||||
circle,
|
||||
pointOnCircle,
|
||||
projectToCircle,
|
||||
angleAtVertex,
|
||||
subtendedArc,
|
||||
} from '$lib/geom-engine/circle.js';
|
||||
import { dist, EPSILON_LEN } from '$lib/geom-engine/vec.js';
|
||||
import { draggable } from '$lib/actions/draggable.svelte.js';
|
||||
|
||||
const copy = t();
|
||||
@@ -16,7 +23,16 @@
|
||||
let M = $state(pointOnCircle(C, 270));
|
||||
|
||||
const inscribed = $derived(angleAtVertex(A, M, B));
|
||||
const central = $derived(angleAtVertex(A, C.center, B));
|
||||
|
||||
// The arc AB that M does NOT sit on — the arc ∠AMB actually subtends. It is
|
||||
// the major arc (> 180°) whenever M is on the minor arc, which is why the
|
||||
// fixed central angle ∠AOB alone does not describe the relationship.
|
||||
const arc = $derived(subtendedArc(C, A, B, M));
|
||||
|
||||
// ∠AMB degenerates when M reaches either endpoint of the chord.
|
||||
const atEndpoint = $derived(
|
||||
dist(M, A) < EPSILON_LEN || dist(M, B) < EPSILON_LEN
|
||||
);
|
||||
|
||||
/** @param {{x: number, y: number}} p */
|
||||
const projector = (p) => projectToCircle(p, C);
|
||||
@@ -52,9 +68,18 @@
|
||||
<p class="text-slate-700 leading-relaxed">{m.intro}</p>
|
||||
</header>
|
||||
|
||||
<section class="mb-2 flex items-baseline justify-between gap-3 text-sm tabular-nums" aria-live="polite">
|
||||
<span><strong style="color:#D7263D">{m.inscribedLabel}:</strong> {inscribed.toFixed(1)}°</span>
|
||||
<span><strong style="color:#1B998B">{m.centralLabel}:</strong> {central.toFixed(1)}°</span>
|
||||
<section class="mb-2 text-sm tabular-nums" aria-live="polite">
|
||||
{#if atEndpoint}
|
||||
<p class="text-slate-500">{m.undefinedLabel}</p>
|
||||
{:else}
|
||||
<div class="flex items-baseline justify-between gap-3">
|
||||
<span><strong style="color:#D7263D">{m.inscribedLabel}:</strong> {inscribed.toFixed(1)}°</span>
|
||||
<span><strong style="color:#1B998B">{m.arcLabel}:</strong> {arc.toFixed(1)}°</span>
|
||||
</div>
|
||||
<p class="mt-1 text-slate-600">
|
||||
{m.relationOk}: {inscribed.toFixed(1)}° = {arc.toFixed(1)}° / 2
|
||||
</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
|
||||
Reference in New Issue
Block a user