use axiolid_core::Point2;
use axiolid_guarantees::Sign;
use num_bigint::BigInt;
use crate::arith::{sign_product, Arith};
use crate::certify::{require_finite, ExactError};
use crate::construct::{Branch, Line};
use crate::dyadic::Dyadic;
use crate::poly::{IntPoly, RealRoot};
use crate::root::Root2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Conic {
coeffs: [Dyadic; 6],
}
fn d(value: f64) -> Dyadic {
Dyadic::from_f64(value)
}
impl Conic {
pub fn from_coefficients(coeffs: [f64; 6]) -> Result<Self, ExactError> {
require_finite(&coeffs)?;
Self::from_dyadic(coeffs.map(d))
}
fn from_dyadic(coeffs: [Dyadic; 6]) -> Result<Self, ExactError> {
if coeffs[..3].iter().all(|c| c.sign() == Some(Sign::Zero)) {
return Err(ExactError::DegenerateConic);
}
Ok(Self { coeffs })
}
pub fn circle(centre: Point2, radius: f64) -> Result<Self, ExactError> {
require_finite(&[centre.x, centre.y, radius])?;
if radius <= 0.0 {
return Err(ExactError::NegativeRadius);
}
let (cx, cy, r) = (d(centre.x), d(centre.y), d(radius));
let two = d(2.0);
Self::from_dyadic([
d(1.0),
Dyadic::zero(),
d(1.0),
two.mul(&cx).neg(),
two.mul(&cy).neg(),
cx.square().add(&cy.square()).sub(&r.square()),
])
}
pub fn ellipse(centre: Point2, axis: Point2, a: f64, b: f64) -> Result<Self, ExactError> {
require_finite(&[centre.x, centre.y, axis.x, axis.y, a, b])?;
if a <= 0.0 || b <= 0.0 {
return Err(ExactError::NegativeRadius);
}
if axis.x == 0.0 && axis.y == 0.0 {
return Err(ExactError::DegenerateLine);
}
let (ux, uy) = (d(axis.x), d(axis.y));
let (a2, b2) = (d(a).square(), d(b).square());
let two = d(2.0);
let qa = b2.mul(&ux.square()).add(&a2.mul(&uy.square()));
let qb = two.mul(&ux).mul(&uy).mul(&b2.sub(&a2));
let qc = b2.mul(&uy.square()).add(&a2.mul(&ux.square()));
let norm2 = ux.square().add(&uy.square());
let rhs = a2.mul(&b2).mul(&norm2);
Self::from_centred(qa, qb, qc, rhs, centre)
}
fn from_centred(
qa: Dyadic,
qb: Dyadic,
qc: Dyadic,
rhs: Dyadic,
centre: Point2,
) -> Result<Self, ExactError> {
let (cx, cy) = (d(centre.x), d(centre.y));
let two = d(2.0);
let dd = two.mul(&qa).mul(&cx).add(&qb.mul(&cy)).neg();
let ee = two.mul(&qc).mul(&cy).add(&qb.mul(&cx)).neg();
let ff = qa
.mul(&cx.square())
.add(&qb.mul(&cx).mul(&cy))
.add(&qc.mul(&cy.square()))
.sub(&rhs);
Self::from_dyadic([qa, qb, qc, dd, ee, ff])
}
#[must_use]
pub fn coefficients(&self) -> &[Dyadic; 6] {
&self.coeffs
}
#[must_use]
pub fn eval(&self, p: Point2) -> Dyadic {
let (x, y) = (d(p.x), d(p.y));
let [a, b, c, dd, e, f] = &self.coeffs;
a.mul(&x.square())
.add(&b.mul(&x).mul(&y))
.add(&c.mul(&y.square()))
.add(&dd.mul(&x))
.add(&e.mul(&y))
.add(f)
}
fn integer(&self) -> [BigInt; 6] {
let poly = IntPoly::from_dyadic(&self.coeffs);
let mut out: [BigInt; 6] = Default::default();
for (slot, c) in out.iter_mut().zip(poly.coeffs()) {
*slot = c.clone();
}
out
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConicLineHits {
None,
Tangent(ConicLineHit),
Secant(ConicLineHit, ConicLineHit),
Single(ConicLineHit),
OnConic,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConicLineHit {
line: Line,
t: Root2<Dyadic>,
}
fn along(line: Line, conic: &Conic) -> (Dyadic, Dyadic, Dyadic) {
let (fx, fy) = (d(line.from().x), d(line.from().y));
let (dx, dy) = (d(line.to().x).sub(&fx), d(line.to().y).sub(&fy));
let [a, b, c, dd, e, _] = &conic.coeffs;
let two = d(2.0);
let alpha = a
.mul(&dx.square())
.add(&b.mul(&dx).mul(&dy))
.add(&c.mul(&dy.square()));
let two_beta = two
.mul(a)
.mul(&fx)
.mul(&dx)
.add(&b.mul(&fx.mul(&dy).add(&fy.mul(&dx))))
.add(&two.mul(c).mul(&fy).mul(&dy))
.add(&dd.mul(&dx))
.add(&e.mul(&dy));
let gamma = conic.eval(line.from());
(two.mul(&alpha), two_beta, two.mul(&gamma))
}
pub fn line_conic_hits(line: Line, conic: &Conic) -> Result<ConicLineHits, ExactError> {
let (al, be, ga) = along(line, conic);
let exact = |x: &Dyadic| x.sign().expect("exact");
if exact(&al) == Sign::Zero {
return Ok(match (exact(&be), exact(&ga)) {
(Sign::Zero, Sign::Zero) => ConicLineHits::OnConic,
(Sign::Zero, _) => ConicLineHits::None,
_ => ConicLineHits::Single(ConicLineHit {
line,
t: Root2 {
a: ga.neg(),
b: Dyadic::zero(),
c: Dyadic::zero(),
d: d(2.0).mul(&be),
},
}),
});
}
let disc = be.square().sub(&al.mul(&ga));
let hit = |branch: Branch| ConicLineHit {
line,
t: Root2 {
a: be.neg(),
b: match branch {
Branch::Minus => d(-1.0),
Branch::Plus => d(1.0),
},
c: disc.clone(),
d: al.clone(),
},
};
Ok(match exact(&disc) {
Sign::Negative => ConicLineHits::None,
Sign::Zero => ConicLineHits::Tangent(hit(Branch::Minus)),
_ => {
let (first, second) = if exact(&al) == Sign::Positive {
(Branch::Minus, Branch::Plus)
} else {
(Branch::Plus, Branch::Minus)
};
ConicLineHits::Secant(hit(first), hit(second))
}
})
}
impl ConicLineHit {
#[must_use]
pub const fn line(&self) -> Line {
self.line
}
pub fn cmp_param(&self, value: f64) -> Result<Sign, ExactError> {
require_finite(&[value])?;
let point = Root2 {
a: d(value),
b: Dyadic::zero(),
c: Dyadic::zero(),
d: d(1.0),
};
self.t.cmp_sign(&point).ok_or(ExactError::Undefined)
}
pub fn compare_along(&self, other: &Self) -> Result<Sign, ExactError> {
if self.line != other.line {
return Err(ExactError::DifferentLines);
}
self.t.cmp_sign(&other.t).ok_or(ExactError::Undefined)
}
#[must_use]
pub fn approx_point(&self) -> Point2 {
let t = approx_root2(&self.t);
let (from, to) = (self.line.from(), self.line.to());
Point2::new(from.x + t * (to.x - from.x), from.y + t * (to.y - from.y))
}
}
fn approx_root2(r: &Root2<Dyadic>) -> f64 {
let root = r.c.to_f64().max(0.0).sqrt();
(r.a.to_f64() + r.b.to_f64() * root) / r.d.to_f64()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConicIntersection {
Points(Vec<ConicPoint>),
Overlapping,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConicPoint {
u: RealRoot,
shear: i64,
y_num: IntPoly,
x_num: IntPoly,
den: IntPoly,
tangent: bool,
}
fn ip(c: &[i64]) -> IntPoly {
IntPoly::new(c.iter().map(|&v| BigInt::from(v)).collect())
}
fn pconst(c: &BigInt) -> IntPoly {
IntPoly::new(vec![c.clone()])
}
fn padd(a: &IntPoly, b: &IntPoly) -> IntPoly {
let n = a.coeffs().len().max(b.coeffs().len());
let get = |p: &IntPoly, i: usize| p.coeffs().get(i).cloned().unwrap_or_default();
IntPoly::new((0..n).map(|i| get(a, i) + get(b, i)).collect())
}
fn pneg(a: &IntPoly) -> IntPoly {
IntPoly::new(a.coeffs().iter().map(|c| -c).collect())
}
fn psub(a: &IntPoly, b: &IntPoly) -> IntPoly {
padd(a, &pneg(b))
}
fn pmul(a: &IntPoly, b: &IntPoly) -> IntPoly {
if a.is_zero() || b.is_zero() {
return IntPoly::new(vec![]);
}
let mut out = vec![BigInt::from(0); a.coeffs().len() + b.coeffs().len() - 1];
for (i, x) in a.coeffs().iter().enumerate() {
for (j, y) in b.coeffs().iter().enumerate() {
out[i + j] += x * y;
}
}
IntPoly::new(out)
}
fn sheared(c: &[BigInt; 6], k: i64) -> [IntPoly; 3] {
let [a, b, cc, dd, e, f] = c;
let k = BigInt::from(k);
let q2 = pconst(&(a * &k * &k - b * &k + cc));
let q1 = IntPoly::new(vec![e - dd * &k, b - BigInt::from(2) * a * &k]);
let q0 = IntPoly::new(vec![f.clone(), dd.clone(), a.clone()]);
[q2, q1, q0]
}
fn determinant(mut m: Vec<Vec<BigInt>>) -> BigInt {
let n = m.len();
let mut sign = BigInt::from(1);
let mut prev = BigInt::from(1);
for k in 0..n {
if m[k][k].sign() == num_bigint::Sign::NoSign {
let Some(swap) = (k + 1..n).find(|&r| m[r][k].sign() != num_bigint::Sign::NoSign)
else {
return BigInt::from(0);
};
m.swap(k, swap);
sign = -sign;
}
for i in k + 1..n {
for j in k + 1..n {
let v = &m[i][j] * &m[k][k] - &m[i][k] * &m[k][j];
m[i][j] = v / &prev;
}
}
prev = m[k][k].clone();
}
sign * &m[n - 1][n - 1]
}
fn resultant(p: &IntPoly, n: usize, q: &IntPoly, m: usize) -> BigInt {
let size = n + m;
if size == 0 {
return BigInt::from(1);
}
let coef = |poly: &IntPoly, i: usize| poly.coeffs().get(i).cloned().unwrap_or_default();
let mut rows = Vec::with_capacity(size);
for r in 0..m {
let mut row = vec![BigInt::from(0); size];
for i in 0..=n {
row[r + i] = coef(p, n - i);
}
rows.push(row);
}
for r in 0..n {
let mut row = vec![BigInt::from(0); size];
for i in 0..=m {
row[r + i] = coef(q, m - i);
}
rows.push(row);
}
determinant(rows)
}
fn eliminate(r: &IntPoly, num: &IntPoly, den: &IntPoly) -> IntPoly {
let n = r.degree().unwrap_or(0);
let m = num.degree().unwrap_or(0).max(den.degree().unwrap_or(0));
let values: Vec<BigInt> = (0..=n as i64)
.map(|y| {
let line = psub(&pmul(den, &ip(&[y])), num);
resultant(r, n, &line, m)
})
.collect();
let mut out = IntPoly::new(vec![]);
for (i, v) in values.iter().enumerate() {
let mut term = pconst(&(v * binomial(n, i)));
if (n - i) % 2 == 1 {
term = pneg(&term);
}
for j in 0..=n {
if j != i {
term = pmul(&term, &ip(&[-(j as i64), 1]));
}
}
out = padd(&out, &term);
}
out
}
fn binomial(n: usize, k: usize) -> BigInt {
let mut out = BigInt::from(1);
for i in 0..k {
out = out * BigInt::from(n - i) / BigInt::from(i + 1);
}
out
}
fn combine(terms: &[(Dyadic, &IntPoly)]) -> IntPoly {
let len = terms
.iter()
.map(|(_, p)| p.coeffs().len())
.max()
.unwrap_or(0);
let coeffs: Vec<Dyadic> = (0..len)
.map(|i| {
terms.iter().fold(Dyadic::zero(), |acc, (c, p)| {
let pi = p.coeffs().get(i).cloned().unwrap_or_default();
acc.add(&c.mul(&Dyadic::from_parts(pi, 0)))
})
})
.collect();
IntPoly::from_dyadic(&coeffs)
}
pub fn conic_intersections(first: &Conic, second: &Conic) -> Result<ConicIntersection, ExactError> {
let (c1, c2) = (first.integer(), second.integer());
for k in 0..=MAX_SHEAR {
let [a2, a1, a0] = sheared(&c1, k);
let [b2, b1, b0] = sheared(&c2, k);
if a2.is_zero() || b2.is_zero() {
continue;
}
let n = psub(&pmul(&a2, &b0), &pmul(&a0, &b2));
let den = psub(&pmul(&b2, &a1), &pmul(&a2, &b1));
let res = psub(
&pmul(&n, &n),
&pmul(
&psub(&pmul(&a2, &b1), &pmul(&a1, &b2)),
&psub(&pmul(&a1, &b0), &pmul(&a0, &b1)),
),
);
if res.is_zero() {
return Ok(ConicIntersection::Overlapping);
}
if res.degree() == Some(0) {
return Ok(ConicIntersection::Points(Vec::new()));
}
let sf = res.square_free();
let shared = sf.gcd(&den);
if shared.degree().unwrap_or(0) >= 1 && !shared.real_roots().is_empty() {
continue;
}
let doubled = res.gcd(&res.derivative());
let sf = if shared.degree().unwrap_or(0) >= 1 {
sf.exact_div(&shared)
} else {
sf
};
let x_num = psub(&pmul(&ip(&[0, 1]), &den), &pmul(&ip(&[k]), &n));
let points = sf
.real_roots()
.into_iter()
.map(|u| {
let tangent =
doubled.degree().unwrap_or(0) >= 1 && u.sign_of(&doubled) == Sign::Zero;
ConicPoint {
u,
shear: k,
y_num: n.clone(),
x_num: x_num.clone(),
den: den.clone(),
tangent,
}
})
.collect();
return Ok(ConicIntersection::Points(points));
}
Err(ExactError::DegenerateConic)
}
const MAX_SHEAR: i64 = 16;
impl ConicPoint {
#[must_use]
pub fn is_tangent(&self) -> bool {
self.tangent
}
#[must_use]
pub fn sign_of_conic(&self, other: &Conic) -> Sign {
let [a, b, c, dd, e, f] = other.integer();
let (xn, yn, den) = (&self.x_num, &self.y_num, &self.den);
let terms = [
pmul(&pconst(&a), &pmul(xn, xn)),
pmul(&pconst(&b), &pmul(xn, yn)),
pmul(&pconst(&c), &pmul(yn, yn)),
pmul(&pconst(&dd), &pmul(xn, den)),
pmul(&pconst(&e), &pmul(yn, den)),
pmul(&pconst(&f), &pmul(den, den)),
];
let total = terms
.iter()
.fold(IntPoly::new(vec![]), |acc, t| padd(&acc, t));
if total.is_zero() {
return Sign::Zero;
}
self.u.sign_of(&total)
}
#[must_use]
pub fn side_of_line(&self, a: Point2, b: Point2) -> Sign {
let (dx, dy) = (d(b.x).sub(&d(a.x)), d(b.y).sub(&d(a.y)));
let shift = dx.mul(&d(a.y)).sub(&dy.mul(&d(a.x)));
let value = combine(&[
(dx, &self.y_num),
(dy.neg(), &self.x_num),
(shift.neg(), &self.den),
]);
if value.is_zero() {
return Sign::Zero;
}
sign_product(self.u.sign_of(&value), self.u.sign_of(&self.den))
}
#[must_use]
pub fn x(&self) -> RealRoot {
self.coordinate(&self.x_num)
}
#[must_use]
pub fn y(&self) -> RealRoot {
self.coordinate(&self.y_num)
}
fn coordinate(&self, num: &IntPoly) -> RealRoot {
let eliminant = eliminate(self.u.poly(), num, &self.den);
let sd = self.u.sign_of(&self.den);
let side = |y0: &Dyadic| {
let shifted = IntPoly::from_dyadic(
&(0..num.coeffs().len().max(self.den.coeffs().len()))
.map(|i| {
let n = num.coeffs().get(i).cloned().unwrap_or_default();
let dd = self.den.coeffs().get(i).cloned().unwrap_or_default();
Dyadic::from_parts(n, 0).sub(&y0.mul(&Dyadic::from_parts(dd, 0)))
})
.collect::<Vec<_>>(),
);
if shifted.is_zero() {
return Sign::Zero;
}
sign_product(self.u.sign_of(&shifted), sd)
};
eliminant
.real_roots()
.into_iter()
.find(|candidate| {
let (lo, hi) = candidate.bounds();
if candidate.is_exact() {
side(lo) == Sign::Zero
} else {
side(lo) == Sign::Positive && side(hi) == Sign::Negative
}
})
.expect("the coordinate is a root of its eliminant")
}
#[must_use]
pub fn approx(&self) -> Point2 {
Point2::new(self.x().approx(), self.y().approx())
}
#[must_use]
pub fn shear(&self) -> i64 {
self.shear
}
}