1use std::f64::consts::{PI, TAU};
8
9use geop_core_math::vector::Vector2;
10
11use crate::sketch::{CurveId, CurveKind, PointId, Sketch};
12
13pub use geop_core_math::plain::{
14 P2, add, bounds, cross2 as cross, distance, dot, normalize as unit, scale, sub,
15};
16
17pub fn perp(v: P2) -> P2 {
19 [-v[1], v[0]]
20}
21
22pub fn rotate(v: P2, angle: f64) -> P2 {
24 let (s, c) = angle.sin_cos();
25 [v[0] * c - v[1] * s, v[0] * s + v[1] * c]
26}
27
28pub fn wrap(angle: f64) -> f64 {
30 let a = (angle + PI).rem_euclid(TAU) - PI;
31 if a == -PI { PI } else { a }
32}
33
34pub fn angle_between(a: P2, b: P2) -> f64 {
36 cross(a, b).atan2(dot(a, b))
37}
38
39pub fn swept(previous: f64, from: P2, to: P2) -> f64 {
43 let raw = angle_between(from, to);
44 if previous == 0.0 {
45 raw
46 } else {
47 previous + wrap(raw - previous)
48 }
49}
50
51pub fn reflect(p: P2, a: P2, b: P2) -> P2 {
53 let Some(u) = unit(sub(b, a)) else {
54 return p;
55 };
56 let q = sub(p, a);
57 let along = scale(u, dot(q, u));
58 add(a, sub(scale(along, 2.0), q))
59}
60
61pub fn of(v: &Vector2) -> P2 {
63 [v[0].to_f64(), v[1].to_f64()]
64}
65
66pub fn xy(sketch: &Sketch, p: PointId) -> P2 {
68 of(&sketch.points[&p].xy())
69}
70
71pub fn segments_cross(a: P2, b: P2, c: P2, d: P2) -> Option<P2> {
74 let (u, v) = (sub(b, a), sub(d, c));
75 let denom = cross(u, v);
76 if denom == 0.0 {
77 return None;
78 }
79 let w = sub(c, a);
80 let (s, t) = (cross(w, v) / denom, cross(w, u) / denom);
81 ((0.0..=1.0).contains(&s) && (0.0..=1.0).contains(&t)).then(|| add(a, scale(u, s)))
82}
83
84pub fn sweep_through(s: P2, e: P2, p: P2) -> f64 {
88 let (a, b) = (sub(s, p), sub(e, p));
89 let phi = cross(a, b).atan2(dot(a, b)).abs();
90 let sign = if cross(sub(e, s), sub(p, s)) < 0.0 {
91 1.0
92 } else {
93 -1.0
94 };
95 sign * (TAU - 2.0 * phi)
96}
97
98#[derive(Clone, Copy, Debug, PartialEq)]
101pub enum Plain {
102 Line { a: P2, b: P2, infinite: bool },
104 Round {
107 center: P2,
108 radius: f64,
109 span: Option<(f64, f64)>,
110 },
111}
112
113impl Plain {
114 pub fn of(sketch: &Sketch, curve: CurveId, infinite: bool) -> Option<Plain> {
117 let p = |q: PointId| xy(sketch, q);
118 Some(match sketch.curves.get(&curve)?.kind {
119 CurveKind::Line { start, end } => Plain::Line {
120 a: p(start),
121 b: p(end),
122 infinite,
123 },
124 CurveKind::Circle { center, radius } => Plain::Round {
125 center: p(center),
126 radius: radius.to_f64(),
127 span: None,
128 },
129 CurveKind::Arc { start, end, sweep } => Plain::arc(p(start), p(end), sweep.to_f64())?,
130 CurveKind::Spline { .. } | CurveKind::Text { .. } => return None,
131 })
132 }
133
134 pub fn arc(s: P2, e: P2, sweep: f64) -> Option<Plain> {
137 let chord = sub(e, s);
140 let length = chord[0].hypot(chord[1]);
141 let half = sweep / 2.0;
142 let left = perp(unit(chord)?);
143 let center = add(
144 scale(add(s, e), 0.5),
145 scale(left, length / 2.0 * half.cos() / half.sin()),
146 );
147 let radius = length / (2.0 * half.sin().abs());
148 let from = sub(s, center);
149 (radius.is_finite() && center.iter().all(|v| v.is_finite())).then_some(Plain::Round {
150 center,
151 radius,
152 span: Some((from[1].atan2(from[0]), sweep)),
153 })
154 }
155
156 pub fn full(self) -> Plain {
158 match self {
159 Plain::Line { a, b, .. } => Plain::Line {
160 a,
161 b,
162 infinite: true,
163 },
164 Plain::Round { center, radius, .. } => Plain::Round {
165 center,
166 radius,
167 span: None,
168 },
169 }
170 }
171
172 pub fn closed(&self) -> bool {
174 matches!(self, Plain::Round { span: None, .. })
175 }
176
177 pub fn param(&self, q: P2) -> f64 {
182 match *self {
183 Plain::Line { a, b, .. } => {
184 let d = sub(b, a);
185 dot(sub(q, a), d) / dot(d, d)
186 }
187 Plain::Round {
188 center, span: None, ..
189 } => {
190 let r = sub(q, center);
191 r[1].atan2(r[0]).rem_euclid(TAU) / TAU
192 }
193 Plain::Round {
194 center,
195 span: Some((start, sweep)),
196 ..
197 } => {
198 let r = sub(q, center);
199 ((r[1].atan2(r[0]) - start) * sweep.signum()).rem_euclid(TAU) / sweep.abs()
200 }
201 }
202 }
203
204 pub fn nearest(&self, q: P2) -> f64 {
207 let t = self.param(q);
208 match *self {
209 Plain::Line { .. } => t.clamp(0.0, 1.0),
210 Plain::Round { span: None, .. } => t,
211 Plain::Round {
212 span: Some((_, sweep)),
213 ..
214 } => {
215 let turned = t * sweep.abs();
218 if turned <= sweep.abs() {
219 t
220 } else if turned - sweep.abs() < TAU - turned {
221 1.0
222 } else {
223 0.0
224 }
225 }
226 }
227 }
228
229 pub fn at(&self, t: f64) -> P2 {
231 match *self {
232 Plain::Line { a, b, .. } => add(a, scale(sub(b, a), t)),
233 Plain::Round {
234 center,
235 radius,
236 span,
237 } => {
238 let angle = match span {
239 None => t * TAU,
240 Some((start, sweep)) => start + t * sweep,
241 };
242 add(center, [radius * angle.cos(), radius * angle.sin()])
243 }
244 }
245 }
246
247 pub fn tangent(&self, t: f64) -> P2 {
249 match *self {
250 Plain::Line { a, b, .. } => unit(sub(b, a)).unwrap_or([1.0, 0.0]),
251 Plain::Round { span, .. } => {
252 let (angle, turn) = match span {
253 None => (t * TAU, 1.0),
254 Some((start, sweep)) => (start + t * sweep, sweep.signum()),
255 };
256 scale([-angle.sin(), angle.cos()], turn)
257 }
258 }
259 }
260
261 pub fn part(&self, t0: f64, t1: f64) -> Plain {
264 match *self {
265 Plain::Line { .. } => Plain::Line {
266 a: self.at(t0),
267 b: self.at(t1),
268 infinite: false,
269 },
270 Plain::Round {
271 center,
272 radius,
273 span,
274 } => {
275 let (start, sweep) = span.unwrap_or((0.0, TAU));
276 Plain::Round {
277 center,
278 radius,
279 span: Some((start + t0 * sweep, (t1 - t0) * sweep)),
280 }
281 }
282 }
283 }
284
285 pub fn polyline(&self, t0: f64, t1: f64) -> Vec<P2> {
287 let steps = match self {
288 Plain::Line { .. } => 1,
289 Plain::Round { span, .. } => {
290 let turn = span.map_or(1.0, |(_, sweep)| sweep.abs() / TAU);
291 ((64.0 * turn * (t1 - t0).abs()).ceil() as usize).max(1)
292 }
293 };
294 (0..=steps)
295 .map(|i| self.at(t0 + (t1 - t0) * i as f64 / steps as f64))
296 .collect()
297 }
298
299 pub fn holds(&self, q: P2) -> bool {
301 match *self {
302 Plain::Line { infinite: true, .. } | Plain::Round { span: None, .. } => true,
303 _ => (0.0..=1.0).contains(&self.param(q)),
304 }
305 }
306
307 pub fn crossings(&self, other: &Plain, touching: bool) -> Vec<P2> {
312 let found = match (self, other) {
313 (Plain::Line { a, b, .. }, Plain::Line { a: c, b: d, .. }) => {
314 let (u, v) = (sub(*b, *a), sub(*d, *c));
315 let denom = cross(u, v);
316 if denom == 0.0 {
317 Vec::new()
318 } else {
319 vec![add(*a, scale(u, cross(sub(*c, *a), v) / denom))]
320 }
321 }
322 (Plain::Line { a, b, .. }, Plain::Round { center, radius, .. })
323 | (Plain::Round { center, radius, .. }, Plain::Line { a, b, .. }) => {
324 let u = sub(*b, *a);
326 let f = sub(*a, *center);
327 let (qa, qb, qc) = (dot(u, u), 2.0 * dot(f, u), dot(f, f) - radius * radius);
328 let disc = qb * qb - 4.0 * qa * qc;
329 if qa == 0.0 {
330 Vec::new()
331 } else if touching {
332 vec![add(*a, scale(u, -qb / (2.0 * qa)))]
333 } else if disc < 0.0 {
334 Vec::new()
335 } else {
336 let root = disc.sqrt();
337 [-1.0, 1.0]
338 .map(|sign| add(*a, scale(u, (-qb + sign * root) / (2.0 * qa))))
339 .to_vec()
340 }
341 }
342 (
343 Plain::Round {
344 center: c0,
345 radius: r0,
346 ..
347 },
348 Plain::Round {
349 center: c1,
350 radius: r1,
351 ..
352 },
353 ) => {
354 let d = sub(*c1, *c0);
355 let dist = d[0].hypot(d[1]);
356 if dist == 0.0 || (!touching && (dist > r0 + r1 || dist < (r0 - r1).abs())) {
357 Vec::new()
358 } else {
359 let along = (r0 * r0 - r1 * r1 + dist * dist) / (2.0 * dist);
360 let base = add(*c0, scale(d, along / dist));
361 if touching {
362 vec![base]
363 } else {
364 let off = (r0 * r0 - along * along).max(0.0).sqrt();
365 let n = [-d[1] / dist, d[0] / dist];
366 [-1.0, 1.0]
367 .map(|sign| add(base, scale(n, sign * off)))
368 .to_vec()
369 }
370 }
371 }
372 };
373 found
374 .into_iter()
375 .filter(|&q| self.holds(q) && other.holds(q))
376 .collect()
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[test]
387 fn sweeps_through_a_point() {
388 let s = sweep_through([-1.0, 0.0], [1.0, 0.0], [0.0, 1.0]);
389 assert!((s + PI).abs() < 1e-12, "{s}");
390 let s = sweep_through([-1.0, 0.0], [1.0, 0.0], [0.0, -1.0]);
391 assert!((s - PI).abs() < 1e-12, "{s}");
392 }
393
394 #[test]
395 fn reflections_and_tangents() {
396 let r = reflect([1.0, 2.0], [0.0, 0.0], [1.0, 1.0]);
397 assert!(distance(r, [2.0, 1.0]) < 1e-12, "{r:?}");
398 let q = Plain::arc([1.0, 0.0], [0.0, 1.0], PI / 2.0).unwrap();
401 assert!(distance(q.tangent(0.0), [0.0, 1.0]) < 1e-12);
402 assert!(distance(q.tangent(1.0), [-1.0, 0.0]) < 1e-12);
403 }
404}