1use serde_json::{Map, Value};
24
25use super::doc::{SketchDoc, SketchGeometry, SketchPoint};
26
27const CLOSED_GAP_FRAC: f64 = 0.15;
30const LINE_DEV_FRAC: f64 = 0.02;
33const CIRCLE_RESIDUAL_FRAC: f64 = 0.08;
36const MIN_RADIUS_FRAC: f64 = 0.02;
39
40#[derive(Clone, Debug, PartialEq)]
44pub enum HandDrawShape {
45 Line { a: (f64, f64), b: (f64, f64) },
47 Circle { center: (f64, f64), rim: (f64, f64) },
49 Arc {
51 center: (f64, f64),
52 start: (f64, f64),
53 end: (f64, f64),
54 },
55 Bezier { controls: [(f64, f64); 4] },
57}
58
59impl HandDrawShape {
60 pub fn kind(&self) -> &'static str {
62 match self {
63 HandDrawShape::Line { .. } => "line",
64 HandDrawShape::Circle { .. } => "circle",
65 HandDrawShape::Arc { .. } => "arc",
66 HandDrawShape::Bezier { .. } => "bezier",
67 }
68 }
69}
70
71pub fn recognize(stroke: &[(f64, f64)]) -> HandDrawShape {
75 let n = stroke.len();
76 if n < 2 {
77 let p = stroke.first().copied().unwrap_or((0.0, 0.0));
78 return HandDrawShape::Line { a: p, b: p };
79 }
80 let a = stroke[0];
81 let b = stroke[n - 1];
82 let extent = stroke_extent(stroke).max(1e-9);
83 let closed = dist(a, b) <= CLOSED_GAP_FRAC * extent;
84
85 if !closed {
88 let max_dev = stroke[1..n - 1]
89 .iter()
90 .map(|&p| point_segment_distance(p, a, b))
91 .fold(0.0_f64, f64::max);
92 if n == 2 || max_dev <= LINE_DEV_FRAC * extent {
93 return HandDrawShape::Line { a, b };
94 }
95 }
96
97 if n >= 3 {
100 if let Some((cx, cy, r)) = fit_circle_lsq(stroke) {
101 let residual = stroke
102 .iter()
103 .map(|&p| (dist(p, (cx, cy)) - r).abs())
104 .fold(0.0_f64, f64::max);
105 if r.is_finite()
106 && r > MIN_RADIUS_FRAC * extent
107 && residual <= CIRCLE_RESIDUAL_FRAC * extent
108 {
109 if closed {
110 return HandDrawShape::Circle {
111 center: (cx, cy),
112 rim: (cx + r, cy),
113 };
114 }
115 return HandDrawShape::Arc {
116 center: (cx, cy),
117 start: a,
118 end: b,
119 };
120 }
121 }
122 }
123
124 HandDrawShape::Bezier {
126 controls: fit_cubic(stroke),
127 }
128}
129
130pub fn emit_shape(doc: &mut SketchDoc, shape: &HandDrawShape, snap_radius: f64) {
136 let base = doc.points.len();
140 match shape {
141 HandDrawShape::Line { a, b } => {
142 let a_id = snap_new_point(doc, base, a.0, a.1, snap_radius);
143 let b_id = snap_new_point(doc, base, b.0, b.1, snap_radius);
144 push_geometry(doc, "line", vec![a_id, b_id], false);
145 }
146 HandDrawShape::Circle { center, rim } => {
147 let c = snap_new_point(doc, base, center.0, center.1, snap_radius);
148 let r = snap_new_point(doc, base, rim.0, rim.1, snap_radius);
149 push_geometry(doc, "circle", vec![c, r], false);
150 }
151 HandDrawShape::Arc { center, start, end } => {
152 let c = snap_new_point(doc, base, center.0, center.1, snap_radius);
153 let s = snap_new_point(doc, base, start.0, start.1, snap_radius);
154 let e = snap_new_point(doc, base, end.0, end.1, snap_radius);
155 push_geometry(doc, "arc", vec![c, s, e], false);
156 }
157 HandDrawShape::Bezier { controls } => {
158 let ids: Vec<Value> = controls
159 .iter()
160 .map(|&(u, v)| snap_new_point(doc, base, u, v, snap_radius))
161 .collect();
162 push_geometry(doc, "bezier", ids.clone(), false);
163 push_geometry(doc, "line", vec![ids[0].clone(), ids[1].clone()], true);
166 push_geometry(doc, "line", vec![ids[3].clone(), ids[2].clone()], true);
167 }
168 }
169}
170
171pub fn stroke_extent(stroke: &[(f64, f64)]) -> f64 {
174 let (mut minx, mut miny) = (f64::INFINITY, f64::INFINITY);
175 let (mut maxx, mut maxy) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
176 for &(x, y) in stroke {
177 minx = minx.min(x);
178 miny = miny.min(y);
179 maxx = maxx.max(x);
180 maxy = maxy.max(y);
181 }
182 if !minx.is_finite() {
183 return 0.0;
184 }
185 ((maxx - minx).powi(2) + (maxy - miny).powi(2)).sqrt()
186}
187
188fn fit_cubic(stroke: &[(f64, f64)]) -> [(f64, f64); 4] {
192 let n = stroke.len();
193 let first = stroke[0];
194 let last = stroke[n - 1];
195 let mut cum = vec![0.0_f64; n];
196 for i in 1..n {
197 cum[i] = cum[i - 1] + dist(stroke[i - 1], stroke[i]);
198 }
199 let total = cum[n - 1];
200 if total < 1e-9 {
201 return [first, first, last, last];
202 }
203 let c1 = sample_arc(stroke, &cum, total, 1.0 / 3.0);
204 let c2 = sample_arc(stroke, &cum, total, 2.0 / 3.0);
205 [first, c1, c2, last]
206}
207
208fn sample_arc(stroke: &[(f64, f64)], cum: &[f64], total: f64, t: f64) -> (f64, f64) {
211 let target = total * t;
212 let mut idx = 0;
213 while idx < cum.len() && cum[idx] < target {
214 idx += 1;
215 }
216 if idx == 0 {
217 return stroke[0];
218 }
219 if idx >= cum.len() {
220 return stroke[stroke.len() - 1];
221 }
222 let (d0, d1) = (cum[idx - 1], cum[idx]);
223 let span = (d1 - d0).max(1e-9);
224 let tt = ((target - d0) / span).clamp(0.0, 1.0);
225 let p0 = stroke[idx - 1];
226 let p1 = stroke[idx];
227 (p0.0 + (p1.0 - p0.0) * tt, p0.1 + (p1.1 - p0.1) * tt)
228}
229
230fn fit_circle_lsq(pts: &[(f64, f64)]) -> Option<(f64, f64, f64)> {
234 let n = pts.len();
235 if n < 3 {
236 return None;
237 }
238 let nf = n as f64;
239 let (mut mx, mut my) = (0.0_f64, 0.0_f64);
240 for &(x, y) in pts {
241 mx += x;
242 my += y;
243 }
244 mx /= nf;
245 my /= nf;
246 let (mut sxx, mut sxy, mut syy) = (0.0_f64, 0.0_f64, 0.0_f64);
248 let (mut sxz, mut syz) = (0.0_f64, 0.0_f64);
249 for &(x, y) in pts {
250 let u = x - mx;
251 let v = y - my;
252 let z = u * u + v * v;
253 sxx += u * u;
254 sxy += u * v;
255 syy += v * v;
256 sxz += u * z;
257 syz += v * z;
258 }
259 let det = sxx * syy - sxy * sxy;
260 if det.abs() < 1e-12 {
261 return None; }
263 let uc = (sxz * syy - syz * sxy) / (2.0 * det);
265 let vc = (sxx * syz - sxy * sxz) / (2.0 * det);
266 let cx = uc + mx;
267 let cy = vc + my;
268 let r = (uc * uc + vc * vc + (sxx + syy) / nf).sqrt();
269 if !cx.is_finite() || !cy.is_finite() || !r.is_finite() {
270 return None;
271 }
272 Some((cx, cy, r))
273}
274
275fn snap_new_point(doc: &mut SketchDoc, base: usize, u: f64, v: f64, radius: f64) -> Value {
280 let mut best: Option<(f64, Value)> = None;
281 for p in doc.points.iter().take(base) {
282 let d = ((p.x - u).powi(2) + (p.y - v).powi(2)).sqrt();
283 if d <= radius && best.as_ref().map_or(true, |(bd, _)| d < *bd) {
284 best = Some((d, p.id.clone()));
285 }
286 }
287 if let Some((_, id)) = best {
288 return id;
289 }
290 let id = doc.next_point_id();
291 doc.points.push(SketchPoint {
292 id: id.clone(),
293 x: u,
294 y: v,
295 fixed: false,
296 construction: false,
297 external_reference: false,
298 });
299 id
300}
301
302fn push_geometry(doc: &mut SketchDoc, geom_type: &str, points: Vec<Value>, construction: bool) {
304 let id = doc.next_geometry_id();
305 let mut extra = Map::new();
306 extra.insert("construction".to_string(), Value::Bool(construction));
307 doc.geometries.push(SketchGeometry {
308 id,
309 geom_type: geom_type.to_string(),
310 points,
311 extra,
312 });
313}
314
315fn dist(a: (f64, f64), b: (f64, f64)) -> f64 {
317 ((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt()
318}
319
320fn point_segment_distance(p: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
322 let (dx, dy) = (b.0 - a.0, b.1 - a.1);
323 let len2 = dx * dx + dy * dy;
324 let t = if len2 <= 1e-18 {
325 0.0
326 } else {
327 (((p.0 - a.0) * dx + (p.1 - a.1) * dy) / len2).clamp(0.0, 1.0)
328 };
329 dist(p, (a.0 + t * dx, a.1 + t * dy))
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::sketch::doc::id_key;
336 use std::f64::consts::{PI, TAU};
337
338 #[test]
341 fn recognize_straight_stroke_is_a_line() {
342 let stroke: Vec<(f64, f64)> = (0..=10).map(|i| (i as f64, 2.0 * i as f64)).collect();
343 match recognize(&stroke) {
344 HandDrawShape::Line { a, b } => {
345 assert_eq!(a, (0.0, 0.0));
346 assert_eq!(b, (10.0, 20.0));
347 }
348 other => panic!("expected line, got {other:?}"),
349 }
350 }
351
352 #[test]
354 fn recognize_two_samples_is_a_line() {
355 assert_eq!(
356 recognize(&[(1.0, 1.0), (5.0, 9.0)]),
357 HandDrawShape::Line {
358 a: (1.0, 1.0),
359 b: (5.0, 9.0)
360 }
361 );
362 }
363
364 #[test]
367 fn recognize_closed_circle() {
368 let (cx, cy, r) = (3.0, -2.0, 5.0);
369 let n = 64;
370 let stroke: Vec<(f64, f64)> = (0..=n)
371 .map(|i| {
372 let t = i as f64 / n as f64 * TAU;
373 (cx + r * t.cos(), cy + r * t.sin())
374 })
375 .collect();
376 match recognize(&stroke) {
377 HandDrawShape::Circle { center, rim } => {
378 assert!((center.0 - cx).abs() < 1e-6 && (center.1 - cy).abs() < 1e-6);
379 assert!((dist(center, rim) - r).abs() < 1e-6);
380 assert!((rim.1 - cy).abs() < 1e-9, "rim should sit on +u");
381 }
382 other => panic!("expected circle, got {other:?}"),
383 }
384 }
385
386 #[test]
389 fn recognize_wobbly_circle() {
390 let (cx, cy, r) = (0.0, 0.0, 10.0);
391 let n = 40;
392 let stroke: Vec<(f64, f64)> = (0..n)
395 .map(|i| {
396 let t = i as f64 / n as f64 * (TAU * 0.96);
397 let rr = r + 0.2 * ((i * 7 % 5) as f64 - 2.0);
398 (cx + rr * t.cos(), cy + rr * t.sin())
399 })
400 .collect();
401 assert_eq!(recognize(&stroke).kind(), "circle");
402 }
403
404 #[test]
407 fn recognize_open_arc() {
408 let (cx, cy, r) = (0.0, 0.0, 4.0);
409 let n = 16;
410 let stroke: Vec<(f64, f64)> = (0..=n)
411 .map(|i| {
412 let t = i as f64 / n as f64 * (PI / 2.0);
413 (cx + r * t.cos(), cy + r * t.sin())
414 })
415 .collect();
416 match recognize(&stroke) {
417 HandDrawShape::Arc { center, start, end } => {
418 assert!((center.0 - cx).abs() < 1e-6 && (center.1 - cy).abs() < 1e-6);
419 assert!((start.0 - r).abs() < 1e-6 && start.1.abs() < 1e-6);
420 assert!(end.0.abs() < 1e-6 && (end.1 - r).abs() < 1e-6);
421 }
422 other => panic!("expected arc, got {other:?}"),
423 }
424 }
425
426 #[test]
429 fn recognize_wiggly_is_bezier() {
430 let stroke: Vec<(f64, f64)> = (0..=40)
432 .map(|i| {
433 let x = i as f64 * 0.5;
434 (x, 3.0 * (x * 0.9).sin())
435 })
436 .collect();
437 match recognize(&stroke) {
438 HandDrawShape::Bezier { controls } => {
439 assert_eq!(controls[0], *stroke.first().unwrap());
440 assert_eq!(controls[3], *stroke.last().unwrap());
441 assert!(controls[1].0 > controls[0].0 && controls[2].0 > controls[1].0);
443 }
444 other => panic!("expected bezier, got {other:?}"),
445 }
446 }
447
448 #[test]
450 fn recognize_zigzag_is_bezier() {
451 let stroke: Vec<(f64, f64)> = (0..=8)
452 .map(|i| (i as f64, if i % 2 == 0 { 0.0 } else { 4.0 }))
453 .collect();
454 assert_eq!(recognize(&stroke).kind(), "bezier");
455 }
456
457 #[test]
460 fn emit_circle_adds_two_points_and_a_circle() {
461 let mut doc = SketchDoc::default();
462 emit_shape(
463 &mut doc,
464 &HandDrawShape::Circle {
465 center: (2.0, 3.0),
466 rim: (7.0, 3.0),
467 },
468 0.5,
469 );
470 assert_eq!(doc.points.len(), 2);
471 assert_eq!(doc.geometries.len(), 1);
472 let g = &doc.geometries[0];
473 assert_eq!(g.geom_type, "circle");
474 assert!(!g.construction());
475 assert_eq!(g.points.len(), 2);
476 }
477
478 #[test]
480 fn emit_line_adds_two_points_and_a_line() {
481 let mut doc = SketchDoc::default();
482 emit_shape(
483 &mut doc,
484 &HandDrawShape::Line {
485 a: (0.0, 0.0),
486 b: (10.0, 0.0),
487 },
488 0.5,
489 );
490 assert_eq!(doc.points.len(), 2);
491 assert_eq!(doc.geometries.len(), 1);
492 assert_eq!(doc.geometries[0].geom_type, "line");
493 }
494
495 #[test]
498 fn emit_bezier_adds_four_points_geometry_and_guides() {
499 let mut doc = SketchDoc::default();
500 emit_shape(
501 &mut doc,
502 &HandDrawShape::Bezier {
503 controls: [(0.0, 0.0), (1.0, 2.0), (3.0, 2.0), (4.0, 0.0)],
504 },
505 0.1,
506 );
507 assert_eq!(doc.points.len(), 4);
508 assert_eq!(doc.geometries.len(), 3);
510 assert_eq!(doc.geometries[0].geom_type, "bezier");
511 assert!(doc.geometries[1].construction() && doc.geometries[2].construction());
512 }
513
514 #[test]
517 fn emit_snaps_endpoint_onto_existing_point() {
518 let mut doc: SketchDoc = serde_json::from_value(serde_json::json!({
519 "points": [{ "id": 42, "x": 0.0, "y": 0.0 }],
520 "geometries": [],
521 "constraints": []
522 }))
523 .unwrap();
524 emit_shape(
526 &mut doc,
527 &HandDrawShape::Line {
528 a: (0.05, 0.0),
529 b: (10.0, 0.0),
530 },
531 0.5,
532 );
533 assert_eq!(doc.points.len(), 2);
535 let line = &doc.geometries[0];
536 assert_eq!(id_key(&line.points[0]), "42");
537 assert_ne!(id_key(&line.points[1]), "42");
538 }
539
540 #[test]
543 fn emit_does_not_collapse_own_points() {
544 let mut doc = SketchDoc::default();
545 emit_shape(
547 &mut doc,
548 &HandDrawShape::Bezier {
549 controls: [(0.0, 0.0), (0.1, 0.0), (0.2, 0.0), (0.3, 0.0)],
550 },
551 5.0,
552 );
553 assert_eq!(doc.points.len(), 4, "own control points must stay distinct");
554 }
555
556 #[test]
557 fn stroke_extent_is_the_bbox_diagonal() {
558 let ext = stroke_extent(&[(0.0, 0.0), (3.0, 0.0), (3.0, 4.0)]);
559 assert!((ext - 5.0).abs() < 1e-9);
560 assert_eq!(stroke_extent(&[]), 0.0);
561 }
562}