mirror of
https://github.com/tiennm99/mathmax.git
synced 2026-09-17 16:20:41 +00:00
Two lines in standard form ax + by = c, with the intersection point as the solution. Standard form rather than the slope-intercept form of linear.js, because it represents vertical lines and the three cases fall out of one determinant. Coefficient sliders plus a drag handle per line that translates it, and presets where the parallel and coincident cases differ only in c.
117 lines
3.9 KiB
JavaScript
117 lines
3.9 KiB
JavaScript
/**
|
|
* Systems of two linear equations in standard form `a·x + b·y = c`.
|
|
*
|
|
* Standard form is used instead of the slope-intercept `y = a·x + b` of
|
|
* `linear.js` because it represents vertical lines and makes the three
|
|
* solution cases fall out of a single determinant.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {import('../geom-engine/vec.js').Vec2} Vec2
|
|
* @typedef {Readonly<{a: number, b: number, c: number}>} StdLine
|
|
* @typedef {{kind: 'unique', point: Vec2}
|
|
* | {kind: 'parallel'}
|
|
* | {kind: 'coincident'}
|
|
* | {kind: 'degenerate'}} SystemSolution
|
|
*/
|
|
|
|
/**
|
|
* Coefficient tolerance. Coefficients are small integers in lesson use, so a
|
|
* tight numeric epsilon is right here — unlike the pixel-scale `EPSILON_LEN`
|
|
* of the geometry engine.
|
|
*/
|
|
export const EPSILON_COEF = 1e-9;
|
|
|
|
/** Geometric tolerance for "is this point on the box edge", in math units. */
|
|
const EPSILON_BOX = 1e-7;
|
|
|
|
/**
|
|
* True when both coefficients vanish, i.e. the equation describes no line.
|
|
* @param {StdLine} line @returns {boolean}
|
|
*/
|
|
export function isDegenerate(line) {
|
|
return Math.abs(line.a) < EPSILON_COEF && Math.abs(line.b) < EPSILON_COEF;
|
|
}
|
|
|
|
/**
|
|
* Solve the system of two standard-form equations.
|
|
*
|
|
* `unique` — the lines cross at one point (determinant ≠ 0).
|
|
* `coincident` — same line, infinitely many solutions.
|
|
* `parallel` — distinct parallel lines, no solution.
|
|
* `degenerate` — an equation has a = b = 0 and is not a line.
|
|
*
|
|
* @param {StdLine} l1 @param {StdLine} l2 @returns {SystemSolution}
|
|
*/
|
|
export function solveSystem(l1, l2) {
|
|
if (isDegenerate(l1) || isDegenerate(l2)) return { kind: 'degenerate' };
|
|
|
|
const det = l1.a * l2.b - l2.a * l1.b;
|
|
if (Math.abs(det) > EPSILON_COEF) {
|
|
return {
|
|
kind: 'unique',
|
|
point: {
|
|
x: (l1.c * l2.b - l2.c * l1.b) / det,
|
|
y: (l1.a * l2.c - l2.a * l1.c) / det,
|
|
},
|
|
};
|
|
}
|
|
|
|
// Determinant is zero: the lines are parallel. They coincide only when the
|
|
// constant terms scale by the same factor as the coefficients.
|
|
const detX = l1.c * l2.b - l2.c * l1.b;
|
|
const detY = l1.a * l2.c - l2.a * l1.c;
|
|
const coincident = Math.abs(detX) < EPSILON_COEF && Math.abs(detY) < EPSILON_COEF;
|
|
return { kind: coincident ? 'coincident' : 'parallel' };
|
|
}
|
|
|
|
/**
|
|
* Clip a standard-form line to an axis-aligned box, returning the two points
|
|
* where it meets the boundary. Returns `null` when the line misses the box,
|
|
* only touches a corner, or the equation is degenerate.
|
|
*
|
|
* @param {StdLine} line
|
|
* @param {number} xMin @param {number} xMax
|
|
* @param {number} yMin @param {number} yMax
|
|
* @returns {[Vec2, Vec2] | null}
|
|
*/
|
|
export function clipToBox(line, xMin, xMax, yMin, yMax) {
|
|
if (isDegenerate(line)) return null;
|
|
const { a, b, c } = line;
|
|
|
|
/** @type {Vec2[]} */
|
|
const hits = [];
|
|
const push = (/** @type {Vec2} */ p) => {
|
|
if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) return;
|
|
if (p.x < xMin - EPSILON_BOX || p.x > xMax + EPSILON_BOX) return;
|
|
if (p.y < yMin - EPSILON_BOX || p.y > yMax + EPSILON_BOX) return;
|
|
const dup = hits.some(
|
|
(q) => Math.abs(q.x - p.x) < EPSILON_BOX && Math.abs(q.y - p.y) < EPSILON_BOX
|
|
);
|
|
if (!dup) hits.push(p);
|
|
};
|
|
|
|
// Vertical edges: solve for y at x = xMin, xMax (needs b ≠ 0).
|
|
if (Math.abs(b) > EPSILON_COEF) {
|
|
push({ x: xMin, y: (c - a * xMin) / b });
|
|
push({ x: xMax, y: (c - a * xMax) / b });
|
|
}
|
|
// Horizontal edges: solve for x at y = yMin, yMax (needs a ≠ 0).
|
|
if (Math.abs(a) > EPSILON_COEF) {
|
|
push({ x: (c - b * yMin) / a, y: yMin });
|
|
push({ x: (c - b * yMax) / a, y: yMax });
|
|
}
|
|
|
|
return hits.length >= 2 ? [hits[0], hits[1]] : null;
|
|
}
|
|
|
|
/**
|
|
* Constant term that moves `line` onto the point `p` without changing its
|
|
* direction — the value of `a·x + b·y` at `p`. Used when a line is dragged.
|
|
*
|
|
* @param {StdLine} line @param {Vec2} p @returns {number}
|
|
*/
|
|
export function constantThrough(line, p) {
|
|
return line.a * p.x + line.b * p.y;
|
|
}
|