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 {
39 center: (f64, f64),
40 start: (f64, f64),
41 end: (f64, f64),
42 },
43 Bezier { controls: [(f64, f64); 4] },
45}
46
47impl HandDrawShape {
48 pub fn kind(&self) -> &'static str {
50 match self {
51 HandDrawShape::Line { .. } => "line",
52 HandDrawShape::Circle { .. } => "circle",
53 HandDrawShape::Arc { .. } => "arc",
54 HandDrawShape::Bezier { .. } => "bezier",
55 }
56 }
57}
58
59pub fn recognize(stroke: &[(f64, f64)]) -> HandDrawShape {
63 let n = stroke.len();
64 if n < 2 {
65 let p = stroke.first().copied().unwrap_or((0.0, 0.0));
66 return HandDrawShape::Line { a: p, b: p };
67 }
68 let a = stroke[0];
69 let b = stroke[n - 1];
70 let extent = stroke_extent(stroke).max(1e-9);
71 let closed = dist(a, b) <= CLOSED_GAP_FRAC * extent;
72
73 if !closed {
76 let max_dev = stroke[1..n - 1]
77 .iter()
78 .map(|&p| point_segment_distance(p, a, b).0)
79 .fold(0.0_f64, f64::max);
80 if n == 2 || max_dev <= LINE_DEV_FRAC * extent {
81 return HandDrawShape::Line { a, b };
82 }
83 }
84
85 if n >= 3 {
88 if let Some((cx, cy, r)) = fit_circle_lsq(stroke) {
89 let residual = stroke
90 .iter()
91 .map(|&p| (dist(p, (cx, cy)) - r).abs())
92 .fold(0.0_f64, f64::max);
93 if r.is_finite()
94 && r > MIN_RADIUS_FRAC * extent
95 && residual <= CIRCLE_RESIDUAL_FRAC * extent
96 {
97 if closed {
98 return HandDrawShape::Circle {
99 center: (cx, cy),
100 rim: (cx + r, cy),
101 };
102 }
103 return HandDrawShape::Arc {
104 center: (cx, cy),
105 start: a,
106 end: b,
107 };
108 }
109 }
110 }
111
112 HandDrawShape::Bezier {
114 controls: fit_cubic(stroke),
115 }
116}
117
118pub fn emit_shape(doc: &mut SketchDoc, shape: &HandDrawShape, snap_radius: f64) {
124 let base = doc.points.len();
128 match shape {
129 HandDrawShape::Line { a, b } => {
130 let a_id = snap_new_point(doc, base, a.0, a.1, snap_radius);
131 let b_id = snap_new_point(doc, base, b.0, b.1, snap_radius);
132 push_geometry(doc, "line", vec![a_id, b_id], false);
133 }
134 HandDrawShape::Circle { center, rim } => {
135 let c = snap_new_point(doc, base, center.0, center.1, snap_radius);
136 let r = snap_new_point(doc, base, rim.0, rim.1, snap_radius);
137 push_geometry(doc, "circle", vec![c, r], false);
138 }
139 HandDrawShape::Arc { center, start, end } => {
140 let c = snap_new_point(doc, base, center.0, center.1, snap_radius);
141 let s = snap_new_point(doc, base, start.0, start.1, snap_radius);
142 let e = snap_new_point(doc, base, end.0, end.1, snap_radius);
143 push_geometry(doc, "arc", vec![c, s, e], false);
144 }
145 HandDrawShape::Bezier { controls } => {
146 let ids: Vec<Value> = controls
147 .iter()
148 .map(|&(u, v)| snap_new_point(doc, base, u, v, snap_radius))
149 .collect();
150 push_geometry(doc, "bezier", ids.clone(), false);
151 push_geometry(doc, "line", vec![ids[0].clone(), ids[1].clone()], true);
154 push_geometry(doc, "line", vec![ids[3].clone(), ids[2].clone()], true);
155 }
156 }
157}
158
159pub fn stroke_extent(stroke: &[(f64, f64)]) -> f64 {
162 let (mut minx, mut miny) = (f64::INFINITY, f64::INFINITY);
163 let (mut maxx, mut maxy) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
164 for &(x, y) in stroke {
165 minx = minx.min(x);
166 miny = miny.min(y);
167 maxx = maxx.max(x);
168 maxy = maxy.max(y);
169 }
170 if !minx.is_finite() {
171 return 0.0;
172 }
173 ((maxx - minx).powi(2) + (maxy - miny).powi(2)).sqrt()
174}
175
176fn fit_cubic(stroke: &[(f64, f64)]) -> [(f64, f64); 4] {
180 let n = stroke.len();
181 let first = stroke[0];
182 let last = stroke[n - 1];
183 let mut cum = vec![0.0_f64; n];
184 for i in 1..n {
185 cum[i] = cum[i - 1] + dist(stroke[i - 1], stroke[i]);
186 }
187 let total = cum[n - 1];
188 if total < 1e-9 {
189 return [first, first, last, last];
190 }
191 let c1 = sample_arc(stroke, &cum, total, 1.0 / 3.0);
192 let c2 = sample_arc(stroke, &cum, total, 2.0 / 3.0);
193 [first, c1, c2, last]
194}
195
196fn sample_arc(stroke: &[(f64, f64)], cum: &[f64], total: f64, t: f64) -> (f64, f64) {
199 let target = total * t;
200 let mut idx = 0;
201 while idx < cum.len() && cum[idx] < target {
202 idx += 1;
203 }
204 if idx == 0 {
205 return stroke[0];
206 }
207 if idx >= cum.len() {
208 return stroke[stroke.len() - 1];
209 }
210 let (d0, d1) = (cum[idx - 1], cum[idx]);
211 let span = (d1 - d0).max(1e-9);
212 let tt = ((target - d0) / span).clamp(0.0, 1.0);
213 let p0 = stroke[idx - 1];
214 let p1 = stroke[idx];
215 (p0.0 + (p1.0 - p0.0) * tt, p0.1 + (p1.1 - p0.1) * tt)
216}
217
218fn fit_circle_lsq(pts: &[(f64, f64)]) -> Option<(f64, f64, f64)> {
222 let n = pts.len();
223 if n < 3 {
224 return None;
225 }
226 let nf = n as f64;
227 let (mut mx, mut my) = (0.0_f64, 0.0_f64);
228 for &(x, y) in pts {
229 mx += x;
230 my += y;
231 }
232 mx /= nf;
233 my /= nf;
234 let (mut sxx, mut sxy, mut syy) = (0.0_f64, 0.0_f64, 0.0_f64);
236 let (mut sxz, mut syz) = (0.0_f64, 0.0_f64);
237 for &(x, y) in pts {
238 let u = x - mx;
239 let v = y - my;
240 let z = u * u + v * v;
241 sxx += u * u;
242 sxy += u * v;
243 syy += v * v;
244 sxz += u * z;
245 syz += v * z;
246 }
247 let det = sxx * syy - sxy * sxy;
248 if det.abs() < 1e-12 {
249 return None; }
251 let uc = (sxz * syy - syz * sxy) / (2.0 * det);
253 let vc = (sxx * syz - sxy * sxz) / (2.0 * det);
254 let cx = uc + mx;
255 let cy = vc + my;
256 let r = (uc * uc + vc * vc + (sxx + syy) / nf).sqrt();
257 if !cx.is_finite() || !cy.is_finite() || !r.is_finite() {
258 return None;
259 }
260 Some((cx, cy, r))
261}
262
263fn snap_new_point(doc: &mut SketchDoc, base: usize, u: f64, v: f64, radius: f64) -> Value {
268 let mut best: Option<(f64, Value)> = None;
269 for p in doc.points.iter().take(base) {
270 let d = ((p.x - u).powi(2) + (p.y - v).powi(2)).sqrt();
271 if d <= radius && best.as_ref().map_or(true, |(bd, _)| d < *bd) {
272 best = Some((d, p.id.clone()));
273 }
274 }
275 if let Some((_, id)) = best {
276 return id;
277 }
278 let id = doc.next_point_id();
279 doc.points.push(SketchPoint {
280 id: id.clone(),
281 x: u,
282 y: v,
283 fixed: false,
284 construction: false,
285 external_reference: false,
286 });
287 id
288}
289
290fn push_geometry(doc: &mut SketchDoc, geom_type: &str, points: Vec<Value>, construction: bool) {
292 let id = doc.next_geometry_id();
293 let mut extra = Map::new();
294 extra.insert("construction".to_string(), Value::Bool(construction));
295 doc.geometries.push(SketchGeometry {
296 id,
297 geom_type: geom_type.to_string(),
298 points,
299 extra,
300 });
301}
302
303