1use crate::geometry2d::{distance as dist, point_segment_distance};
10
11use serde_json::{Map, Value};
12
13use super::doc::{SketchDoc, SketchGeometry, SketchPoint};
14
15const CLOSED_GAP_FRAC: f64 = 0.15;
18const LINE_DEV_FRAC: f64 = 0.02;
21const CIRCLE_RESIDUAL_FRAC: f64 = 0.08;
24const MIN_RADIUS_FRAC: f64 = 0.02;
27
28#[derive(Clone, Debug, PartialEq)]
32pub enum HandDrawShape {
33 Line { a: (f64, f64), b: (f64, f64) },
35 Circle { center: (f64, f64), rim: (f64, f64) },
37 Arc {
41 center: (f64, f64),
42 start: (f64, f64),
43 end: (f64, f64),
44 },
45 Bezier { controls: [(f64, f64); 4] },
47}
48
49impl HandDrawShape {
50 pub fn kind(&self) -> &'static str {
52 match self {
53 HandDrawShape::Line { .. } => "line",
54 HandDrawShape::Circle { .. } => "circle",
55 HandDrawShape::Arc { .. } => "arc",
56 HandDrawShape::Bezier { .. } => "bezier",
57 }
58 }
59}
60
61pub fn recognize(stroke: &[(f64, f64)]) -> HandDrawShape {
65 let n = stroke.len();
66 if n < 2 {
67 let p = stroke.first().copied().unwrap_or((0.0, 0.0));
68 return HandDrawShape::Line { a: p, b: p };
69 }
70 let a = stroke[0];
71 let b = stroke[n - 1];
72 let extent = stroke_extent(stroke).max(1e-9);
73 let closed = dist(a, b) <= CLOSED_GAP_FRAC * extent;
74
75 if !closed {
78 let max_dev = stroke[1..n - 1]
79 .iter()
80 .map(|&p| point_segment_distance(p, a, b).0)
81 .fold(0.0_f64, f64::max);
82 if n == 2 || max_dev <= LINE_DEV_FRAC * extent {
83 return HandDrawShape::Line { a, b };
84 }
85 }
86
87 if n >= 3 {
90 if let Some((cx, cy, r)) = fit_circle_lsq(stroke) {
91 let residual = stroke
92 .iter()
93 .map(|&p| (dist(p, (cx, cy)) - r).abs())
94 .fold(0.0_f64, f64::max);
95 if r.is_finite()
96 && r > MIN_RADIUS_FRAC * extent
97 && residual <= CIRCLE_RESIDUAL_FRAC * extent
98 {
99 if closed {
100 return HandDrawShape::Circle {
101 center: (cx, cy),
102 rim: (cx + r, cy),
103 };
104 }
105 let (start, end) = if drawn_sweep(stroke, (cx, cy)) < 0.0 {
106 (b, a)
107 } else {
108 (a, b)
109 };
110 return HandDrawShape::Arc {
111 center: (cx, cy),
112 start,
113 end,
114 };
115 }
116 }
117 }
118
119 HandDrawShape::Bezier {
121 controls: fit_cubic(stroke),
122 }
123}
124
125fn drawn_sweep(stroke: &[(f64, f64)], center: (f64, f64)) -> f64 {
130 stroke
131 .windows(2)
132 .map(|pair| {
133 let (ux, uy) = (pair[0].0 - center.0, pair[0].1 - center.1);
134 let (vx, vy) = (pair[1].0 - center.0, pair[1].1 - center.1);
135 (ux * vy - uy * vx).atan2(ux * vx + uy * vy)
136 })
137 .sum()
138}
139
140pub fn emit_shape(doc: &mut SketchDoc, shape: &HandDrawShape, snap_radius: f64) {
146 let base = doc.points.len();
150 match shape {
151 HandDrawShape::Line { a, b } => {
152 let a_id = snap_new_point(doc, base, a.0, a.1, snap_radius);
153 let b_id = snap_new_point(doc, base, b.0, b.1, snap_radius);
154 push_geometry(doc, "line", vec![a_id, b_id], false);
155 }
156 HandDrawShape::Circle { center, rim } => {
157 let c = snap_new_point(doc, base, center.0, center.1, snap_radius);
158 let r = snap_new_point(doc, base, rim.0, rim.1, snap_radius);
159 push_geometry(doc, "circle", vec![c, r], false);
160 }
161 HandDrawShape::Arc { center, start, end } => {
162 let c = snap_new_point(doc, base, center.0, center.1, snap_radius);
163 let s = snap_new_point(doc, base, start.0, start.1, snap_radius);
164 let e = snap_new_point(doc, base, end.0, end.1, snap_radius);
165 push_geometry(doc, "arc", vec![c, s, e], false);
166 }
167 HandDrawShape::Bezier { controls } => {
168 let ids: Vec<Value> = controls
169 .iter()
170 .map(|&(u, v)| snap_new_point(doc, base, u, v, snap_radius))
171 .collect();
172 push_geometry(doc, "bezier", ids.clone(), false);
173 push_geometry(doc, "line", vec![ids[0].clone(), ids[1].clone()], true);
176 push_geometry(doc, "line", vec![ids[3].clone(), ids[2].clone()], true);
177 }
178 }
179}
180
181pub fn stroke_extent(stroke: &[(f64, f64)]) -> f64 {
184 let (mut minx, mut miny) = (f64::INFINITY, f64::INFINITY);
185 let (mut maxx, mut maxy) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
186 for &(x, y) in stroke {
187 minx = minx.min(x);
188 miny = miny.min(y);
189 maxx = maxx.max(x);
190 maxy = maxy.max(y);
191 }
192 if !minx.is_finite() {
193 return 0.0;
194 }
195 ((maxx - minx).powi(2) + (maxy - miny).powi(2)).sqrt()
196}
197
198fn fit_cubic(stroke: &[(f64, f64)]) -> [(f64, f64); 4] {
202 let n = stroke.len();
203 let first = stroke[0];
204 let last = stroke[n - 1];
205 let mut cum = vec![0.0_f64; n];
206 for i in 1..n {
207 cum[i] = cum[i - 1] + dist(stroke[i - 1], stroke[i]);
208 }
209 let total = cum[n - 1];
210 if total < 1e-9 {
211 return [first, first, last, last];
212 }
213 let c1 = sample_arc(stroke, &cum, total, 1.0 / 3.0);
214 let c2 = sample_arc(stroke, &cum, total, 2.0 / 3.0);
215 [first, c1, c2, last]
216}
217
218fn sample_arc(stroke: &[(f64, f64)], cum: &[f64], total: f64, t: f64) -> (f64, f64) {
221 let target = total * t;
222 let mut idx = 0;
223 while idx < cum.len() && cum[idx] < target {
224 idx += 1;
225 }
226 if idx == 0 {
227 return stroke[0];
228 }
229 if idx >= cum.len() {
230 return stroke[stroke.len() - 1];
231 }
232 let (d0, d1) = (cum[idx - 1], cum[idx]);
233 let span = (d1 - d0).max(1e-9);
234 let tt = ((target - d0) / span).clamp(0.0, 1.0);
235 let p0 = stroke[idx - 1];
236 let p1 = stroke[idx];
237 (p0.0 + (p1.0 - p0.0) * tt, p0.1 + (p1.1 - p0.1) * tt)
238}
239
240fn fit_circle_lsq(pts: &[(f64, f64)]) -> Option<(f64, f64, f64)> {
244 let n = pts.len();
245 if n < 3 {
246 return None;
247 }
248 let nf = n as f64;
249 let (mut mx, mut my) = (0.0_f64, 0.0_f64);
250 for &(x, y) in pts {
251 mx += x;
252 my += y;
253 }
254 mx /= nf;
255 my /= nf;
256 let (mut sxx, mut sxy, mut syy) = (0.0_f64, 0.0_f64, 0.0_f64);
258 let (mut sxz, mut syz) = (0.0_f64, 0.0_f64);
259 for &(x, y) in pts {
260 let u = x - mx;
261 let v = y - my;
262 let z = u * u + v * v;
263 sxx += u * u;
264 sxy += u * v;
265 syy += v * v;
266 sxz += u * z;
267 syz += v * z;
268 }
269 let det = sxx * syy - sxy * sxy;
270 if det.abs() < 1e-12 {
271 return None; }
273 let uc = (sxz * syy - syz * sxy) / (2.0 * det);
275 let vc = (sxx * syz - sxy * sxz) / (2.0 * det);
276 let cx = uc + mx;
277 let cy = vc + my;
278 let r = (uc * uc + vc * vc + (sxx + syy) / nf).sqrt();
279 if !cx.is_finite() || !cy.is_finite() || !r.is_finite() {
280 return None;
281 }
282 Some((cx, cy, r))
283}
284
285fn snap_new_point(doc: &mut SketchDoc, base: usize, u: f64, v: f64, radius: f64) -> Value {
290 let mut best: Option<(f64, Value)> = None;
291 for p in doc.points.iter().take(base) {
292 let d = ((p.x - u).powi(2) + (p.y - v).powi(2)).sqrt();
293 if d <= radius && best.as_ref().map_or(true, |(bd, _)| d < *bd) {
294 best = Some((d, p.id.clone()));
295 }
296 }
297 if let Some((_, id)) = best {
298 return id;
299 }
300 let id = doc.next_point_id();
301 doc.points.push(SketchPoint {
302 id: id.clone(),
303 x: u,
304 y: v,
305 fixed: false,
306 construction: false,
307 external_reference: false,
308 });
309 id
310}
311
312fn push_geometry(doc: &mut SketchDoc, geom_type: &str, points: Vec<Value>, construction: bool) {
314 let id = doc.next_geometry_id();
315 let mut extra = Map::new();
316 extra.insert("construction".to_string(), Value::Bool(construction));
317 doc.geometries.push(SketchGeometry {
318 id,
319 geom_type: geom_type.to_string(),
320 points,
321 extra,
322 });
323}
324