1use glam::DVec2;
11
12pub type SketchId = u32;
14
15pub const DEFAULT_ARC_SEGMENTS: usize = 32;
18
19#[derive(Clone, Copy, Debug, PartialEq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct SketchPoint {
23 pub id: SketchId,
24 pub pos: [f64; 2],
25}
26
27#[derive(Clone, Copy, Debug, PartialEq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub enum SketchEntityKind {
31 Line { p0: SketchId, p1: SketchId },
33 Circle { center: SketchId, radius: f64 },
35 Arc {
38 center: SketchId,
39 start: SketchId,
40 end: SketchId,
41 },
42}
43
44#[derive(Clone, Copy, Debug, PartialEq)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46pub struct SketchEntity {
47 pub id: SketchId,
48 pub kind: SketchEntityKind,
49 #[cfg_attr(feature = "serde", serde(default))]
52 pub construction: bool,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61pub enum SketchConstraint {
62 Coincident {
64 a: SketchId,
65 b: SketchId,
66 },
67 Horizontal {
69 line: SketchId,
70 },
71 Vertical {
73 line: SketchId,
74 },
75 Parallel {
76 a: SketchId,
77 b: SketchId,
78 },
79 Perpendicular {
80 a: SketchId,
81 b: SketchId,
82 },
83 Tangent {
85 line: SketchId,
86 entity: SketchId,
87 },
88 PointOnCurve {
90 point: SketchId,
91 entity: SketchId,
92 },
93 Distance {
95 a: SketchId,
96 b: SketchId,
97 d: f64,
98 },
99 Radius {
101 entity: SketchId,
102 r: f64,
103 },
104 Fixed {
106 point: SketchId,
107 x: f64,
108 y: f64,
109 },
110}
111
112impl SketchConstraint {
113 pub fn label(&self) -> &'static str {
115 match self {
116 Self::Coincident { .. } => "Coincident",
117 Self::Horizontal { .. } => "Horizontal",
118 Self::Vertical { .. } => "Vertical",
119 Self::Parallel { .. } => "Parallel",
120 Self::Perpendicular { .. } => "Perpendicular",
121 Self::Tangent { .. } => "Tangent",
122 Self::PointOnCurve { .. } => "On Curve",
123 Self::Distance { .. } => "Distance",
124 Self::Radius { .. } => "Radius",
125 Self::Fixed { .. } => "Fixed",
126 }
127 }
128
129 pub fn value(&self) -> Option<f64> {
131 match self {
132 Self::Distance { d, .. } => Some(*d),
133 Self::Radius { r, .. } => Some(*r),
134 _ => None,
135 }
136 }
137
138 pub fn set_value(&mut self, value: f64) {
140 match self {
141 Self::Distance { d, .. } => *d = value,
142 Self::Radius { r, .. } => *r = value,
143 _ => {}
144 }
145 }
146}
147
148#[derive(Clone, Debug, Default, PartialEq)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
151pub struct SketchData {
152 pub points: Vec<SketchPoint>,
153 pub entities: Vec<SketchEntity>,
154 pub constraints: Vec<SketchConstraint>,
155 #[cfg_attr(feature = "serde", serde(default))]
157 pub next_id: SketchId,
158}
159
160impl SketchData {
161 fn alloc_id(&mut self) -> SketchId {
162 let floor = self
164 .points
165 .iter()
166 .map(|p| p.id)
167 .chain(self.entities.iter().map(|e| e.id))
168 .max()
169 .map_or(0, |m| m + 1);
170 self.next_id = self.next_id.max(floor);
171 let id = self.next_id;
172 self.next_id += 1;
173 id
174 }
175
176 pub fn point(&self, id: SketchId) -> Option<&SketchPoint> {
177 self.points.iter().find(|p| p.id == id)
178 }
179
180 pub fn point_mut(&mut self, id: SketchId) -> Option<&mut SketchPoint> {
181 self.points.iter_mut().find(|p| p.id == id)
182 }
183
184 pub fn point_pos(&self, id: SketchId) -> Option<DVec2> {
185 self.point(id).map(|p| DVec2::from_array(p.pos))
186 }
187
188 pub fn entity(&self, id: SketchId) -> Option<&SketchEntity> {
189 self.entities.iter().find(|e| e.id == id)
190 }
191
192 pub fn entity_mut(&mut self, id: SketchId) -> Option<&mut SketchEntity> {
193 self.entities.iter_mut().find(|e| e.id == id)
194 }
195
196 pub fn set_construction(&mut self, id: SketchId, construction: bool) -> bool {
199 match self.entity_mut(id) {
200 Some(e) => {
201 e.construction = construction;
202 true
203 }
204 None => false,
205 }
206 }
207
208 pub fn add_point(&mut self, pos: DVec2) -> SketchId {
211 let id = self.alloc_id();
212 self.points.push(SketchPoint {
213 id,
214 pos: pos.to_array(),
215 });
216 id
217 }
218
219 pub fn add_line(&mut self, p0: SketchId, p1: SketchId) -> SketchId {
221 let id = self.alloc_id();
222 self.entities.push(SketchEntity {
223 id,
224 kind: SketchEntityKind::Line { p0, p1 },
225 construction: false,
226 });
227 id
228 }
229
230 pub fn add_circle(&mut self, center: DVec2, radius: f64) -> SketchId {
231 let center = self.add_point(center);
232 self.add_circle_with_center(center, radius)
233 }
234
235 pub fn add_circle_with_center(&mut self, center: SketchId, radius: f64) -> SketchId {
237 let id = self.alloc_id();
238 self.entities.push(SketchEntity {
239 id,
240 kind: SketchEntityKind::Circle { center, radius },
241 construction: false,
242 });
243 id
244 }
245
246 pub fn add_arc(&mut self, center: SketchId, start: SketchId, end: SketchId) -> SketchId {
248 let id = self.alloc_id();
249 self.entities.push(SketchEntity {
250 id,
251 kind: SketchEntityKind::Arc { center, start, end },
252 construction: false,
253 });
254 id
255 }
256
257 pub fn add_rect(&mut self, corner_a: DVec2, corner_b: DVec2) -> [SketchId; 4] {
260 let (min, max) = (corner_a.min(corner_b), corner_a.max(corner_b));
261 let p0 = self.add_point(min);
262 let p1 = self.add_point(DVec2::new(max.x, min.y));
263 let p2 = self.add_point(max);
264 let p3 = self.add_point(DVec2::new(min.x, max.y));
265 let bottom = self.add_line(p0, p1);
266 let right = self.add_line(p1, p2);
267 let top = self.add_line(p2, p3);
268 let left = self.add_line(p3, p0);
269 self.constraints
270 .push(SketchConstraint::Horizontal { line: bottom });
271 self.constraints
272 .push(SketchConstraint::Horizontal { line: top });
273 self.constraints
274 .push(SketchConstraint::Vertical { line: right });
275 self.constraints
276 .push(SketchConstraint::Vertical { line: left });
277 [bottom, right, top, left]
278 }
279
280 pub fn delete_entity(&mut self, id: SketchId) {
285 self.entities.retain(|e| e.id != id);
286 self.constraints.retain(|c| match c {
287 SketchConstraint::Horizontal { line } | SketchConstraint::Vertical { line } => {
288 *line != id
289 }
290 SketchConstraint::Parallel { a, b } | SketchConstraint::Perpendicular { a, b } => {
291 *a != id && *b != id
292 }
293 SketchConstraint::Tangent { line, entity } => *line != id && *entity != id,
294 SketchConstraint::PointOnCurve { entity, .. } => *entity != id,
295 SketchConstraint::Radius { entity, .. } => *entity != id,
296 _ => true,
297 });
298 self.prune_orphan_points();
299 }
300
301 pub fn delete_constraint(&mut self, index: usize) {
302 if index < self.constraints.len() {
303 self.constraints.remove(index);
304 }
305 }
306
307 pub fn delete_point(&mut self, id: SketchId) {
311 let uses: Vec<SketchId> = self
312 .entities
313 .iter()
314 .filter(|e| match e.kind {
315 SketchEntityKind::Line { p0, p1 } => p0 == id || p1 == id,
316 SketchEntityKind::Circle { center, .. } => center == id,
317 SketchEntityKind::Arc { center, start, end } => {
318 center == id || start == id || end == id
319 }
320 })
321 .map(|e| e.id)
322 .collect();
323 for entity in uses {
324 self.delete_entity(entity);
325 }
326 self.points.retain(|p| p.id != id);
329 self.constraints.retain(|c| match c {
330 SketchConstraint::Coincident { a, b } | SketchConstraint::Distance { a, b, .. } => {
331 *a != id && *b != id
332 }
333 SketchConstraint::Fixed { point, .. }
334 | SketchConstraint::PointOnCurve { point, .. } => *point != id,
335 _ => true,
336 });
337 }
338
339 fn prune_orphan_points(&mut self) {
342 let used = |id: SketchId, entities: &[SketchEntity]| {
343 entities.iter().any(|e| match e.kind {
344 SketchEntityKind::Line { p0, p1 } => p0 == id || p1 == id,
345 SketchEntityKind::Circle { center, .. } => center == id,
346 SketchEntityKind::Arc { center, start, end } => {
347 center == id || start == id || end == id
348 }
349 })
350 };
351 let entities = core::mem::take(&mut self.entities);
352 self.points.retain(|p| used(p.id, &entities));
353 self.entities = entities;
354
355 let alive = |id: SketchId, points: &[SketchPoint]| points.iter().any(|p| p.id == id);
356 let points = core::mem::take(&mut self.points);
357 self.constraints.retain(|c| match c {
358 SketchConstraint::Coincident { a, b } | SketchConstraint::Distance { a, b, .. } => {
359 alive(*a, &points) && alive(*b, &points)
360 }
361 SketchConstraint::Fixed { point, .. }
362 | SketchConstraint::PointOnCurve { point, .. } => alive(*point, &points),
363 _ => true,
364 });
365 self.points = points;
366 }
367
368 pub fn connected_components(&self) -> Vec<Vec<SketchId>> {
379 let index: std::collections::HashMap<SketchId, usize> = self
381 .points
382 .iter()
383 .enumerate()
384 .map(|(i, p)| (p.id, i))
385 .collect();
386 let mut parent: Vec<usize> = (0..self.points.len()).collect();
387 let union = |parent: &mut [usize], a: SketchId, b: SketchId| {
388 if let (Some(&i), Some(&j)) = (index.get(&a), index.get(&b)) {
389 let (ri, rj) = (uf_find(parent, i), uf_find(parent, j));
390 parent[ri] = rj;
391 }
392 };
393 for entity in &self.entities {
394 let pts = entity_point_ids(entity);
395 for pair in pts.windows(2) {
396 union(&mut parent, pair[0], pair[1]);
397 }
398 }
399 for constraint in &self.constraints {
400 if let SketchConstraint::Coincident { a, b } = constraint {
401 union(&mut parent, *a, *b);
402 }
403 }
404
405 let mut components: Vec<Vec<SketchId>> = Vec::new();
407 let mut root_to_component: std::collections::HashMap<usize, usize> =
408 std::collections::HashMap::new();
409 for entity in &self.entities {
410 let root = entity_point_ids(entity)
411 .iter()
412 .find_map(|id| index.get(id).copied())
413 .map(|i| uf_find(&mut parent, i));
414 match root {
415 Some(root) => {
416 let slot = *root_to_component.entry(root).or_insert_with(|| {
417 components.push(Vec::new());
418 components.len() - 1
419 });
420 components[slot].push(entity.id);
421 }
422 None => components.push(vec![entity.id]),
424 }
425 }
426 components
427 }
428
429 pub fn entity_subset(&self, entity_ids: &[SketchId]) -> SketchData {
434 SketchData {
435 points: self.points.clone(),
436 entities: self
437 .entities
438 .iter()
439 .filter(|e| entity_ids.contains(&e.id))
440 .copied()
441 .collect(),
442 constraints: self.constraints.clone(),
443 next_id: self.next_id,
444 }
445 }
446
447 pub fn geometric_components(&self, arc_segments: usize) -> Vec<Vec<SketchId>> {
458 let components = self.connected_components();
459 if components.len() <= 1 {
460 return components;
461 }
462 let loops: Vec<Vec<Vec<DVec2>>> = components
463 .iter()
464 .map(|ids| profile_loops(&self.entity_subset(ids), arc_segments).loops)
465 .collect();
466
467 let mut parent: Vec<usize> = (0..components.len()).collect();
469 for i in 0..components.len() {
470 for j in (i + 1)..components.len() {
471 let interact = loops[i]
472 .iter()
473 .any(|a| loops[j].iter().any(|b| loops_interact(a, b)));
474 if interact {
475 let (ri, rj) = (uf_find(&mut parent, i), uf_find(&mut parent, j));
476 parent[ri] = rj;
477 }
478 }
479 }
480
481 let mut groups: Vec<Vec<SketchId>> = Vec::new();
483 let mut root_to_group: std::collections::HashMap<usize, usize> =
484 std::collections::HashMap::new();
485 for (i, ids) in components.into_iter().enumerate() {
486 let root = uf_find(&mut parent, i);
487 let slot = *root_to_group.entry(root).or_insert_with(|| {
488 groups.push(Vec::new());
489 groups.len() - 1
490 });
491 groups[slot].extend(ids);
492 }
493 groups
494 }
495
496 pub fn split_off_entities(&mut self, entity_ids: &[SketchId]) -> SketchData {
504 let (taken_entities, rest_entities): (Vec<_>, Vec<_>) = core::mem::take(&mut self.entities)
505 .into_iter()
506 .partition(|e| entity_ids.contains(&e.id));
507 self.entities = rest_entities;
508
509 let taken_point_ids: Vec<SketchId> =
510 taken_entities.iter().flat_map(entity_point_ids).collect();
511 let (taken_points, rest_points): (Vec<_>, Vec<_>) = core::mem::take(&mut self.points)
512 .into_iter()
513 .partition(|p| taken_point_ids.contains(&p.id));
514 self.points = rest_points;
515
516 let point_taken = |id: &SketchId| taken_points.iter().any(|p| p.id == *id);
517 let point_rest = |id: &SketchId| self.points.iter().any(|p| p.id == *id);
518 let entity_taken = |id: &SketchId| taken_entities.iter().any(|e| e.id == *id);
519 let entity_rest = |id: &SketchId| self.entities.iter().any(|e| e.id == *id);
520 let mut taken_constraints = Vec::new();
521 let mut rest_constraints = Vec::new();
522 for c in core::mem::take(&mut self.constraints) {
523 let (points, entities) = constraint_refs(&c);
524 if points.iter().all(point_taken) && entities.iter().all(entity_taken) {
525 taken_constraints.push(c);
526 } else if points.iter().all(point_rest) && entities.iter().all(entity_rest) {
527 rest_constraints.push(c);
528 }
529 }
531 self.constraints = rest_constraints;
532
533 SketchData {
534 points: taken_points,
535 entities: taken_entities,
536 constraints: taken_constraints,
537 next_id: self.next_id,
538 }
539 }
540}
541
542fn entity_point_ids(entity: &SketchEntity) -> Vec<SketchId> {
544 match entity.kind {
545 SketchEntityKind::Line { p0, p1 } => vec![p0, p1],
546 SketchEntityKind::Circle { center, .. } => vec![center],
547 SketchEntityKind::Arc { center, start, end } => vec![center, start, end],
548 }
549}
550
551fn constraint_refs(c: &SketchConstraint) -> (Vec<SketchId>, Vec<SketchId>) {
553 match c {
554 SketchConstraint::Coincident { a, b } | SketchConstraint::Distance { a, b, .. } => {
555 (vec![*a, *b], vec![])
556 }
557 SketchConstraint::Horizontal { line } | SketchConstraint::Vertical { line } => {
558 (vec![], vec![*line])
559 }
560 SketchConstraint::Parallel { a, b } | SketchConstraint::Perpendicular { a, b } => {
561 (vec![], vec![*a, *b])
562 }
563 SketchConstraint::Tangent { line, entity } => (vec![], vec![*line, *entity]),
564 SketchConstraint::PointOnCurve { point, entity } => (vec![*point], vec![*entity]),
565 SketchConstraint::Radius { entity, .. } => (vec![], vec![*entity]),
566 SketchConstraint::Fixed { point, .. } => (vec![*point], vec![]),
567 }
568}
569
570fn uf_find(parent: &mut [usize], mut i: usize) -> usize {
572 while parent[i] != i {
573 parent[i] = parent[parent[i]];
574 i = parent[i];
575 }
576 i
577}
578
579#[derive(Clone, Debug, Default, PartialEq)]
586pub struct Profile {
587 pub loops: Vec<Vec<DVec2>>,
588 pub open_chains: usize,
590}
591
592impl Profile {
593 pub fn has_loops(&self) -> bool {
595 !self.loops.is_empty()
596 }
597}
598
599pub fn profile_loops(data: &SketchData, arc_segments: usize) -> Profile {
605 let arc_segments = arc_segments.max(8);
606
607 let mut canon: Vec<(SketchId, SketchId)> = data.points.iter().map(|p| (p.id, p.id)).collect();
609 fn find(canon: &mut Vec<(SketchId, SketchId)>, id: SketchId) -> SketchId {
610 let Some(idx) = canon.iter().position(|(k, _)| *k == id) else {
611 return id;
612 };
613 let parent = canon[idx].1;
614 if parent == id {
615 return id;
616 }
617 let root = find(canon, parent);
618 canon[idx].1 = root;
619 root
620 }
621 for c in &data.constraints {
622 if let SketchConstraint::Coincident { a, b } = c {
623 let (ra, rb) = (find(&mut canon, *a), find(&mut canon, *b));
624 if ra != rb
625 && let Some(idx) = canon.iter().position(|(k, _)| *k == ra)
626 {
627 canon[idx].1 = rb;
628 }
629 }
630 }
631
632 struct Edge {
635 a: SketchId,
636 b: SketchId,
637 entity: SketchId,
638 used: bool,
639 }
640 let mut edges: Vec<Edge> = Vec::new();
641 let mut loops: Vec<Vec<DVec2>> = Vec::new();
642
643 for e in &data.entities {
644 if e.construction {
645 continue;
646 }
647 match e.kind {
648 SketchEntityKind::Line { p0, p1 }
649 | SketchEntityKind::Arc {
650 start: p0, end: p1, ..
651 } => {
652 let (a, b) = (find(&mut canon, p0), find(&mut canon, p1));
653 if a != b {
654 edges.push(Edge {
655 a,
656 b,
657 entity: e.id,
658 used: false,
659 });
660 }
661 }
662 SketchEntityKind::Circle { center, radius } => {
663 if let Some(c) = data.point_pos(center) {
664 loops.push(tessellate_circle(c, radius, arc_segments));
665 }
666 }
667 }
668 }
669
670 let degree = |id: SketchId, edges: &[Edge]| {
673 edges
674 .iter()
675 .filter(|e| !e.used && (e.a == id || e.b == id))
676 .count()
677 };
678
679 loop {
686 let mut pruned = false;
687 for idx in 0..edges.len() {
688 if !edges[idx].used
689 && (degree(edges[idx].a, &edges) == 1 || degree(edges[idx].b, &edges) == 1)
690 {
691 edges[idx].used = true;
692 pruned = true;
693 }
694 }
695 if !pruned {
696 break;
697 }
698 }
699 let mut open_chains = {
700 let mut chain_canon: Vec<(SketchId, SketchId)> = Vec::new();
701 for e in edges.iter().filter(|e| e.used) {
702 for v in [e.a, e.b] {
703 if !chain_canon.iter().any(|(k, _)| *k == v) {
704 chain_canon.push((v, v));
705 }
706 }
707 }
708 for e in edges.iter().filter(|e| e.used) {
709 let (ra, rb) = (find(&mut chain_canon, e.a), find(&mut chain_canon, e.b));
710 if ra != rb
711 && let Some(i) = chain_canon.iter().position(|(k, _)| *k == ra)
712 {
713 chain_canon[i].1 = rb;
714 }
715 }
716 let mut roots: Vec<SketchId> = Vec::new();
717 for e in edges.iter().filter(|e| e.used) {
718 let r = find(&mut chain_canon, e.a);
719 if !roots.contains(&r) {
720 roots.push(r);
721 }
722 }
723 roots.len()
724 };
725
726 for start_idx in 0..edges.len() {
727 if edges[start_idx].used {
728 continue;
729 }
730 let start_vertex = edges[start_idx].a;
732 let mut chain: Vec<(usize, bool)> = Vec::new(); let mut cursor = start_vertex;
734 let mut ok = true;
735 loop {
736 let Some(next_idx) = edges.iter().position(|e| {
737 !e.used
738 && !chain.iter().any(|(i, _)| edges[*i].entity == e.entity)
739 && (e.a == cursor || e.b == cursor)
740 }) else {
741 ok = false;
742 break;
743 };
744 let forward = edges[next_idx].a == cursor;
745 let far = if forward {
746 edges[next_idx].b
747 } else {
748 edges[next_idx].a
749 };
750 chain.push((next_idx, forward));
751 cursor = far;
752 if cursor == start_vertex {
753 break;
754 }
755 if degree(cursor, &edges) != 2 {
756 ok = false;
757 break;
758 }
759 }
760
761 if !ok || chain.is_empty() {
762 open_chains += 1;
763 for (idx, _) in &chain {
765 edges[*idx].used = true;
766 }
767 continue;
768 }
769
770 let mut poly: Vec<DVec2> = Vec::new();
771 for (idx, forward) in &chain {
772 edges[*idx].used = true;
773 let entity = data.entity(edges[*idx].entity).expect("BUG: edge entity");
774 emit_edge(data, entity, *forward, arc_segments, &mut poly);
775 }
776 if poly.len() >= 3 {
777 loops.push(poly);
778 }
779 }
780
781 normalize_windings(&mut loops);
782 Profile { loops, open_chains }
783}
784
785fn emit_edge(
787 data: &SketchData,
788 entity: &SketchEntity,
789 forward: bool,
790 arc_segments: usize,
791 poly: &mut Vec<DVec2>,
792) {
793 match entity.kind {
794 SketchEntityKind::Line { p0, p1 } => {
795 let id = if forward { p0 } else { p1 };
796 if let Some(p) = data.point_pos(id) {
797 poly.push(p);
798 }
799 }
800 SketchEntityKind::Arc { center, start, end } => {
801 let (Some(c), Some(s), Some(e)) = (
802 data.point_pos(center),
803 data.point_pos(start),
804 data.point_pos(end),
805 ) else {
806 return;
807 };
808 let pts = tessellate_arc(c, s, e, arc_segments);
809 if forward {
810 poly.extend(pts[..pts.len() - 1].iter().copied());
811 } else {
812 poly.extend(pts[1..].iter().rev().copied());
813 }
814 }
815 SketchEntityKind::Circle { .. } => {}
816 }
817}
818
819fn tessellate_arc(center: DVec2, start: DVec2, end: DVec2, arc_segments: usize) -> Vec<DVec2> {
823 let r0 = (start - center).length();
824 let r1 = (end - center).length();
825 let a0 = (start.y - center.y).atan2(start.x - center.x);
826 let tau = core::f64::consts::TAU;
827 let sweep = arc_sweep_angle(center, start, end);
828
829 let n = ((arc_segments as f64 * sweep / tau).ceil() as usize).max(2);
830 (0..=n)
831 .map(|i| {
832 let t = i as f64 / n as f64;
833 match i {
834 0 => start,
835 _ if i == n => end,
836 _ => {
837 let a = a0 + sweep * t;
838 let r = r0 + (r1 - r0) * t;
839 center + DVec2::new(a.cos(), a.sin()) * r
840 }
841 }
842 })
843 .collect()
844}
845
846fn tessellate_circle(center: DVec2, radius: f64, segments: usize) -> Vec<DVec2> {
847 let n = segments.max(8);
848 (0..n)
849 .map(|i| {
850 let a = core::f64::consts::TAU * i as f64 / n as f64;
851 center + DVec2::new(a.cos(), a.sin()) * radius
852 })
853 .collect()
854}
855
856pub fn signed_area(poly: &[DVec2]) -> f64 {
859 signed_area_2x(poly) / 2.0
860}
861
862fn signed_area_2x(poly: &[DVec2]) -> f64 {
864 poly.iter()
865 .zip(poly.iter().cycle().skip(1))
866 .map(|(a, b)| a.x * b.y - b.x * a.y)
867 .take(poly.len())
868 .sum()
869}
870
871fn point_in_polygon(p: DVec2, poly: &[DVec2]) -> bool {
872 let mut inside = false;
873 let n = poly.len();
874 for i in 0..n {
875 let (a, b) = (poly[i], poly[(i + 1) % n]);
876 if (a.y > p.y) != (b.y > p.y) {
877 let x = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
878 if p.x < x {
879 inside = !inside;
880 }
881 }
882 }
883 inside
884}
885
886fn loop_aabb(poly: &[DVec2]) -> (DVec2, DVec2) {
888 poly.iter().fold(
889 (DVec2::splat(f64::INFINITY), DVec2::splat(f64::NEG_INFINITY)),
890 |(min, max), p| (min.min(*p), max.max(*p)),
891 )
892}
893
894fn orient(a: DVec2, b: DVec2, c: DVec2) -> f64 {
897 (b - a).perp_dot(c - a)
898}
899
900fn segments_properly_intersect(a0: DVec2, a1: DVec2, b0: DVec2, b1: DVec2) -> bool {
905 orient(b0, b1, a0) * orient(b0, b1, a1) < 0.0 && orient(a0, a1, b0) * orient(a0, a1, b1) < 0.0
906}
907
908fn loops_cross(a: &[DVec2], b: &[DVec2]) -> bool {
910 let ((amin, amax), (bmin, bmax)) = (loop_aabb(a), loop_aabb(b));
911 if amin.x > bmax.x || bmin.x > amax.x || amin.y > bmax.y || bmin.y > amax.y {
912 return false;
913 }
914 (0..a.len()).any(|i| {
915 let (a0, a1) = (a[i], a[(i + 1) % a.len()]);
916 (0..b.len()).any(|j| segments_properly_intersect(a0, a1, b[j], b[(j + 1) % b.len()]))
917 })
918}
919
920fn loops_interact(a: &[DVec2], b: &[DVec2]) -> bool {
925 loops_cross(a, b) || point_in_polygon(a[0], b) || point_in_polygon(b[0], a)
926}
927
928fn normalize_windings(loops: &mut [Vec<DVec2>]) {
940 let n = loops.len();
941 let mut crosses = vec![false; n * n];
942 for i in 0..n {
943 for j in (i + 1)..n {
944 if loops_cross(&loops[i], &loops[j]) {
945 crosses[i * n + j] = true;
946 crosses[j * n + i] = true;
947 }
948 }
949 }
950 let representatives: Vec<DVec2> = loops.iter().map(|l| l[0]).collect();
951 let depths: Vec<usize> = representatives
952 .iter()
953 .enumerate()
954 .map(|(i, &p)| {
955 loops
956 .iter()
957 .enumerate()
958 .filter(|(j, other)| *j != i && !crosses[i * n + j] && point_in_polygon(p, other))
959 .count()
960 })
961 .collect();
962 for (poly, depth) in loops.iter_mut().zip(depths) {
963 let ccw = signed_area_2x(poly) > 0.0;
964 let want_ccw = depth % 2 == 0;
965 if ccw != want_ccw {
966 poly.reverse();
967 }
968 }
969}
970
971#[derive(Clone, Copy, Debug, PartialEq)]
976pub struct SketchIntersection {
977 pub a: SketchId,
978 pub b: SketchId,
979 pub pos: DVec2,
980}
981
982fn segment_segment_intersection(a0: DVec2, a1: DVec2, b0: DVec2, b1: DVec2) -> Option<DVec2> {
986 if !segments_properly_intersect(a0, a1, b0, b1) {
987 return None;
988 }
989 let t = (b0 - a0).perp_dot(b1 - b0) / (a1 - a0).perp_dot(b1 - b0);
991 Some(a0 + (a1 - a0) * t)
992}
993
994fn segment_circle_intersections(a0: DVec2, a1: DVec2, center: DVec2, radius: f64) -> Vec<DVec2> {
998 let d = a1 - a0;
999 let f = a0 - center;
1000 let a = d.length_squared();
1001 if a == 0.0 {
1002 return Vec::new();
1003 }
1004 let b = 2.0 * f.dot(d);
1005 let c = f.length_squared() - radius * radius;
1006 let eps = 1e-9 * radius.max(1.0);
1009 let disc = b * b - 4.0 * a * c;
1010 if disc <= a * eps * eps {
1011 return Vec::new();
1012 }
1013 let sqrt_disc = disc.sqrt();
1014 [(-b - sqrt_disc) / (2.0 * a), (-b + sqrt_disc) / (2.0 * a)]
1015 .into_iter()
1016 .filter(|t| *t > 0.0 && *t < 1.0)
1017 .map(|t| a0 + d * t)
1018 .collect()
1019}
1020
1021fn arc_sweep_angle(center: DVec2, start: DVec2, end: DVec2) -> f64 {
1023 let a0 = (start.y - center.y).atan2(start.x - center.x);
1024 let a1 = (end.y - center.y).atan2(end.x - center.x);
1025 let tau = core::f64::consts::TAU;
1026 let sweep = ((a1 - a0).rem_euclid(tau) + tau) % tau;
1027 if sweep == 0.0 { tau } else { sweep }
1028}
1029
1030fn arc_angle_offset(center: DVec2, start: DVec2, p: DVec2) -> f64 {
1032 let a0 = (start.y - center.y).atan2(start.x - center.x);
1033 let ap = (p.y - center.y).atan2(p.x - center.x);
1034 (ap - a0).rem_euclid(core::f64::consts::TAU)
1035}
1036
1037fn arc_contains_angle(center: DVec2, start: DVec2, end: DVec2, p: DVec2) -> bool {
1040 let off = arc_angle_offset(center, start, p);
1041 off > 0.0 && off < arc_sweep_angle(center, start, end)
1042}
1043
1044fn segment_arc_intersections(
1048 a0: DVec2,
1049 a1: DVec2,
1050 center: DVec2,
1051 start: DVec2,
1052 end: DVec2,
1053) -> Vec<DVec2> {
1054 let r0 = (start - center).length();
1055 let r1 = (end - center).length();
1056 let sweep = arc_sweep_angle(center, start, end);
1057 segment_circle_intersections(a0, a1, center, (r0 + r1) * 0.5)
1058 .into_iter()
1059 .filter(|p| arc_contains_angle(center, start, end, *p))
1060 .map(|p| {
1061 let off = arc_angle_offset(center, start, p);
1062 let a = (start.y - center.y).atan2(start.x - center.x) + off;
1063 let r = r0 + (r1 - r0) * (off / sweep);
1064 center + DVec2::new(a.cos(), a.sin()) * r
1065 })
1066 .collect()
1067}
1068
1069fn point_segment_distance(p: DVec2, a: DVec2, b: DVec2) -> f64 {
1071 let ab = b - a;
1072 let len_sq = ab.length_squared();
1073 if len_sq == 0.0 {
1074 return (p - a).length();
1075 }
1076 let t = ((p - a).dot(ab) / len_sq).clamp(0.0, 1.0);
1077 (a + ab * t - p).length()
1078}
1079
1080enum OtherSplit {
1082 Line,
1083 Arc,
1084 Circle {
1087 xj: DVec2,
1088 },
1089}
1090
1091impl SketchData {
1092 pub fn nearest_intersection(
1097 &self,
1098 cursor: DVec2,
1099 tolerance: f64,
1100 ) -> Option<SketchIntersection> {
1101 let mut best: Option<SketchIntersection> = None;
1102 let mut best_d = tolerance;
1103 for (i, ea) in self.entities.iter().enumerate() {
1104 for eb in &self.entities[i + 1..] {
1105 for pos in self.pair_intersections(ea, eb) {
1106 let d = (pos - cursor).length();
1107 if d <= best_d {
1108 best_d = d;
1109 best = Some(SketchIntersection {
1110 a: ea.id,
1111 b: eb.id,
1112 pos,
1113 });
1114 }
1115 }
1116 }
1117 }
1118 best
1119 }
1120
1121 fn pair_intersections(&self, a: &SketchEntity, b: &SketchEntity) -> Vec<DVec2> {
1124 let (line, other) = match (a.kind, b.kind) {
1125 (SketchEntityKind::Line { .. }, _) => (a, b),
1126 (_, SketchEntityKind::Line { .. }) => (b, a),
1127 _ => return Vec::new(),
1128 };
1129 let SketchEntityKind::Line { p0, p1 } = line.kind else {
1130 return Vec::new();
1131 };
1132 let (Some(l0), Some(l1)) = (self.point_pos(p0), self.point_pos(p1)) else {
1133 return Vec::new();
1134 };
1135 match other.kind {
1136 SketchEntityKind::Line { p0, p1 } => {
1137 let (Some(m0), Some(m1)) = (self.point_pos(p0), self.point_pos(p1)) else {
1138 return Vec::new();
1139 };
1140 segment_segment_intersection(l0, l1, m0, m1)
1141 .into_iter()
1142 .collect()
1143 }
1144 SketchEntityKind::Circle { center, radius } => {
1145 let Some(c) = self.point_pos(center) else {
1146 return Vec::new();
1147 };
1148 segment_circle_intersections(l0, l1, c, radius)
1149 }
1150 SketchEntityKind::Arc { center, start, end } => {
1151 let (Some(c), Some(s), Some(e)) = (
1152 self.point_pos(center),
1153 self.point_pos(start),
1154 self.point_pos(end),
1155 ) else {
1156 return Vec::new();
1157 };
1158 segment_arc_intersections(l0, l1, c, s, e)
1159 }
1160 }
1161 }
1162
1163 pub fn split_at(
1180 &mut self,
1181 a: SketchId,
1182 b: SketchId,
1183 pos: DVec2,
1184 tolerance: f64,
1185 ) -> Result<Vec<SketchId>, &'static str> {
1186 let ea = *self.entity(a).ok_or("unknown entity")?;
1187 let eb = *self.entity(b).ok_or("unknown entity")?;
1188 let (line_e, other_e) = match (ea.kind, eb.kind) {
1189 (SketchEntityKind::Line { .. }, _) => (ea, eb),
1190 (_, SketchEntityKind::Line { .. }) => (eb, ea),
1191 _ => return Err("unsupported entity pair"),
1192 };
1193 let SketchEntityKind::Line { p0: lp0, p1: lp1 } = line_e.kind else {
1194 return Err("unsupported entity pair");
1195 };
1196
1197 let hits = self.pair_intersections(&line_e, &other_e);
1198 let xi = *hits
1199 .iter()
1200 .min_by(|p, q| (**p - pos).length().total_cmp(&(**q - pos).length()))
1201 .ok_or("no intersection")?;
1202
1203 let far = |p: Option<DVec2>| p.is_some_and(|p| (xi - p).length() > tolerance);
1204 let line_splits = far(self.point_pos(lp0)) && far(self.point_pos(lp1));
1205 let other_plan = match other_e.kind {
1206 SketchEntityKind::Line { p0, p1 } => {
1207 (far(self.point_pos(p0)) && far(self.point_pos(p1))).then_some(OtherSplit::Line)
1208 }
1209 SketchEntityKind::Arc { start, end, .. } => {
1210 (far(self.point_pos(start)) && far(self.point_pos(end))).then_some(OtherSplit::Arc)
1211 }
1212 SketchEntityKind::Circle { .. } => {
1213 (hits.len() == 2 && (hits[0] - hits[1]).length() > tolerance).then(|| {
1214 let xj = if (hits[0] - xi).length() < (hits[1] - xi).length() {
1215 hits[1]
1216 } else {
1217 hits[0]
1218 };
1219 OtherSplit::Circle { xj }
1220 })
1221 }
1222 };
1223 if !line_splits && other_plan.is_none() {
1224 return Err("intersection too close to endpoints");
1225 }
1226
1227 let x = self.add_point(xi);
1228 let mut new_ids = Vec::new();
1229 let mut splits: Vec<(SketchId, SketchId)> = Vec::new(); if line_splits {
1232 if let Some(e) = self.entity_mut(line_e.id) {
1233 e.kind = SketchEntityKind::Line { p0: lp0, p1: x };
1234 }
1235 let id = self.alloc_id();
1236 self.entities.push(SketchEntity {
1237 id,
1238 kind: SketchEntityKind::Line { p0: x, p1: lp1 },
1239 construction: line_e.construction,
1240 });
1241 new_ids.push(id);
1242 splits.push((line_e.id, id));
1243 }
1244 match (other_plan, other_e.kind) {
1245 (Some(OtherSplit::Line), SketchEntityKind::Line { p0, p1 }) => {
1246 if let Some(e) = self.entity_mut(other_e.id) {
1247 e.kind = SketchEntityKind::Line { p0, p1: x };
1248 }
1249 let id = self.alloc_id();
1250 self.entities.push(SketchEntity {
1251 id,
1252 kind: SketchEntityKind::Line { p0: x, p1 },
1253 construction: other_e.construction,
1254 });
1255 new_ids.push(id);
1256 splits.push((other_e.id, id));
1257 }
1258 (Some(OtherSplit::Arc), SketchEntityKind::Arc { center, start, end }) => {
1259 if let Some(e) = self.entity_mut(other_e.id) {
1260 e.kind = SketchEntityKind::Arc {
1261 center,
1262 start,
1263 end: x,
1264 };
1265 }
1266 let id = self.alloc_id();
1267 self.entities.push(SketchEntity {
1268 id,
1269 kind: SketchEntityKind::Arc {
1270 center,
1271 start: x,
1272 end,
1273 },
1274 construction: other_e.construction,
1275 });
1276 new_ids.push(id);
1277 splits.push((other_e.id, id));
1278 }
1279 (Some(OtherSplit::Circle { xj }), SketchEntityKind::Circle { center, .. }) => {
1280 let xj_id = self.add_point(xj);
1281 if let Some(e) = self.entity_mut(other_e.id) {
1282 e.kind = SketchEntityKind::Arc {
1283 center,
1284 start: x,
1285 end: xj_id,
1286 };
1287 }
1288 let id = self.alloc_id();
1289 self.entities.push(SketchEntity {
1290 id,
1291 kind: SketchEntityKind::Arc {
1292 center,
1293 start: xj_id,
1294 end: x,
1295 },
1296 construction: other_e.construction,
1297 });
1298 new_ids.push(id);
1299 splits.push((other_e.id, id));
1300 }
1301 _ => {}
1302 }
1303
1304 for (old, new) in splits {
1305 self.retarget_split_constraints(old, new);
1306 }
1307 Ok(new_ids)
1308 }
1309
1310 fn retarget_split_constraints(&mut self, old: SketchId, new: SketchId) {
1315 let mut constraints = core::mem::take(&mut self.constraints);
1316 let mut additions: Vec<SketchConstraint> = Vec::new();
1317 for c in &mut constraints {
1318 match c {
1319 SketchConstraint::Horizontal { line } if *line == old => {
1320 additions.push(SketchConstraint::Horizontal { line: new });
1321 }
1322 SketchConstraint::Vertical { line } if *line == old => {
1323 additions.push(SketchConstraint::Vertical { line: new });
1324 }
1325 SketchConstraint::Parallel { a, b } => {
1326 if *a == old {
1327 additions.push(SketchConstraint::Parallel { a: new, b: *b });
1328 }
1329 if *b == old {
1330 additions.push(SketchConstraint::Parallel { a: *a, b: new });
1331 }
1332 }
1333 SketchConstraint::Perpendicular { a, b } => {
1334 if *a == old {
1335 additions.push(SketchConstraint::Perpendicular { a: new, b: *b });
1336 }
1337 if *b == old {
1338 additions.push(SketchConstraint::Perpendicular { a: *a, b: new });
1339 }
1340 }
1341 SketchConstraint::Radius { entity, r } if *entity == old => {
1342 additions.push(SketchConstraint::Radius { entity: new, r: *r });
1343 }
1344 SketchConstraint::Tangent { line, entity } => {
1345 if *line == old
1346 && let Some(target) = self.entity_center_pos(*entity)
1347 && self.entity_distance_to(new, target)
1348 < self.entity_distance_to(old, target)
1349 {
1350 *line = new;
1351 }
1352 if *entity == old
1353 && let Some(target) = self.closest_point_on_line_entity(*line, old)
1354 && self.entity_distance_to(new, target)
1355 < self.entity_distance_to(old, target)
1356 {
1357 *entity = new;
1358 }
1359 }
1360 SketchConstraint::PointOnCurve { point, entity } if *entity == old => {
1361 if let Some(target) = self.point_pos(*point)
1362 && self.entity_distance_to(new, target)
1363 < self.entity_distance_to(old, target)
1364 {
1365 *entity = new;
1366 }
1367 }
1368 _ => {}
1369 }
1370 }
1371 constraints.extend(additions);
1372 self.constraints = constraints;
1373 }
1374
1375 fn entity_center_pos(&self, id: SketchId) -> Option<DVec2> {
1377 match self.entity(id)?.kind {
1378 SketchEntityKind::Circle { center, .. } | SketchEntityKind::Arc { center, .. } => {
1379 self.point_pos(center)
1380 }
1381 SketchEntityKind::Line { .. } => None,
1382 }
1383 }
1384
1385 fn closest_point_on_line_entity(&self, line: SketchId, curve: SketchId) -> Option<DVec2> {
1388 let SketchEntityKind::Line { p0, p1 } = self.entity(line)?.kind else {
1389 return None;
1390 };
1391 let (a, b) = (self.point_pos(p0)?, self.point_pos(p1)?);
1392 let c = self.entity_center_pos(curve)?;
1393 let ab = b - a;
1394 let len_sq = ab.length_squared();
1395 if len_sq == 0.0 {
1396 return Some(a);
1397 }
1398 let t = ((c - a).dot(ab) / len_sq).clamp(0.0, 1.0);
1399 Some(a + ab * t)
1400 }
1401
1402 fn entity_distance_to(&self, id: SketchId, p: DVec2) -> f64 {
1405 let Some(e) = self.entity(id) else {
1406 return f64::INFINITY;
1407 };
1408 match e.kind {
1409 SketchEntityKind::Line { p0, p1 } => match (self.point_pos(p0), self.point_pos(p1)) {
1410 (Some(a), Some(b)) => point_segment_distance(p, a, b),
1411 _ => f64::INFINITY,
1412 },
1413 SketchEntityKind::Circle { center, radius } => match self.point_pos(center) {
1414 Some(c) => ((p - c).length() - radius).abs(),
1415 None => f64::INFINITY,
1416 },
1417 SketchEntityKind::Arc { center, start, end } => {
1418 let (Some(c), Some(s), Some(e)) = (
1419 self.point_pos(center),
1420 self.point_pos(start),
1421 self.point_pos(end),
1422 ) else {
1423 return f64::INFINITY;
1424 };
1425 if arc_contains_angle(c, s, e, p) {
1426 let r0 = (s - c).length();
1427 let r1 = (e - c).length();
1428 let t = arc_angle_offset(c, s, p) / arc_sweep_angle(c, s, e);
1429 ((p - c).length() - (r0 + (r1 - r0) * t)).abs()
1430 } else {
1431 (p - s).length().min((p - e).length())
1432 }
1433 }
1434 }
1435 }
1436}
1437
1438#[cfg(test)]
1439mod tests {
1440 use super::*;
1441
1442 fn rect_sketch() -> SketchData {
1443 let mut data = SketchData::default();
1444 data.add_rect(DVec2::ZERO, DVec2::new(2.0, 1.0));
1445 data
1446 }
1447
1448 #[test]
1449 fn rect_forms_one_ccw_loop() {
1450 let profile = profile_loops(&rect_sketch(), 32);
1451 assert_eq!(profile.loops.len(), 1);
1452 assert_eq!(profile.open_chains, 0);
1453 assert_eq!(profile.loops[0].len(), 4);
1454 assert!(signed_area_2x(&profile.loops[0]) > 0.0, "outer must be CCW");
1455 assert!((signed_area_2x(&profile.loops[0]).abs() / 2.0 - 2.0).abs() < 1e-9);
1456 }
1457
1458 #[test]
1459 fn circle_inside_rect_becomes_cw_hole() {
1460 let mut data = rect_sketch();
1461 data.add_circle(DVec2::new(1.0, 0.5), 0.25);
1462 let profile = profile_loops(&data, 32);
1463 assert_eq!(profile.loops.len(), 2);
1464 let (outer, hole) = if profile.loops[0].len() == 4 {
1465 (&profile.loops[0], &profile.loops[1])
1466 } else {
1467 (&profile.loops[1], &profile.loops[0])
1468 };
1469 assert!(signed_area_2x(outer) > 0.0);
1470 assert!(signed_area_2x(hole) < 0.0, "hole must be CW");
1471 }
1472
1473 #[test]
1474 fn open_chain_is_skipped_and_counted() {
1475 let mut data = SketchData::default();
1476 let a = data.add_point(DVec2::ZERO);
1477 let b = data.add_point(DVec2::new(1.0, 0.0));
1478 let c = data.add_point(DVec2::new(1.0, 1.0));
1479 data.add_line(a, b);
1480 data.add_line(b, c);
1481 let profile = profile_loops(&data, 32);
1482 assert!(profile.loops.is_empty());
1483 assert_eq!(profile.open_chains, 1);
1484 }
1485
1486 #[test]
1487 fn delete_point_cascades_and_removes_orphans() {
1488 let mut data = SketchData::default();
1491 let lone = data.add_point(DVec2::ZERO);
1492 data.constraints.push(SketchConstraint::Fixed {
1493 point: lone,
1494 x: 0.0,
1495 y: 0.0,
1496 });
1497 data.delete_point(lone);
1498 assert!(data.points.is_empty());
1499 assert!(data.constraints.is_empty());
1500
1501 let mut data = SketchData::default();
1504 let a = data.add_point(DVec2::ZERO);
1505 let b = data.add_point(DVec2::new(1.0, 0.0));
1506 let line = data.add_line(a, b);
1507 data.constraints.push(SketchConstraint::Horizontal { line });
1508 data.constraints
1509 .push(SketchConstraint::Distance { a, b, d: 1.0 });
1510 data.delete_point(a);
1511 assert!(data.points.is_empty());
1512 assert!(data.entities.is_empty());
1513 assert!(data.constraints.is_empty());
1514
1515 let mut data = SketchData::default();
1517 let a = data.add_point(DVec2::ZERO);
1518 let b = data.add_point(DVec2::new(1.0, 0.0));
1519 data.add_line(a, b);
1520 let lone = data.add_point(DVec2::new(5.0, 5.0));
1521 data.delete_point(lone);
1522 assert_eq!(data.points.len(), 2);
1523 assert_eq!(data.entities.len(), 1);
1524 }
1525
1526 #[test]
1527 fn loop_survives_dangling_line_at_any_corner() {
1528 let corners = [
1531 DVec2::ZERO,
1532 DVec2::new(2.0, 0.0),
1533 DVec2::new(2.0, 1.0),
1534 DVec2::new(0.0, 1.0),
1535 ];
1536 for corner in 0..4 {
1537 let mut data = SketchData::default();
1538 let pts: Vec<SketchId> = corners.iter().map(|&c| data.add_point(c)).collect();
1539 for i in 0..4 {
1540 data.add_line(pts[i], pts[(i + 1) % 4]);
1541 }
1542 let tip = data.add_point(DVec2::new(3.0, 3.0));
1543 data.add_line(pts[corner], tip);
1544 let profile = profile_loops(&data, 32);
1545 assert_eq!(profile.loops.len(), 1, "corner {corner}");
1546 assert_eq!(profile.open_chains, 1, "corner {corner}");
1547 assert!((signed_area_2x(&profile.loops[0]).abs() / 2.0 - 2.0).abs() < 1e-9);
1548 }
1549 }
1550
1551 #[test]
1552 fn loop_survives_multi_edge_dangle() {
1553 let mut data = SketchData::default();
1556 let corners = [
1557 DVec2::ZERO,
1558 DVec2::new(2.0, 0.0),
1559 DVec2::new(2.0, 1.0),
1560 DVec2::new(0.0, 1.0),
1561 ];
1562 let pts: Vec<SketchId> = corners.iter().map(|&c| data.add_point(c)).collect();
1563 for i in 0..4 {
1564 data.add_line(pts[i], pts[(i + 1) % 4]);
1565 }
1566 let mid = data.add_point(DVec2::new(3.0, 1.0));
1567 let tip = data.add_point(DVec2::new(4.0, 2.0));
1568 data.add_line(pts[2], mid);
1569 data.add_line(mid, tip);
1570 let profile = profile_loops(&data, 32);
1571 assert_eq!(profile.loops.len(), 1);
1572 assert_eq!(profile.open_chains, 1);
1573 }
1574
1575 #[test]
1576 fn coincident_endpoints_close_a_loop() {
1577 let mut data = SketchData::default();
1578 let a0 = data.add_point(DVec2::ZERO);
1580 let b0 = data.add_point(DVec2::new(2.0, 0.0));
1581 let b1 = data.add_point(DVec2::new(2.0, 0.0));
1582 let c0 = data.add_point(DVec2::new(1.0, 1.5));
1583 let c1 = data.add_point(DVec2::new(1.0, 1.5));
1584 let a1 = data.add_point(DVec2::ZERO);
1585 data.add_line(a0, b0);
1586 data.add_line(b1, c0);
1587 data.add_line(c1, a1);
1588 data.constraints
1589 .push(SketchConstraint::Coincident { a: b0, b: b1 });
1590 data.constraints
1591 .push(SketchConstraint::Coincident { a: c0, b: c1 });
1592 data.constraints
1593 .push(SketchConstraint::Coincident { a: a1, b: a0 });
1594 let profile = profile_loops(&data, 32);
1595 assert_eq!(profile.loops.len(), 1);
1596 assert_eq!(profile.open_chains, 0);
1597 assert_eq!(profile.loops[0].len(), 3);
1598 }
1599
1600 #[test]
1601 fn arc_edges_tessellate_into_the_loop() {
1602 let mut data = SketchData::default();
1603 let a = data.add_point(DVec2::new(-1.0, 0.0));
1605 let b = data.add_point(DVec2::new(1.0, 0.0));
1606 let center = data.add_point(DVec2::ZERO);
1607 data.add_line(a, b);
1608 data.add_arc(center, b, a);
1609 let profile = profile_loops(&data, 32);
1610 assert_eq!(profile.loops.len(), 1);
1611 assert_eq!(profile.open_chains, 0);
1612 assert!(profile.loops[0].len() > 10, "arc should tessellate");
1613 let area = signed_area_2x(&profile.loops[0]).abs() / 2.0;
1615 assert!((area - core::f64::consts::FRAC_PI_2).abs() < 0.05, "{area}");
1616 }
1617
1618 #[test]
1619 fn connected_components_split_by_shared_points_and_coincidence() {
1620 let mut data = SketchData::default();
1621 let rect = data.add_rect(DVec2::ZERO, DVec2::new(2.0, 1.0));
1623 let circle = data.add_circle(DVec2::new(5.0, 5.0), 1.0);
1625 let a0 = data.add_point(DVec2::new(8.0, 0.0));
1627 let a1 = data.add_point(DVec2::new(9.0, 0.0));
1628 let b0 = data.add_point(DVec2::new(9.0, 0.0));
1629 let b1 = data.add_point(DVec2::new(9.0, 1.0));
1630 let la = data.add_line(a0, a1);
1631 let lb = data.add_line(b0, b1);
1632 data.constraints
1633 .push(SketchConstraint::Coincident { a: a1, b: b0 });
1634 data.constraints.push(SketchConstraint::Distance {
1636 a: a0,
1637 b: data.points[0].id,
1638 d: 8.0,
1639 });
1640
1641 let components = data.connected_components();
1642 assert_eq!(components.len(), 3);
1643 assert_eq!(components[0], rect.to_vec());
1644 assert_eq!(components[1], vec![circle]);
1645 assert_eq!(components[2], vec![la, lb]);
1646 }
1647
1648 #[test]
1649 fn split_off_entities_moves_shape_and_drops_cross_constraints() {
1650 let mut data = SketchData::default();
1651 let rect = data.add_rect(DVec2::ZERO, DVec2::new(2.0, 1.0));
1652 let circle = data.add_circle(DVec2::new(5.0, 5.0), 1.0);
1653 data.constraints.push(SketchConstraint::Distance {
1655 a: data.points[0].id,
1656 b: data.points[4].id, d: 5.0,
1658 });
1659 let next_id = data.next_id;
1660 let n_constraints = data.constraints.len();
1661
1662 let taken = data.split_off_entities(&rect);
1663 assert_eq!(taken.entities.len(), 4);
1664 assert_eq!(taken.points.len(), 4);
1665 assert_eq!(taken.constraints.len(), 4);
1667 assert_eq!(taken.next_id, next_id);
1668
1669 assert_eq!(data.entities.len(), 1);
1670 assert_eq!(data.entities[0].id, circle);
1671 assert_eq!(data.points.len(), 1);
1672 assert_eq!(data.constraints.len(), n_constraints - 4 - 1);
1674 assert_eq!(data.next_id, next_id);
1675
1676 assert_eq!(profile_loops(&taken, 16).loops.len(), 1);
1678 assert_eq!(profile_loops(&data, 16).loops.len(), 1);
1679 }
1680
1681 #[test]
1682 fn delete_entity_cascades_points_and_constraints() {
1683 let mut data = SketchData::default();
1684 let lines = {
1685 let d = &mut data;
1686 d.add_rect(DVec2::ZERO, DVec2::new(1.0, 1.0))
1687 };
1688 let n_points = data.points.len();
1689 assert_eq!(n_points, 4);
1690 assert_eq!(data.constraints.len(), 4);
1691 data.delete_entity(lines[0]);
1692 assert_eq!(data.entities.len(), 3);
1693 assert_eq!(data.constraints.len(), 3);
1695 assert_eq!(data.points.len(), 4);
1697 data.delete_entity(lines[1]);
1698 data.delete_entity(lines[2]);
1699 data.delete_entity(lines[3]);
1700 assert!(data.points.is_empty());
1701 assert!(data.constraints.is_empty());
1702 }
1703
1704 #[test]
1705 fn segments_properly_intersect_policy() {
1706 let p = DVec2::new;
1707 assert!(segments_properly_intersect(
1709 p(0.0, 0.0),
1710 p(2.0, 2.0),
1711 p(0.0, 2.0),
1712 p(2.0, 0.0)
1713 ));
1714 assert!(!segments_properly_intersect(
1716 p(0.0, 0.0),
1717 p(2.0, 0.0),
1718 p(2.0, 0.0),
1719 p(3.0, 1.0)
1720 ));
1721 assert!(!segments_properly_intersect(
1723 p(0.0, 0.0),
1724 p(2.0, 0.0),
1725 p(1.0, 0.0),
1726 p(1.0, 5.0)
1727 ));
1728 assert!(!segments_properly_intersect(
1730 p(0.0, 0.0),
1731 p(2.0, 0.0),
1732 p(1.0, 0.0),
1733 p(3.0, 0.0)
1734 ));
1735 assert!(!segments_properly_intersect(
1737 p(0.0, 0.0),
1738 p(2.0, 0.0),
1739 p(0.0, 1.0),
1740 p(2.0, 1.0)
1741 ));
1742 }
1743
1744 #[test]
1745 fn crossing_loops_normalize_both_ccw() {
1746 let mut data = SketchData::default();
1747 data.add_rect(DVec2::ZERO, DVec2::new(2.0, 2.0));
1748 data.add_rect(DVec2::new(1.0, 1.0), DVec2::new(3.0, 3.0));
1749 let profile = profile_loops(&data, 32);
1750 assert_eq!(profile.loops.len(), 2);
1751 for l in &profile.loops {
1752 assert!(
1753 signed_area_2x(l) > 0.0,
1754 "crossing loops must both stay CCW outers (union)"
1755 );
1756 }
1757 }
1758
1759 #[test]
1760 fn three_level_nesting_alternates_windings() {
1761 let mut data = SketchData::default();
1762 data.add_rect(DVec2::ZERO, DVec2::new(8.0, 8.0));
1763 data.add_rect(DVec2::new(1.0, 1.0), DVec2::new(7.0, 7.0));
1764 data.add_rect(DVec2::new(2.0, 2.0), DVec2::new(6.0, 6.0));
1765 let profile = profile_loops(&data, 32);
1766 assert_eq!(profile.loops.len(), 3);
1767 let mut areas: Vec<f64> = profile.loops.iter().map(|l| signed_area(l)).collect();
1768 areas.sort_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("BUG: NaN area"));
1769 assert!(areas[0] > 0.0, "island must be CCW: {areas:?}");
1771 assert!(areas[1] < 0.0, "hole must be CW: {areas:?}");
1772 assert!(areas[2] > 0.0, "outer must be CCW: {areas:?}");
1773 }
1774
1775 #[test]
1776 fn loop_crossing_a_hole_stays_a_cutout() {
1777 let mut data = SketchData::default();
1778 data.add_rect(DVec2::ZERO, DVec2::new(6.0, 6.0));
1780 data.add_circle(DVec2::new(3.0, 3.0), 1.5);
1781 data.add_rect(DVec2::new(2.75, 2.75), DVec2::new(3.25, 5.0));
1782 let profile = profile_loops(&data, 32);
1783 assert_eq!(profile.loops.len(), 3);
1784 let mut areas: Vec<f64> = profile.loops.iter().map(|l| signed_area(l)).collect();
1785 areas.sort_by(|a, b| a.abs().partial_cmp(&b.abs()).expect("BUG: NaN area"));
1786 assert!(
1789 areas[0] < 0.0,
1790 "crossing rect must stay a cutout: {areas:?}"
1791 );
1792 assert!(areas[1] < 0.0, "circle hole must be CW: {areas:?}");
1793 assert!(areas[2] > 0.0, "outer must be CCW: {areas:?}");
1794 }
1795
1796 #[test]
1797 fn geometric_components_merges_nested_circle_into_rect() {
1798 let mut data = rect_sketch();
1799 let circle = data.add_circle(DVec2::new(1.0, 0.5), 0.25);
1800 assert_eq!(data.connected_components().len(), 2);
1801 let groups = data.geometric_components(32);
1802 assert_eq!(groups.len(), 1);
1803 assert_eq!(groups[0].len(), 5);
1804 assert!(groups[0].contains(&circle));
1805 }
1806
1807 #[test]
1808 fn geometric_components_merges_crossing_rects() {
1809 let mut data = SketchData::default();
1810 data.add_rect(DVec2::ZERO, DVec2::new(2.0, 2.0));
1811 data.add_rect(DVec2::new(1.0, 1.0), DVec2::new(3.0, 3.0));
1812 let groups = data.geometric_components(32);
1813 assert_eq!(groups.len(), 1);
1814 assert_eq!(groups[0].len(), 8);
1815 }
1816
1817 #[test]
1818 fn geometric_components_three_level_nesting_single_group() {
1819 let mut data = SketchData::default();
1820 data.add_rect(DVec2::ZERO, DVec2::new(8.0, 8.0));
1821 data.add_rect(DVec2::new(1.0, 1.0), DVec2::new(7.0, 7.0));
1822 data.add_rect(DVec2::new(2.0, 2.0), DVec2::new(6.0, 6.0));
1823 let groups = data.geometric_components(32);
1824 assert_eq!(groups.len(), 1, "nesting must merge transitively");
1825 assert_eq!(groups[0].len(), 12);
1826 }
1827
1828 #[test]
1829 fn geometric_components_nested_group_plus_far_shape() {
1830 let mut data = rect_sketch();
1831 let inner = data.add_circle(DVec2::new(1.0, 0.5), 0.25);
1832 let far = data.add_circle(DVec2::new(20.0, 20.0), 1.0);
1833 let groups = data.geometric_components(32);
1834 assert_eq!(groups.len(), 2);
1835 assert_eq!(groups[0].len(), 5, "rect + inner circle merge");
1836 assert!(groups[0].contains(&inner));
1837 assert_eq!(groups[1], vec![far]);
1838 }
1839
1840 #[test]
1841 fn geometric_components_open_chain_never_merges() {
1842 let mut data = SketchData::default();
1843 data.add_rect(DVec2::ZERO, DVec2::new(4.0, 4.0));
1844 let a = data.add_point(DVec2::new(1.0, 1.0));
1846 let b = data.add_point(DVec2::new(2.0, 1.0));
1847 let c = data.add_point(DVec2::new(2.0, 2.0));
1848 let la = data.add_line(a, b);
1849 let lb = data.add_line(b, c);
1850 let groups = data.geometric_components(32);
1851 assert_eq!(groups.len(), 2);
1852 assert_eq!(groups[1], vec![la, lb]);
1853 }
1854
1855 #[test]
1856 fn geometric_components_identical_duplicate_shapes() {
1857 let mut data = SketchData::default();
1858 data.add_rect(DVec2::ZERO, DVec2::new(2.0, 1.0));
1859 data.add_rect(DVec2::ZERO, DVec2::new(2.0, 1.0));
1860 let groups = data.geometric_components(32);
1862 assert_eq!(groups.len(), 1);
1863 assert_eq!(groups[0].len(), 8);
1864 let profile = profile_loops(&data, 32);
1866 assert_eq!(profile.loops.len(), 2);
1867 for l in &profile.loops {
1868 assert!(signed_area_2x(l) < 0.0);
1869 }
1870 }
1871
1872 #[cfg(feature = "serde")]
1873 #[test]
1874 fn sketch_data_roundtrips_ron() {
1875 let mut data = rect_sketch();
1876 let circle = data.add_circle(DVec2::new(1.0, 0.5), 0.25);
1877 data.set_construction(circle, true);
1878 data.constraints.push(SketchConstraint::Distance {
1879 a: data.points[0].id,
1880 b: data.points[1].id,
1881 d: 2.0,
1882 });
1883 let ron = ron::to_string(&data).expect("serialize");
1884 let back: SketchData = ron::from_str(&ron).expect("deserialize");
1885 assert_eq!(data, back);
1886 }
1887
1888 #[cfg(feature = "serde")]
1889 #[test]
1890 fn construction_defaults_false_on_old_files() {
1891 let ron = "(points:[(id:0,pos:(0.0,0.0)),(id:1,pos:(2.0,0.0))],\
1893 entities:[(id:2,kind:Line(p0:0,p1:1))],constraints:[],next_id:3)";
1894 let data: SketchData = ron::from_str(ron).expect("deserialize old file");
1895 assert!(!data.entities[0].construction);
1896 }
1897
1898 #[test]
1901 fn construction_entities_excluded_from_profile() {
1902 let mut data = rect_sketch();
1903 let diag = data.add_line(data.points[0].id, data.points[2].id);
1906 data.set_construction(diag, true);
1907 let profile = profile_loops(&data, 32);
1908 assert_eq!(profile.loops.len(), 1);
1909 assert_eq!(profile.open_chains, 0);
1910
1911 let circle = data.add_circle(DVec2::new(1.0, 0.5), 0.25);
1913 data.set_construction(circle, true);
1914 let profile = profile_loops(&data, 32);
1915 assert_eq!(profile.loops.len(), 1);
1916 }
1917
1918 #[test]
1919 fn geometric_components_ignore_construction_circle() {
1920 let mut data = rect_sketch();
1921 let circle = data.add_circle(DVec2::new(1.0, 0.5), 0.25);
1922 data.set_construction(circle, true);
1923 let groups = data.geometric_components(32);
1926 assert_eq!(groups.len(), 2);
1927 assert_eq!(groups[1], vec![circle]);
1928 }
1929
1930 #[test]
1933 fn segment_segment_intersection_point_and_policy() {
1934 let p = DVec2::new;
1935 let x = segment_segment_intersection(p(0.0, 0.0), p(2.0, 2.0), p(0.0, 2.0), p(2.0, 0.0))
1936 .expect("X-cross");
1937 assert!((x - p(1.0, 1.0)).length() < 1e-12);
1938 assert!(segment_segment_intersection(p(0., 0.), p(2., 0.), p(2., 0.), p(3., 1.)).is_none());
1940 assert!(segment_segment_intersection(p(0., 0.), p(2., 0.), p(1., 0.), p(1., 5.)).is_none());
1941 assert!(segment_segment_intersection(p(0., 0.), p(2., 0.), p(1., 0.), p(3., 0.)).is_none());
1942 assert!(segment_segment_intersection(p(0., 0.), p(2., 0.), p(0., 1.), p(2., 1.)).is_none());
1943 }
1944
1945 #[test]
1946 fn segment_circle_intersections_secant_tangent_miss() {
1947 let p = DVec2::new;
1948 let c = p(0.0, 0.0);
1949 let hits = segment_circle_intersections(p(-2.0, 0.0), p(2.0, 0.0), c, 1.0);
1951 assert_eq!(hits.len(), 2);
1952 assert!((hits[0] - p(-1.0, 0.0)).length() < 1e-9);
1953 assert!((hits[1] - p(1.0, 0.0)).length() < 1e-9);
1954 assert!(segment_circle_intersections(p(-2.0, 1.0), p(2.0, 1.0), c, 1.0).is_empty());
1956 assert!(segment_circle_intersections(p(-2.0, 3.0), p(2.0, 3.0), c, 1.0).is_empty());
1958 let hits = segment_circle_intersections(p(-2.0, 0.0), p(0.0, 0.0), c, 1.0);
1960 assert_eq!(hits.len(), 1);
1961 assert!((hits[0] - p(-1.0, 0.0)).length() < 1e-9);
1962 }
1963
1964 #[test]
1965 fn segment_arc_intersections_respect_sweep() {
1966 let p = DVec2::new;
1967 let (c, s, e) = (p(0.0, 0.0), p(1.0, 0.0), p(-1.0, 0.0));
1969 let hits = segment_arc_intersections(p(0.0, -2.0), p(0.0, 2.0), c, s, e);
1971 assert_eq!(hits.len(), 1);
1972 assert!((hits[0] - p(0.0, 1.0)).length() < 1e-9);
1973 assert!(segment_arc_intersections(p(-2.0, -0.5), p(2.0, -0.5), c, s, e).is_empty());
1975 }
1976
1977 #[test]
1978 fn nearest_intersection_picks_closest_within_tolerance() {
1979 let mut data = SketchData::default();
1980 let a0 = data.add_point(DVec2::new(-1.0, 0.0));
1981 let a1 = data.add_point(DVec2::new(1.0, 0.0));
1982 let h = data.add_line(a0, a1);
1983 let b0 = data.add_point(DVec2::new(-0.5, -1.0));
1984 let b1 = data.add_point(DVec2::new(-0.5, 1.0));
1985 let v1 = data.add_line(b0, b1);
1986 let c0 = data.add_point(DVec2::new(0.5, -1.0));
1987 let c1 = data.add_point(DVec2::new(0.5, 1.0));
1988 data.add_line(c0, c1);
1989
1990 let hit = data
1991 .nearest_intersection(DVec2::new(-0.4, 0.1), 0.5)
1992 .expect("hit");
1993 assert_eq!((hit.a, hit.b), (h, v1));
1994 assert!((hit.pos - DVec2::new(-0.5, 0.0)).length() < 1e-12);
1995 assert!(
1997 data.nearest_intersection(DVec2::new(5.0, 5.0), 0.5)
1998 .is_none()
1999 );
2000
2001 let mut data = SketchData::default();
2003 data.add_circle(DVec2::new(-0.5, 0.0), 1.0);
2004 data.add_circle(DVec2::new(0.5, 0.0), 1.0);
2005 assert!(
2006 data.nearest_intersection(DVec2::new(0.0, 0.8), 10.0)
2007 .is_none()
2008 );
2009 }
2010
2011 #[test]
2014 fn split_line_line_shares_one_point() {
2015 let mut data = SketchData::default();
2016 let a0 = data.add_point(DVec2::new(-1.0, 0.0));
2017 let a1 = data.add_point(DVec2::new(1.0, 0.0));
2018 let la = data.add_line(a0, a1);
2019 let b0 = data.add_point(DVec2::new(0.0, -1.0));
2020 let b1 = data.add_point(DVec2::new(0.0, 1.0));
2021 let lb = data.add_line(b0, b1);
2022 data.constraints
2023 .push(SketchConstraint::Horizontal { line: la });
2024 let n_points = data.points.len();
2025
2026 let new_ids = data
2027 .split_at(la, lb, DVec2::new(0.05, 0.05), 1e-6)
2028 .expect("split");
2029 assert_eq!(new_ids.len(), 2);
2030 assert_eq!(data.entities.len(), 4);
2031 assert_eq!(data.points.len(), n_points + 1, "one shared point");
2032 let x = data.points.last().expect("split point");
2033 assert!((DVec2::from_array(x.pos) - DVec2::ZERO).length() < 1e-12);
2034 let uses_x = data
2036 .entities
2037 .iter()
2038 .filter(|e| entity_point_ids(e).contains(&x.id))
2039 .count();
2040 assert_eq!(uses_x, 4);
2041 assert!(data.entity(la).is_some());
2043 assert!(data.entity(lb).is_some());
2044 let horizontals = data
2046 .constraints
2047 .iter()
2048 .filter(|c| matches!(c, SketchConstraint::Horizontal { .. }))
2049 .count();
2050 assert_eq!(horizontals, 2);
2051 }
2052
2053 #[test]
2054 fn split_line_arc_preserves_winding() {
2055 let mut data = SketchData::default();
2056 let c = data.add_point(DVec2::ZERO);
2058 let s = data.add_point(DVec2::new(1.0, 0.0));
2059 let e = data.add_point(DVec2::new(-1.0, 0.0));
2060 let arc = data.add_arc(c, s, e);
2061 let l0 = data.add_point(DVec2::new(0.0, -2.0));
2062 let l1 = data.add_point(DVec2::new(0.0, 2.0));
2063 let line = data.add_line(l0, l1);
2064
2065 let new_ids = data
2066 .split_at(line, arc, DVec2::new(0.0, 1.0), 1e-6)
2067 .expect("split");
2068 assert_eq!(new_ids.len(), 2);
2069 assert_eq!(data.entities.len(), 4);
2070 let sweeps: Vec<f64> = data
2071 .entities
2072 .iter()
2073 .filter_map(|ent| match ent.kind {
2074 SketchEntityKind::Arc { center, start, end } => Some(arc_sweep_angle(
2075 data.point_pos(center).unwrap(),
2076 data.point_pos(start).unwrap(),
2077 data.point_pos(end).unwrap(),
2078 )),
2079 _ => None,
2080 })
2081 .collect();
2082 assert_eq!(sweeps.len(), 2);
2083 let total: f64 = sweeps.iter().sum();
2084 assert!(
2085 (total - core::f64::consts::PI).abs() < 1e-9,
2086 "fragment sweeps must sum to the original half-turn: {sweeps:?}"
2087 );
2088 }
2089
2090 #[test]
2091 fn split_line_circle_makes_two_arcs() {
2092 let mut data = SketchData::default();
2093 let circle = data.add_circle(DVec2::ZERO, 1.0);
2094 let l0 = data.add_point(DVec2::new(-2.0, 0.0));
2095 let l1 = data.add_point(DVec2::new(2.0, 0.0));
2096 let line = data.add_line(l0, l1);
2097 data.constraints.push(SketchConstraint::Radius {
2098 entity: circle,
2099 r: 1.0,
2100 });
2101 let n_points = data.points.len();
2102
2103 let new_ids = data
2105 .split_at(line, circle, DVec2::new(0.9, 0.1), 1e-6)
2106 .expect("split");
2107 assert_eq!(new_ids.len(), 2);
2108 let arcs = data
2110 .entities
2111 .iter()
2112 .filter(|e| matches!(e.kind, SketchEntityKind::Arc { .. }))
2113 .count();
2114 let lines = data
2115 .entities
2116 .iter()
2117 .filter(|e| matches!(e.kind, SketchEntityKind::Line { .. }))
2118 .count();
2119 assert_eq!((arcs, lines), (2, 2));
2120 assert_eq!(data.points.len(), n_points + 2);
2122 let radii = data
2124 .constraints
2125 .iter()
2126 .filter(|c| matches!(c, SketchConstraint::Radius { .. }))
2127 .count();
2128 assert_eq!(radii, 2);
2129 let total: f64 = data
2131 .entities
2132 .iter()
2133 .filter_map(|ent| match ent.kind {
2134 SketchEntityKind::Arc { center, start, end } => Some(arc_sweep_angle(
2135 data.point_pos(center).unwrap(),
2136 data.point_pos(start).unwrap(),
2137 data.point_pos(end).unwrap(),
2138 )),
2139 _ => None,
2140 })
2141 .sum();
2142 assert!((total - core::f64::consts::TAU).abs() < 1e-9);
2143 }
2144
2145 #[test]
2146 fn split_line_ending_inside_circle_splits_line_only() {
2147 let mut data = SketchData::default();
2148 let circle = data.add_circle(DVec2::ZERO, 1.0);
2149 let l0 = data.add_point(DVec2::new(-2.0, 0.0));
2150 let l1 = data.add_point(DVec2::ZERO); let line = data.add_line(l0, l1);
2152
2153 let new_ids = data
2154 .split_at(line, circle, DVec2::new(-1.0, 0.0), 1e-6)
2155 .expect("split");
2156 assert_eq!(new_ids.len(), 1, "only the line splits");
2157 assert!(
2158 matches!(
2159 data.entity(circle).expect("circle").kind,
2160 SketchEntityKind::Circle { .. }
2161 ),
2162 "circle stays whole without a second crossing"
2163 );
2164 }
2165
2166 #[test]
2167 fn split_skips_near_endpoints() {
2168 let mut data = SketchData::default();
2169 let a0 = data.add_point(DVec2::new(0.0, 0.0));
2171 let a1 = data.add_point(DVec2::new(2.0, 0.0));
2172 let la = data.add_line(a0, a1);
2173 let b0 = data.add_point(DVec2::new(0.01, -1.0));
2174 let b1 = data.add_point(DVec2::new(0.01, 1.0));
2175 let lb = data.add_line(b0, b1);
2176
2177 let new_ids = data
2180 .split_at(la, lb, DVec2::new(0.01, 0.0), 0.05)
2181 .expect("split");
2182 assert_eq!(new_ids.len(), 1);
2183 assert_eq!(data.entities.len(), 3);
2184
2185 let mut data = SketchData::default();
2187 let a0 = data.add_point(DVec2::new(0.0, 0.0));
2188 let a1 = data.add_point(DVec2::new(2.0, 0.0));
2189 let la = data.add_line(a0, a1);
2190 let b0 = data.add_point(DVec2::new(0.01, -0.02));
2191 let b1 = data.add_point(DVec2::new(0.01, 0.02));
2192 let lb = data.add_line(b0, b1);
2193 let before = data.clone();
2194 assert!(data.split_at(la, lb, DVec2::new(0.01, 0.0), 0.05).is_err());
2195 assert_eq!(data, before);
2196 }
2197
2198 #[test]
2199 fn split_unsupported_pair_and_no_intersection_err() {
2200 let mut data = SketchData::default();
2201 let c1 = data.add_circle(DVec2::ZERO, 1.0);
2202 let c2 = data.add_circle(DVec2::new(1.0, 0.0), 1.0);
2203 assert_eq!(
2204 data.split_at(c1, c2, DVec2::ZERO, 1e-6),
2205 Err("unsupported entity pair")
2206 );
2207 let l0 = data.add_point(DVec2::new(5.0, 5.0));
2208 let l1 = data.add_point(DVec2::new(6.0, 5.0));
2209 let line = data.add_line(l0, l1);
2210 assert_eq!(
2211 data.split_at(line, c1, DVec2::ZERO, 1e-6),
2212 Err("no intersection")
2213 );
2214 }
2215
2216 #[test]
2217 fn split_retargets_tangent_and_point_on_curve() {
2218 let mut data = SketchData::default();
2220 let l0 = data.add_point(DVec2::new(-4.0, 1.0));
2221 let l1 = data.add_point(DVec2::new(4.0, 1.0));
2222 let line = data.add_line(l0, l1);
2223 let circle = data.add_circle(DVec2::new(-3.0, 0.0), 1.0);
2224 data.constraints.push(SketchConstraint::Tangent {
2225 line,
2226 entity: circle,
2227 });
2228 let v0 = data.add_point(DVec2::new(2.0, -1.0));
2231 let v1 = data.add_point(DVec2::new(2.0, 3.0));
2232 let vline = data.add_line(v0, v1);
2233 data.split_at(line, vline, DVec2::new(2.0, 1.0), 1e-6)
2234 .expect("split");
2235 assert!(
2236 data.constraints
2237 .iter()
2238 .any(|c| matches!(c, SketchConstraint::Tangent { line: l, .. } if *l == line)),
2239 "tangent stays on the fragment containing the foot"
2240 );
2241
2242 let mut data = SketchData::default();
2245 let circle = data.add_circle(DVec2::ZERO, 1.0);
2246 let p = data.add_point(DVec2::new(-1.0, 0.0));
2247 data.constraints.push(SketchConstraint::PointOnCurve {
2248 point: p,
2249 entity: circle,
2250 });
2251 let l0 = data.add_point(DVec2::new(0.5, -2.0));
2252 let l1 = data.add_point(DVec2::new(0.5, 2.0));
2253 let line = data.add_line(l0, l1);
2254 data.split_at(line, circle, DVec2::new(0.5, 0.9), 1e-6)
2255 .expect("split");
2256 let target = data
2257 .constraints
2258 .iter()
2259 .find_map(|c| match c {
2260 SketchConstraint::PointOnCurve { entity, .. } => Some(*entity),
2261 _ => None,
2262 })
2263 .expect("constraint kept");
2264 let d_target = data.entity_distance_to(target, DVec2::new(-1.0, 0.0));
2265 assert!(
2266 d_target < 1e-6,
2267 "PointOnCurve must follow the fragment containing the point: {d_target}"
2268 );
2269 }
2270
2271 #[test]
2272 fn split_inherits_construction_flag() {
2273 let mut data = SketchData::default();
2274 let a0 = data.add_point(DVec2::new(-1.0, 0.0));
2275 let a1 = data.add_point(DVec2::new(1.0, 0.0));
2276 let la = data.add_line(a0, a1);
2277 data.set_construction(la, true);
2278 let b0 = data.add_point(DVec2::new(0.0, -1.0));
2279 let b1 = data.add_point(DVec2::new(0.0, 1.0));
2280 let lb = data.add_line(b0, b1);
2281
2282 let new_ids = data.split_at(la, lb, DVec2::ZERO, 1e-6).expect("split");
2283 let frag_of = |old: SketchId| {
2284 data.entities.iter().find(|e| {
2285 new_ids.contains(&e.id) && e.construction == data.entity(old).unwrap().construction
2286 })
2287 };
2288 assert!(data.entity(la).expect("la").construction);
2289 assert!(frag_of(la).expect("la fragment").construction);
2290 assert!(!data.entity(lb).expect("lb").construction);
2291 }
2292}