mirror of
https://github.com/tiennm99/mathmax.git
synced 2026-09-18 12:21:01 +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)));
|
const cosTheta = Math.max(-1, Math.min(1, dot(va, vb) / (lenA * lenB)));
|
||||||
return (Math.acos(cosTheta) * 180) / Math.PI;
|
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 { describe, expect, it } from 'vitest';
|
||||||
import { vec } from './vec.js';
|
import { vec } from './vec.js';
|
||||||
import { angleAtVertex, circle, pointOnCircle, projectToCircle } from './circle.js';
|
import { angleAtVertex, circle, pointOnCircle, projectToCircle, subtendedArc } from './circle.js';
|
||||||
|
|
||||||
describe('pointOnCircle', () => {
|
describe('pointOnCircle', () => {
|
||||||
it('places 0° at +x', () => {
|
it('places 0° at +x', () => {
|
||||||
@@ -59,3 +59,46 @@ describe('angleAtVertex', () => {
|
|||||||
expect(inscribed).toBeCloseTo(central / 2, 1);
|
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',
|
title: 'Góc nội tiếp',
|
||||||
gradeLabel: 'Lớp 9',
|
gradeLabel: 'Lớp 9',
|
||||||
intro:
|
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',
|
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',
|
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ý',
|
theoremTitle: 'Định lý',
|
||||||
theoremStatement:
|
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ụ',
|
exampleTitle: 'Ví dụ',
|
||||||
exampleBody:
|
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.',
|
'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 { base } from '$app/paths';
|
||||||
import { t } from '$lib/i18n/index.js';
|
import { t } from '$lib/i18n/index.js';
|
||||||
import { vi as m } from '$lib/lessons/goc-noi-tiep/copy.vi.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';
|
import { draggable } from '$lib/actions/draggable.svelte.js';
|
||||||
|
|
||||||
const copy = t();
|
const copy = t();
|
||||||
@@ -16,7 +23,16 @@
|
|||||||
let M = $state(pointOnCircle(C, 270));
|
let M = $state(pointOnCircle(C, 270));
|
||||||
|
|
||||||
const inscribed = $derived(angleAtVertex(A, M, B));
|
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 */
|
/** @param {{x: number, y: number}} p */
|
||||||
const projector = (p) => projectToCircle(p, C);
|
const projector = (p) => projectToCircle(p, C);
|
||||||
@@ -52,9 +68,18 @@
|
|||||||
<p class="text-slate-700 leading-relaxed">{m.intro}</p>
|
<p class="text-slate-700 leading-relaxed">{m.intro}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="mb-2 flex items-baseline justify-between gap-3 text-sm tabular-nums" aria-live="polite">
|
<section class="mb-2 text-sm tabular-nums" aria-live="polite">
|
||||||
<span><strong style="color:#D7263D">{m.inscribedLabel}:</strong> {inscribed.toFixed(1)}°</span>
|
{#if atEndpoint}
|
||||||
<span><strong style="color:#1B998B">{m.centralLabel}:</strong> {central.toFixed(1)}°</span>
|
<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>
|
||||||
|
|
||||||
<section class="mb-6">
|
<section class="mb-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user