1use serde_json::Value;
39
40use super::doc::{id_key, SketchDoc, SketchPoint};
41
42const SPAN_SAMPLES: usize = 64;
47
48const MIN_SPAN_PARAM: f64 = 1e-3;
55
56pub fn is_spline_type(geom_type: &str) -> bool {
61 geom_type == "bezier" || geom_type == "spline"
62}
63
64#[derive(Clone, Debug)]
68pub struct InsertedAnchor {
69 pub anchor: Value,
71 pub before: Value,
73 pub after: Value,
75}
76
77pub fn insert_anchor(doc: &mut SketchDoc, u: f64, v: f64, radius: f64) -> Option<InsertedAnchor> {
97 if doc
100 .points
101 .iter()
102 .any(|p| (p.x - u).hypot(p.y - v) <= radius)
103 {
104 return None;
105 }
106
107 let (geo_id, span, t) = nearest_spline_span(doc, u, v, radius)?;
108 if !(MIN_SPAN_PARAM..=1.0 - MIN_SPAN_PARAM).contains(&t) {
109 return None;
110 }
111
112 let ids = {
115 let geo = doc.geometry(&geo_id)?;
116 let i0 = span * 3;
117 [
118 geo.points.get(i0)?.clone(),
119 geo.points.get(i0 + 1)?.clone(),
120 geo.points.get(i0 + 2)?.clone(),
121 geo.points.get(i0 + 3)?.clone(),
122 ]
123 };
124 let keys: Vec<String> = ids.iter().map(id_key).collect();
125 for i in 0..keys.len() {
126 for j in (i + 1)..keys.len() {
127 if keys[i] == keys[j] {
128 return None; }
130 }
131 }
132 let controls = span_controls(doc, &ids)?;
133
134 let [p0, p1, p2, p3] = controls;
136 let a = lerp(p0, p1, t);
137 let b = lerp(p1, p2, t);
138 let c = lerp(p2, p3, t);
139 let d = lerp(a, b, t);
140 let e = lerp(b, c, t);
141 let s = lerp(d, e, t);
142
143 move_point(doc, &ids[1], a)?;
147 move_point(doc, &ids[2], c)?;
148 let d_id = add_point(doc, d);
150 let s_id = add_point(doc, s);
151 let e_id = add_point(doc, e);
152
153 let geo = doc.geometry_mut(&geo_id)?;
156 let at = span * 3 + 2;
157 geo.points
158 .splice(at..at, [d_id.clone(), s_id.clone(), e_id.clone()]);
159
160 Some(InsertedAnchor { anchor: s_id, before: d_id, after: e_id })
161}
162
163fn nearest_spline_span(doc: &SketchDoc, u: f64, v: f64, radius: f64) -> Option<(Value, usize, f64)> {
167 let mut best: Option<(f64, Value, usize, f64)> = None;
168 for geo in &doc.geometries {
169 if !is_spline_type(&geo.geom_type) {
170 continue;
171 }
172 let ids = &geo.points;
173 if ids.len() < 4 || (ids.len() - 1) % 3 != 0 {
174 continue;
175 }
176 for span in 0..(ids.len() - 1) / 3 {
177 let i0 = span * 3;
178 let Some(controls) = span_controls(
179 doc,
180 &[
181 ids[i0].clone(),
182 ids[i0 + 1].clone(),
183 ids[i0 + 2].clone(),
184 ids[i0 + 3].clone(),
185 ],
186 ) else {
187 continue;
188 };
189 let (dist, t) = closest_on_span(&controls, u, v);
190 if dist <= radius && best.as_ref().map_or(true, |(bd, ..)| dist < *bd) {
191 best = Some((dist, geo.id.clone(), span, t));
192 }
193 }
194 }
195 best.map(|(_, id, span, t)| (id, span, t))
196}
197
198fn closest_on_span(controls: &[[f64; 2]; 4], u: f64, v: f64) -> (f64, f64) {
203 let mut prev = eval_cubic(controls, 0.0);
204 let mut best = ((u - prev[0]).hypot(v - prev[1]), 0.0);
205 for i in 1..=SPAN_SAMPLES {
206 let t1 = i as f64 / SPAN_SAMPLES as f64;
207 let next = eval_cubic(controls, t1);
208 let (dx, dy) = (next[0] - prev[0], next[1] - prev[1]);
209 let len2 = (dx * dx + dy * dy).max(1e-24);
210 let s = (((u - prev[0]) * dx + (v - prev[1]) * dy) / len2).clamp(0.0, 1.0);
211 let dist = (u - (prev[0] + dx * s)).hypot(v - (prev[1] + dy * s));
212 if dist < best.0 {
213 let t0 = (i - 1) as f64 / SPAN_SAMPLES as f64;
214 best = (dist, t0 + (t1 - t0) * s);
215 }
216 prev = next;
217 }
218 best
219}
220
221fn eval_cubic(controls: &[[f64; 2]; 4], t: f64) -> [f64; 2] {
223 let mt = 1.0 - t;
224 let (w0, w1, w2, w3) = (
225 mt * mt * mt,
226 3.0 * mt * mt * t,
227 3.0 * mt * t * t,
228 t * t * t,
229 );
230 [
231 w0 * controls[0][0] + w1 * controls[1][0] + w2 * controls[2][0] + w3 * controls[3][0],
232 w0 * controls[0][1] + w1 * controls[1][1] + w2 * controls[2][1] + w3 * controls[3][1],
233 ]
234}
235
236fn span_controls(doc: &SketchDoc, ids: &[Value; 4]) -> Option<[[f64; 2]; 4]> {
238 let mut out = [[0.0; 2]; 4];
239 for (slot, id) in ids.iter().enumerate() {
240 let p = doc.point(id)?;
241 out[slot] = [p.x, p.y];
242 }
243 Some(out)
244}
245
246fn lerp(a: [f64; 2], b: [f64; 2], t: f64) -> [f64; 2] {
247 [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]
248}
249
250fn move_point(doc: &mut SketchDoc, id: &Value, at: [f64; 2]) -> Option<()> {
252 let p = doc.point_mut(id)?;
253 p.x = at[0];
254 p.y = at[1];
255 Some(())
256}
257
258fn add_point(doc: &mut SketchDoc, at: [f64; 2]) -> Value {
262 let id = doc.next_point_id();
263 doc.points.push(SketchPoint {
264 id: id.clone(),
265 x: at[0],
266 y: at[1],
267 fixed: false,
268 construction: false,
269 external_reference: false,
270 });
271 id
272}
273
274#[cfg(test)]
287pub(crate) fn span_controls_at(doc: &SketchDoc, geo_id: &Value, span: usize) -> [[f64; 2]; 4] {
288 let geo = doc.geometry(geo_id).expect("geometry");
289 let i0 = span * 3;
290 let ids = [
291 geo.points[i0].clone(),
292 geo.points[i0 + 1].clone(),
293 geo.points[i0 + 2].clone(),
294 geo.points[i0 + 3].clone(),
295 ];
296 span_controls(doc, &ids).expect("controls")
297}
298
299#[cfg(test)]
301pub(crate) fn chain_spans(doc: &SketchDoc, geo_id: &Value) -> Vec<[[f64; 2]; 4]> {
302 let count = (doc.geometry(geo_id).expect("geometry").points.len() - 1) / 3;
303 (0..count).map(|span| span_controls_at(doc, geo_id, span)).collect()
304}
305
306#[cfg(test)]
310pub(crate) fn point_on_span(doc: &SketchDoc, geo_id: &Value, span: usize, t: f64) -> [f64; 2] {
311 eval_cubic(&span_controls_at(doc, geo_id, span), t)
312}
313
314#[cfg(test)]
319fn dist_to_cubic(controls: &[[f64; 2]; 4], q: [f64; 2]) -> f64 {
320 const SCAN: usize = 256;
321 let at = |t: f64| {
322 let p = eval_cubic(controls, t);
323 (p[0] - q[0]).hypot(p[1] - q[1])
324 };
325 let mut best = (at(0.0), 0.0);
326 for i in 1..=SCAN {
327 let t = i as f64 / SCAN as f64;
328 let d = at(t);
329 if d < best.0 {
330 best = (d, t);
331 }
332 }
333 let h = 1.0 / SCAN as f64;
334 let (mut lo, mut hi) = ((best.1 - h).max(0.0), (best.1 + h).min(1.0));
335 for _ in 0..200 {
336 let a = lo + (hi - lo) / 3.0;
337 let b = hi - (hi - lo) / 3.0;
338 if at(a) < at(b) {
339 hi = b;
340 } else {
341 lo = a;
342 }
343 }
344 at((lo + hi) / 2.0)
345}
346
347#[cfg(test)]
352pub(crate) fn shape_deviation(spans: &[[[f64; 2]; 4]], original: &[[f64; 2]; 4]) -> f64 {
353 const WALK: usize = 400;
354 let mut worst: f64 = 0.0;
355 for controls in spans {
356 for i in 0..=WALK {
357 let q = eval_cubic(controls, i as f64 / WALK as f64);
358 worst = worst.max(dist_to_cubic(original, q));
359 }
360 }
361 for i in 0..=WALK {
362 let q = eval_cubic(original, i as f64 / WALK as f64);
363 let near = spans
364 .iter()
365 .map(|c| dist_to_cubic(c, q))
366 .fold(f64::INFINITY, f64::min);
367 worst = worst.max(near);
368 }
369 worst
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use serde_json::json;
376
377 fn doc_from(value: Value) -> SketchDoc {
378 serde_json::from_value(value).expect("sketch doc")
379 }
380
381 fn hump_doc() -> SketchDoc {
384 doc_from(json!({
385 "points": [
386 { "id": 0, "x": 0.0, "y": 0.0 },
387 { "id": 1, "x": 0.0, "y": 30.0 },
388 { "id": 2, "x": 30.0, "y": 30.0 },
389 { "id": 3, "x": 30.0, "y": 0.0 }
390 ],
391 "geometries": [{ "id": 10, "type": "bezier", "points": [0, 1, 2, 3] }],
392 "constraints": []
393 }))
394 }
395
396 fn point_ids(doc: &SketchDoc, geo_id: &Value) -> Vec<String> {
397 doc.geometry(geo_id)
398 .expect("geometry")
399 .points
400 .iter()
401 .map(id_key)
402 .collect()
403 }
404
405 #[test]
406 fn insert_subdivides_without_moving_the_curve() {
407 let mut doc = hump_doc();
408 let geo = json!(10);
409 let before = span_controls_at(&doc, &geo, 0);
410
411 let added = insert_anchor(&mut doc, 15.0, 22.5, 1.0).expect("inserted");
413 assert_eq!(point_ids(&doc, &geo).len(), 7, "3n+1 grew by one span");
414
415 let dev = shape_deviation(&chain_spans(&doc, &geo), &before);
420 assert!(dev < 1e-12, "curve moved by {dev}");
421
422 let s = doc.point(&added.anchor).expect("anchor");
425 assert!((s.x - 15.0).abs() < 1e-6 && (s.y - 22.5).abs() < 1e-6, "anchor at {s:?}");
426 let d = doc.point(&added.before).expect("before handle");
427 let e = doc.point(&added.after).expect("after handle");
428 let cross = (s.x - d.x) * (e.y - d.y) - (s.y - d.y) * (e.x - d.x);
429 assert!(cross.abs() < 1e-9, "anchor off the handle line: {cross}");
430 }
431
432 #[test]
433 fn insert_keeps_the_end_ids_and_reuses_the_handle_ids() {
434 let mut doc = hump_doc();
435 let geo = json!(10);
436 insert_anchor(&mut doc, 15.0, 22.5, 1.0).expect("inserted");
437
438 let ids = point_ids(&doc, &geo);
439 assert_eq!(ids[0], "0", "start anchor id untouched");
440 assert_eq!(ids[6], "3", "end anchor id untouched");
441 assert_eq!(ids[1], "1", "P1 reused as the first half-span's handle");
442 assert_eq!(ids[5], "2", "P2 reused as the second half-span's handle");
443 let p1 = doc.point(&json!(1)).expect("P1");
446 assert!((p1.x - 0.0).abs() < 1e-9 && (p1.y - 15.0).abs() < 1e-9, "A = {p1:?}");
447 let p2 = doc.point(&json!(2)).expect("P2");
448 assert!((p2.x - 30.0).abs() < 1e-9 && (p2.y - 15.0).abs() < 1e-9, "C = {p2:?}");
449 assert_eq!(doc.points.len(), 7);
451 }
452
453 #[test]
454 fn insert_into_the_second_span_splices_at_the_right_slot() {
455 let mut doc = doc_from(json!({
458 "points": [
459 { "id": 0, "x": 0.0, "y": 0.0 },
460 { "id": 1, "x": 0.0, "y": 20.0 },
461 { "id": 2, "x": 20.0, "y": 20.0 },
462 { "id": 3, "x": 20.0, "y": 0.0 },
463 { "id": 4, "x": 20.0, "y": -20.0 },
464 { "id": 5, "x": 40.0, "y": -20.0 },
465 { "id": 6, "x": 40.0, "y": 0.0 }
466 ],
467 "geometries": [{ "id": 10, "type": "bezier", "points": [0, 1, 2, 3, 4, 5, 6] }],
468 "constraints": []
469 }));
470 let geo = json!(10);
471 let span0_before = span_controls_at(&doc, &geo, 0);
472 let span1_before = span_controls_at(&doc, &geo, 1);
473 let mid = eval_cubic(&span1_before, 0.5);
474
475 insert_anchor(&mut doc, mid[0], mid[1], 1.0).expect("inserted");
476 let ids = point_ids(&doc, &geo);
477 assert_eq!(ids.len(), 10, "3n+1 holds at three spans");
478 assert_eq!(&ids[..4], &["0", "1", "2", "3"], "first span disturbed: {ids:?}");
480 assert_eq!(ids[4], "4", "second span's leading handle kept its id");
481 assert_eq!(ids[8], "5", "second span's trailing handle kept its id");
482 assert_eq!(ids[9], "6", "end anchor untouched");
483
484 assert_eq!(span_controls_at(&doc, &geo, 0), span0_before, "first span moved");
487 let dev = shape_deviation(&chain_spans(&doc, &geo)[1..=2], &span1_before);
488 assert!(dev < 1e-12, "subdivided span moved by {dev}");
489 }
490
491 #[test]
492 fn a_second_insert_into_a_freshly_made_span_still_holds_the_invariant() {
493 let mut doc = hump_doc();
494 let geo = json!(10);
495 let original = span_controls_at(&doc, &geo, 0);
496
497 insert_anchor(&mut doc, 15.0, 22.5, 1.0).expect("first insert");
498 let mid = eval_cubic(&span_controls_at(&doc, &geo, 0), 0.5);
500 insert_anchor(&mut doc, mid[0], mid[1], 1.0).expect("second insert");
501 assert_eq!(point_ids(&doc, &geo).len(), 10, "3n+1 after two inserts");
502
503 let dev = shape_deviation(&chain_spans(&doc, &geo), &original);
505 assert!(dev < 1e-12, "curve moved by {dev} over two inserts");
506 }
507
508 #[test]
509 fn two_inserts_into_the_same_original_span_both_land() {
510 let mut doc = hump_doc();
511 let geo = json!(10);
512 let original = span_controls_at(&doc, &geo, 0);
513 let first = eval_cubic(&original, 0.25);
516 let second = eval_cubic(&original, 0.75);
517 insert_anchor(&mut doc, first[0], first[1], 1.0).expect("first insert");
518 insert_anchor(&mut doc, second[0], second[1], 1.0).expect("second insert");
519
520 assert_eq!(point_ids(&doc, &geo).len(), 10, "two spans became four");
521 let dev = shape_deviation(&chain_spans(&doc, &geo), &original);
522 assert!(dev < 1e-12, "curve moved by {dev}");
523 }
524
525 #[test]
526 fn a_click_off_the_curve_or_on_another_geometry_inserts_nothing() {
527 let mut doc = hump_doc();
528 assert!(insert_anchor(&mut doc, 15.0, 0.5, 1.0).is_none(), "empty space inserted");
530 assert_eq!(doc.points.len(), 4, "nothing minted");
531
532 let mut doc = doc_from(json!({
534 "points": [
535 { "id": 0, "x": 0.0, "y": 0.0 },
536 { "id": 1, "x": 0.0, "y": 30.0 },
537 { "id": 2, "x": 30.0, "y": 30.0 },
538 { "id": 3, "x": 30.0, "y": 0.0 },
539 { "id": 4, "x": -50.0, "y": -10.0 },
540 { "id": 5, "x": 50.0, "y": -10.0 }
541 ],
542 "geometries": [
543 { "id": 10, "type": "bezier", "points": [0, 1, 2, 3] },
544 { "id": 11, "type": "line", "points": [4, 5] }
545 ],
546 "constraints": []
547 }));
548 assert!(insert_anchor(&mut doc, 0.0, -10.0, 1.0).is_none(), "the line was subdivided");
549 assert_eq!(doc.points.len(), 6, "nothing minted");
550 }
551
552 #[test]
553 fn a_click_on_a_control_point_or_span_end_inserts_nothing() {
554 let mut doc = hump_doc();
555 assert!(insert_anchor(&mut doc, 0.0, 0.0, 1.0).is_none(), "anchor click inserted");
557 assert!(insert_anchor(&mut doc, 0.2, 30.0, 1.0).is_none(), "handle click inserted");
559 assert_eq!(doc.points.len(), 4, "nothing minted");
560 }
561
562 #[test]
563 fn a_cusp_span_that_repeats_an_id_is_left_alone() {
564 let mut doc = doc_from(json!({
567 "points": [
568 { "id": 0, "x": 0.0, "y": 0.0 },
569 { "id": 1, "x": 15.0, "y": 30.0 },
570 { "id": 3, "x": 30.0, "y": 0.0 }
571 ],
572 "geometries": [{ "id": 10, "type": "bezier", "points": [0, 1, 1, 3] }],
573 "constraints": []
574 }));
575 let mid = eval_cubic(&span_controls_at(&doc, &json!(10), 0), 0.5);
576 assert!(insert_anchor(&mut doc, mid[0], mid[1], 1.0).is_none(), "cusp subdivided");
577 assert_eq!(doc.points.len(), 3, "nothing minted");
578 }
579
580 #[test]
581 fn the_spline_type_alias_is_pickable_too() {
582 let mut doc = doc_from(json!({
583 "points": [
584 { "id": 0, "x": 0.0, "y": 0.0 },
585 { "id": 1, "x": 0.0, "y": 30.0 },
586 { "id": 2, "x": 30.0, "y": 30.0 },
587 { "id": 3, "x": 30.0, "y": 0.0 }
588 ],
589 "geometries": [{ "id": 10, "type": "spline", "points": [0, 1, 2, 3] }],
590 "constraints": []
591 }));
592 assert!(insert_anchor(&mut doc, 15.0, 22.5, 1.0).is_some(), "alias not subdivided");
593 assert_eq!(point_ids(&doc, &json!(10)).len(), 7);
594 }
595}