1use super::*;
2
3#[derive(Clone, Debug, PartialEq)]
10pub struct SketchEntityRow {
11 pub kind: &'static str,
13 pub id: serde_json::Value,
15 pub label: String,
17 pub selected: bool,
19 pub construction: bool,
21 pub conflicting: bool,
24}
25
26#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub enum NoticeSeverity {
32 #[default]
33 Error,
34 Warning,
35 Info,
36}
37
38impl EngineState {
39 pub fn push_notice_as(&mut self, severity: NoticeSeverity, message: impl Into<String>) {
42 if severity == NoticeSeverity::Error {
43 return self.push_notice(message);
44 }
45 let message = message.into();
46 #[cfg(not(target_arch = "wasm32"))]
47 eprintln!("{message}");
48 self.graded_notices.push((severity, message));
49 if self.graded_notices.len() > 8 {
50 let overflow = self.graded_notices.len() - 8;
51 self.graded_notices.drain(0..overflow);
52 }
53 }
54
55 pub fn take_graded_notices(&mut self) -> Vec<(NoticeSeverity, String)> {
58 let errors = std::mem::take(&mut self.notices).into_iter().map(|text| (NoticeSeverity::Error, text));
59 errors.chain(std::mem::take(&mut self.graded_notices)).collect()
60 }
61
62 pub fn push_notice(&mut self, message: impl Into<String>) {
65 let message = message.into();
66 #[cfg(not(target_arch = "wasm32"))]
67 eprintln!("{message}");
68 self.notices.push(message);
69 if self.notices.len() > 8 {
70 let overflow = self.notices.len() - 8;
71 self.notices.drain(0..overflow);
72 }
73 }
74
75 pub fn take_notices(&mut self) -> Vec<String> {
78 self.take_graded_notices().into_iter().map(|(_, text)| text).collect()
79 }
80
81 pub(super) fn resolve_active_sketch(&mut self, context: &str) {
86 let error = self
87 .sketch_edit
88 .as_mut()
89 .and_then(|edit| edit.session.resolve().err());
90 if let Some(error) = error {
91 self.push_notice(format!("Sketch solve failed ({context}): {error}"));
92 }
93 }
94
95 fn sketch_ref_selected(session: &crate::sketch::SketchSession, kind: &str, id: &serde_json::Value) -> bool {
97 use crate::sketch::doc::id_key;
98 let key = id_key(id);
99 session.selection.iter().any(|r| {
100 r.get("kind").and_then(serde_json::Value::as_str) == Some(kind)
101 && r.get("id").map(id_key).as_deref() == Some(key.as_str())
102 })
103 }
104
105 pub fn sketch_point_rows(&self) -> Vec<SketchEntityRow> {
108 use crate::sketch::doc::id_key;
109 let Some(edit) = self.sketch_edit.as_ref() else {
110 return Vec::new();
111 };
112 let session = &edit.session;
113 session
114 .doc
115 .points
116 .iter()
117 .map(|p| {
118 let grounded = session.doc.constraints.iter().any(|c| {
119 c.ctype() == Some("⏚")
120 && c.points().first().map(id_key).as_deref() == Some(id_key(&p.id).as_str())
121 });
122 let mut marks = String::new();
123 if p.external_reference {
124 marks.push_str(" \u{26D3}"); }
126 if p.construction {
127 marks.push_str(" ◐");
128 }
129 if grounded {
130 marks.push_str(" ⏚");
131 }
132 SketchEntityRow {
133 kind: "point",
134 id: p.id.clone(),
135 label: format!("P{} ({:.1}, {:.1}){marks}", id_key(&p.id), p.x, p.y),
136 selected: Self::sketch_ref_selected(session, "point", &p.id),
137 construction: p.construction,
138 conflicting: false,
139 }
140 })
141 .collect()
142 }
143
144 pub fn sketch_geometry_rows(&self) -> Vec<SketchEntityRow> {
147 use crate::sketch::doc::id_key;
148 let Some(edit) = self.sketch_edit.as_ref() else {
149 return Vec::new();
150 };
151 let session = &edit.session;
152 session
153 .doc
154 .geometries
155 .iter()
156 .map(|g| {
157 let pts = g
158 .points
159 .iter()
160 .map(id_key)
161 .collect::<Vec<_>>()
162 .join(",");
163 let construction = g.construction();
164 let mark = if construction { " ◐" } else { "" };
165 SketchEntityRow {
166 kind: "geometry",
167 id: g.id.clone(),
168 label: format!("{}:{}{mark} [{pts}]", g.geom_type, id_key(&g.id)),
169 selected: Self::sketch_ref_selected(session, "geometry", &g.id),
170 construction,
171 conflicting: false,
172 }
173 })
174 .collect()
175 }
176
177 pub fn sketch_constraint_rows(&self) -> Vec<SketchEntityRow> {
180 use crate::sketch::doc::id_key;
181 let Some(edit) = self.sketch_edit.as_ref() else {
182 return Vec::new();
183 };
184 let session = &edit.session;
185 session
186 .doc
187 .constraints
188 .iter()
189 .filter_map(|c| {
190 let id = c.raw.get("id")?.clone();
191 let ctype = c.ctype().unwrap_or("?");
192 let value = c
193 .raw
194 .get("value")
195 .and_then(serde_json::Value::as_f64)
196 .map(|v| format!(" {v:.3}"))
197 .unwrap_or_default();
198 let pts = c
199 .points()
200 .iter()
201 .map(id_key)
202 .collect::<Vec<_>>()
203 .join(",");
204 Some(SketchEntityRow {
205 kind: "constraint",
206 id: id.clone(),
207 label: format!("{} {ctype}{value} [{pts}]", id_key(&id)),
208 selected: Self::sketch_ref_selected(session, "constraint", &id),
209 construction: false,
210 conflicting: session.diagnostics.constraint_conflicting(&id),
211 })
212 })
213 .collect()
214 }
215
216 pub fn sketch_select_entity(&mut self, kind: &str, id: serde_json::Value, additive: bool) {
221 let toggles = self.settings.multi_select == crate::style::MultiSelectMode::ClickToggles;
222 let entity_ref = serde_json::json!({ "kind": kind, "id": id });
223 let Some(edit) = self.sketch_edit.as_mut() else {
224 return;
225 };
226 if !additive && !toggles {
227 edit.session.clear_selection();
228 }
229 edit.session.toggle_selection(entity_ref);
230 self.refresh_sketch_overlay();
231 self.dirty = true;
232 }
233
234 pub fn sketch_hover_entity(&mut self, kind: &str, id: serde_json::Value) {
238 if self.sketch_edit.is_none() {
239 return;
240 }
241 let entity_ref = serde_json::json!({ "kind": kind, "id": id });
244 self.set_sketch_hover(Some(entity_ref));
245 self.sketch_list_hover_active = true;
246 }
247
248 pub fn take_sketch_list_hover(&mut self) -> bool {
252 std::mem::take(&mut self.sketch_list_hover_active)
253 }
254
255 pub fn sketch_solver_settings(&self) -> Option<crate::sketch::SketchSolverSettings> {
258 self.sketch_edit
259 .as_ref()
260 .map(|edit| edit.session.solver_settings.clone())
261 }
262
263 pub fn sketch_set_solver_settings(&mut self, settings: crate::sketch::SketchSolverSettings) {
266 if let Some(edit) = self.sketch_edit.as_mut() {
267 edit.session.solver_settings = settings;
268 } else {
269 return;
270 }
271 self.resolve_active_sketch("solver settings");
272 self.refresh_sketch_overlay();
273 self.dirty = true;
274 }
275}
276
277#[derive(Clone, Debug, PartialEq)]
281pub struct SketchConstraintAction {
282 pub symbol: String,
285 pub label: String,
287 pub dimensional: bool,
290}
291
292impl EngineState {
293 pub fn sketch_applicable_constraints(&self) -> Vec<SketchConstraintAction> {
303 match self.sketch_edit.as_ref() {
304 Some(edit) => sketch_palette_for(&edit.session),
305 None => Vec::new(),
306 }
307 }
308
309 pub fn sketch_add_constraint(&mut self, symbol: &str) -> bool {
314 let added = match self.sketch_edit.as_mut() {
317 Some(edit) => {
318 edit.record_undo();
319 sketch_build_and_add_constraint(&mut edit.session, symbol)
320 }
321 None => return false,
322 };
323 if added {
324 self.resolve_active_sketch("add-constraint");
325 self.refresh_sketch_overlay();
326 self.dirty = true;
327 } else if let Some(edit) = self.sketch_edit.as_mut() {
328 edit.undo_stack.pop();
329 }
330 added
331 }
332
333 pub fn sketch_toggle_ground(&mut self) -> bool {
339 use crate::sketch::doc::id_key;
340 use std::collections::HashSet;
341
342 let Some(edit) = self.sketch_edit.as_mut() else {
343 return false;
344 };
345 let sel_ids: Vec<serde_json::Value> = edit
346 .session
347 .selection
348 .iter()
349 .filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("point"))
350 .filter_map(|r| r.get("id").cloned())
351 .collect();
352 if sel_ids.is_empty() {
353 return false;
354 }
355 edit.record_undo();
357 let doc = &mut edit.session.doc;
358 let has_ground = |doc: &crate::sketch::SketchDoc, id: &serde_json::Value| -> bool {
359 doc.constraints.iter().any(|c| {
360 c.ctype() == Some("⏚")
361 && c.points().first().map(id_key) == Some(id_key(id))
362 })
363 };
364 let all_grounded = sel_ids.iter().all(|id| has_ground(doc, id));
365 if all_grounded {
366 let sel_keys: HashSet<String> = sel_ids.iter().map(id_key).collect();
367 doc.constraints.retain(|c| {
368 if c.ctype() != Some("⏚") {
369 return true;
370 }
371 match c.points().first() {
372 Some(p) => !sel_keys.contains(&id_key(p)),
373 None => true,
374 }
375 });
376 for id in &sel_ids {
377 if let Some(p) = doc.point_mut(id) {
378 p.fixed = false;
379 }
380 }
381 } else {
382 for id in &sel_ids {
383 if has_ground(doc, id) {
384 continue;
385 }
386 let cid = doc.next_constraint_id();
387 let mut raw = serde_json::Map::new();
388 raw.insert("id".to_string(), cid);
389 raw.insert("type".to_string(), serde_json::Value::String("⏚".to_string()));
390 raw.insert(
391 "points".to_string(),
392 serde_json::Value::Array(vec![id.clone()]),
393 );
394 doc.constraints.push(crate::sketch::SketchConstraint { raw });
395 if let Some(p) = doc.point_mut(id) {
396 p.fixed = true;
397 }
398 }
399 }
400 self.resolve_active_sketch("toggle-ground");
401 self.refresh_sketch_overlay();
402 self.dirty = true;
403 true
404 }
405
406 pub fn sketch_toggle_construction(&mut self) -> bool {
411 let Some(edit) = self.sketch_edit.as_mut() else {
412 return false;
413 };
414 let mut pt_ids: Vec<serde_json::Value> = Vec::new();
415 let mut geo_ids: Vec<serde_json::Value> = Vec::new();
416 for r in &edit.session.selection {
417 match r.get("kind").and_then(|v| v.as_str()) {
418 Some("point") => {
419 if let Some(id) = r.get("id") {
420 pt_ids.push(id.clone());
421 }
422 }
423 Some("geometry") => {
424 if let Some(id) = r.get("id") {
425 geo_ids.push(id.clone());
426 }
427 }
428 _ => {}
429 }
430 }
431 if pt_ids.is_empty() && geo_ids.is_empty() {
432 return false;
433 }
434 edit.record_undo();
436 let doc = &mut edit.session.doc;
437 let all_construction = pt_ids
438 .iter()
439 .all(|id| doc.point(id).map_or(false, |p| p.construction))
440 && geo_ids
441 .iter()
442 .all(|id| doc.geometry(id).map_or(false, |g| g.construction()));
443 let next = !all_construction;
444 for id in &pt_ids {
445 if let Some(p) = doc.point_mut(id) {
446 p.construction = next;
447 }
448 }
449 for id in &geo_ids {
450 if let Some(g) = doc.geometry_mut(id) {
451 g.extra
452 .insert("construction".to_string(), serde_json::Value::Bool(next));
453 }
454 }
455 self.resolve_active_sketch("toggle-construction");
456 self.refresh_sketch_overlay();
457 self.dirty = true;
458 true
459 }
460
461 pub fn sketch_cleanup_unused_points(&mut self) -> bool {
465 use crate::sketch::doc::id_key;
466 use std::collections::HashSet;
467
468 let Some(edit) = self.sketch_edit.as_mut() else {
469 return false;
470 };
471 edit.record_undo();
473 let doc = &mut edit.session.doc;
474 let mut used: HashSet<String> = HashSet::new();
475 for g in &doc.geometries {
476 for pid in &g.points {
477 used.insert(id_key(pid));
478 }
479 }
480 for c in &doc.constraints {
481 for pid in c.points() {
482 used.insert(id_key(pid));
483 }
484 }
485 let before = doc.points.len();
486 doc.points.retain(|p| used.contains(&id_key(&p.id)));
487 let removed = doc.points.len() != before;
488 if !removed {
489 edit.undo_stack.pop();
490 return false;
491 }
492 self.resolve_active_sketch("cleanup");
493 self.refresh_sketch_overlay();
494 self.dirty = true;
495 true
496 }
497
498 pub fn sketch_constraint_count(&self) -> usize {
501 self.sketch_edit
502 .as_ref()
503 .map_or(0, |edit| edit.session.doc.constraints.len())
504 }
505
506 pub fn sketch_selection_all_grounded(&self) -> Option<bool> {
510 use crate::sketch::doc::id_key;
511 let edit = self.sketch_edit.as_ref()?;
512 let doc = &edit.session.doc;
513 let sel: Vec<&serde_json::Value> = edit
514 .session
515 .selection
516 .iter()
517 .filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("point"))
518 .filter_map(|r| r.get("id"))
519 .collect();
520 if sel.is_empty() {
521 return None;
522 }
523 let all = sel.iter().all(|id| {
524 doc.constraints.iter().any(|c| {
525 c.ctype() == Some("⏚")
526 && c.points().first().map(id_key) == Some(id_key(id))
527 })
528 });
529 Some(all)
530 }
531
532 pub fn sketch_selection_all_construction(&self) -> Option<bool> {
536 let edit = self.sketch_edit.as_ref()?;
537 let doc = &edit.session.doc;
538 let mut any = false;
539 let mut all = true;
540 for r in &edit.session.selection {
541 match r.get("kind").and_then(|v| v.as_str()) {
542 Some("point") => {
543 any = true;
544 if let Some(id) = r.get("id") {
545 if !doc.point(id).map_or(false, |p| p.construction) {
546 all = false;
547 }
548 }
549 }
550 Some("geometry") => {
551 any = true;
552 if let Some(id) = r.get("id") {
553 if !doc.geometry(id).map_or(false, |g| g.construction()) {
554 all = false;
555 }
556 }
557 }
558 _ => {}
559 }
560 }
561 if !any {
562 return None;
563 }
564 Some(all)
565 }
566}
567
568fn sketch_palette_for(session: &crate::sketch::SketchSession) -> Vec<SketchConstraintAction> {
572 use crate::sketch::doc::id_key;
573 use std::collections::HashSet;
574
575 let doc = &session.doc;
576 let mut sel_points: Vec<serde_json::Value> = Vec::new();
577 let mut geos: Vec<&crate::sketch::SketchGeometry> = Vec::new();
578 for r in &session.selection {
579 match r.get("kind").and_then(|v| v.as_str()) {
580 Some("point") => {
581 if let Some(id) = r.get("id") {
582 sel_points.push(id.clone());
583 }
584 }
585 Some("geometry") => {
586 if let Some(id) = r.get("id") {
587 if let Some(g) = doc.geometry(id) {
588 geos.push(g);
589 }
590 }
591 }
592 _ => {}
593 }
594 }
595
596 let mut point_set: HashSet<String> = HashSet::new();
599 for id in &sel_points {
600 point_set.insert(id_key(id));
601 }
602 for g in &geos {
603 let pts: &[serde_json::Value] = if g.geom_type == "arc" {
604 &g.points[..g.points.len().min(2)]
605 } else {
606 &g.points
607 };
608 for pid in pts {
609 point_set.insert(id_key(pid));
610 }
611 }
612 let point_count = point_set.len();
613 let selected_point_ids_len = sel_points.len();
614 let is_radial =
615 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
616
617 let mut out: Vec<SketchConstraintAction> = Vec::new();
618 let mut push = |symbol: &str, label: &str, dimensional: bool| {
619 out.push(SketchConstraintAction {
620 symbol: symbol.to_string(),
621 label: label.to_string(),
622 dimensional,
623 });
624 };
625
626 if geos.len() == 1 && is_radial(geos[0]) {
628 push("R", "Radius", true);
629 push("⌀", "Diameter", true);
630 return out;
631 }
632 if geos.len() == 2 && geos.iter().all(|g| g.geom_type == "line") {
641 push("∥", "Parallel", false);
642 push("⟂", "Perpendicular", false);
643 push("∠", "Angle", true);
644 push("⇌", "Equal distance", false);
645 push("⋰", "Collinear", false);
646 push("⏛", "Point on line", false);
647 if geos
648 .iter()
649 .any(|g| crate::sketch::spline::is_handle_guide(doc, g))
650 {
651 push("⌒", "Tangent", false);
652 }
653 if crate::sketch::spline::is_curvature_joint(doc, geos[0], geos[1]) {
659 push("ϰ", "Curvature (G2)", false);
660 }
661 return out;
662 }
663 if geos.len() == 2 && geos.iter().all(|g| is_radial(g)) {
665 push("⊜", "Equal radius", false);
666 push("◎", "Concentric", false);
667 push("⌒", "Tangent", false);
668 return out;
669 }
670 if geos.len() == 2
674 && ((geos[0].geom_type == "line" && is_radial(geos[1]))
675 || (geos[1].geom_type == "line" && is_radial(geos[0])))
676 {
677 push("⌒", "Tangent", false);
678 if geos
679 .iter()
680 .any(|g| crate::sketch::spline::is_end_handle_guide(doc, g))
681 {
682 push("ϰ", "Curvature (G2)", false);
683 }
684 return out;
685 }
686 if sketch_foot_tangent_pair(doc, &geos).is_some() {
696 push("∿", "Tangent (span point)", false);
697 return out;
698 }
699 if geos.len() == 1 && geos[0].geom_type == "line" && selected_point_ids_len == 2 {
701 push("⟂", "Perpendicular", false);
702 push("⋈", "Symmetric about line", false);
703 }
704
705 if point_count == 1 {
706 push("⏚", "Ground (fix point)", false);
707 }
708 if point_count == 2 {
709 push("━", "Horizontal", false);
710 push("│", "Vertical", false);
711 push("≡", "Coincident", false);
712 push("⟺", "Distance", true);
713 }
714 if point_count == 3 {
715 push("⋯", "Midpoint", false);
716 push("⏛", "Point on line", false);
717 push("↥", "Line to point distance", true);
718 push("∠", "Angle", true);
719 if selected_point_ids_len >= 3 {
721 push("⋰", "Collinear", false);
722 }
723 }
724 if point_count > 3 && geos.is_empty() && selected_point_ids_len >= 3 {
726 push("⋰", "Collinear", false);
727 }
728
729 out
730}
731
732fn sketch_build_tangent_points(
743 doc: &crate::sketch::SketchDoc,
744 geos: &[&crate::sketch::SketchGeometry],
745) -> Option<Vec<serde_json::Value>> {
746 if geos.len() != 2 {
747 return None;
748 }
749 let is_radial = |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
750 if geos.iter().all(|g| g.geom_type == "line") {
751 let (a, b) = (geos[0], geos[1]);
752 if a.points.len() < 2 || b.points.len() < 2 {
753 return None;
754 }
755 if !crate::sketch::spline::is_handle_guide(doc, a)
756 && !crate::sketch::spline::is_handle_guide(doc, b)
757 {
758 return None;
759 }
760 return Some(vec![
761 a.points[0].clone(),
762 a.points[1].clone(),
763 b.points[0].clone(),
764 b.points[1].clone(),
765 ]);
766 }
767 let line = geos.iter().find(|g| g.geom_type == "line");
768 let circ = geos.iter().find(|g| is_radial(g));
769 if let (Some(line), Some(circ)) = (line, circ) {
770 if line.points.len() < 2 || circ.points.len() < 2 {
771 return None;
772 }
773 return Some(vec![
774 line.points[0].clone(),
775 line.points[1].clone(),
776 circ.points[0].clone(),
777 circ.points[1].clone(),
778 ]);
779 }
780 if geos.iter().all(|g| is_radial(g)) {
781 let (a, b) = (geos[0], geos[1]);
782 if a.points.len() < 2 || b.points.len() < 2 {
783 return None;
784 }
785 return Some(vec![
786 a.points[0].clone(),
787 a.points[1].clone(),
788 b.points[0].clone(),
789 b.points[1].clone(),
790 ]);
791 }
792 None
793}
794
795fn sketch_foot_tangent_pair<'a>(
809 doc: &crate::sketch::SketchDoc,
810 geos: &[&'a crate::sketch::SketchGeometry],
811) -> Option<(&'a crate::sketch::SketchGeometry, &'a crate::sketch::SketchGeometry)> {
812 if geos.len() != 2 {
813 return None;
814 }
815 let is_spline = |g: &crate::sketch::SketchGeometry| {
816 crate::sketch::spline::is_spline_type(&g.geom_type)
817 };
818 let is_radial =
819 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
820 let spline = *geos.iter().find(|g| is_spline(g))?;
821 let target = *geos.iter().find(|g| !is_spline(g))?;
822 if spline.points.len() < 4 || target.points.len() < 2 {
823 return None;
824 }
825 let target_is_usable = if target.geom_type == "line" {
826 !crate::sketch::spline::is_handle_guide(doc, target)
827 } else {
828 is_radial(target)
829 };
830 target_is_usable.then_some((spline, target))
831}
832
833fn sketch_build_foot_tangent_points(
844 doc: &crate::sketch::SketchDoc,
845 geos: &[&crate::sketch::SketchGeometry],
846) -> Option<Vec<serde_json::Value>> {
847 let (spline, target) = sketch_foot_tangent_pair(doc, geos)?;
848 Some(vec![
849 target.points[0].clone(),
850 target.points[1].clone(),
851 spline.points[0].clone(),
852 spline.points[1].clone(),
853 ])
854}
855
856fn sketch_build_curvature_points(
869 doc: &crate::sketch::SketchDoc,
870 geos: &[&crate::sketch::SketchGeometry],
871) -> Option<Vec<serde_json::Value>> {
872 if geos.len() != 2 {
873 return None;
874 }
875 let is_radial =
876 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
877 if geos.iter().all(|g| g.geom_type == "line") {
878 if !crate::sketch::spline::is_curvature_joint(doc, geos[0], geos[1]) {
879 return None;
880 }
881 let (a, b) = (geos[0], geos[1]);
882 return Some(vec![
883 a.points[0].clone(),
884 a.points[1].clone(),
885 b.points[0].clone(),
886 b.points[1].clone(),
887 ]);
888 }
889 let guide = geos
890 .iter()
891 .find(|g| crate::sketch::spline::is_end_handle_guide(doc, g))?;
892 let circ = geos.iter().find(|g| is_radial(g))?;
893 if circ.points.len() < 2 {
894 return None;
895 }
896 Some(vec![
897 guide.points[0].clone(),
898 guide.points[1].clone(),
899 circ.points[0].clone(),
900 circ.points[1].clone(),
901 ])
902}
903
904pub(super) fn sketch_perpendicular_should_swap(
908 doc: &crate::sketch::SketchDoc,
909 pts: &[serde_json::Value],
910) -> bool {
911 let coord = |v: &serde_json::Value| doc.point(v).map(|p| (p.x, p.y));
912 let (Some(p0), Some(p1), Some(p2), Some(p3)) =
913 (coord(&pts[0]), coord(&pts[1]), coord(&pts[2]), coord(&pts[3]))
914 else {
915 return false;
916 };
917 let angle = |a: (f64, f64), b: (f64, f64)| -> f64 {
919 let deg = (b.1 - a.1).atan2(b.0 - a.0) * 180.0 / std::f64::consts::PI;
920 (deg + 360.0) % 360.0
921 };
922 let fold = |a: f64| (a + 180.0) % 360.0 - 180.0;
924 let line1_a = fold(angle(p0, p1));
925 let line1_b = fold(angle(p1, p0));
926 let line2 = fold(angle(p2, p3));
927 let diff_a = line1_a - line2;
928 let diff_b = line1_b - line2;
929 (90.0 - diff_a).abs() > (90.0 - diff_b).abs()
930}
931
932pub(super) fn sketch_constraint_signature(ctype: &str, points: &[serde_json::Value]) -> String {
935 use crate::sketch::doc::id_key;
936 let mut keys: Vec<String> = points.iter().map(id_key).collect();
937 keys.sort();
938 format!("{ctype}|{}", keys.join(","))
939}
940
941fn sketch_build_and_add_constraint(
946 session: &mut crate::sketch::SketchSession,
947 symbol: &str,
948) -> bool {
949 struct Built {
953 store_type: String,
954 display_style: &'static str,
955 lists: Vec<Vec<serde_json::Value>>,
956 }
957
958 let built: Option<Built> = {
960 let doc = &session.doc;
961 let mut selected: Vec<serde_json::Value> = Vec::new();
966 let mut geo_items: Vec<&crate::sketch::SketchGeometry> = Vec::new();
967 let mut point_items: Vec<serde_json::Value> = Vec::new();
968 let mut geometry_type: Option<String> = None;
969 let mut first_kind: Option<String> = None;
970 let mut has_geometry = false;
971 for (i, r) in session.selection.iter().enumerate() {
972 let kind = r.get("kind").and_then(|v| v.as_str());
973 let Some(id) = r.get("id") else { continue };
974 if i == 0 {
975 first_kind = kind.map(|k| k.to_string());
976 }
977 match kind {
978 Some("point") => {
979 if let Some(p) = doc.point(id) {
980 selected.push(p.id.clone());
981 point_items.push(p.id.clone());
982 }
983 }
984 Some("geometry") => {
985 if let Some(g) = doc.geometry(id) {
986 for pid in &g.points {
987 if doc.point(pid).is_some() {
988 selected.push(pid.clone());
989 }
990 }
991 if g.geom_type == "arc" {
992 selected.pop();
993 }
994 geometry_type = Some(g.geom_type.clone());
995 geo_items.push(g);
996 has_geometry = true;
997 }
998 }
999 _ => {}
1000 }
1001 }
1002 if selected.is_empty() {
1003 None
1004 } else {
1005 let radial = |g: &crate::sketch::SketchGeometry| {
1006 g.geom_type == "arc" || g.geom_type == "circle"
1007 };
1008 let simple = |t: &str, list: Vec<serde_json::Value>, ds: &'static str| Built {
1009 store_type: t.to_string(),
1010 display_style: ds,
1011 lists: vec![list],
1012 };
1013
1014 match symbol {
1016 "◎" => {
1017 if geo_items.len() == 2 && geo_items.iter().all(|g| radial(g)) {
1018 Some(simple(
1019 "◎",
1020 vec![geo_items[0].points[0].clone(), geo_items[1].points[0].clone()],
1021 "",
1022 ))
1023 } else {
1024 None
1025 }
1026 }
1027 "⊜" => {
1028 if geo_items.len() == 2 && geo_items.iter().all(|g| radial(g)) {
1029 let (g0, g1) = (geo_items[0], geo_items[1]);
1030 Some(simple(
1031 "⊜",
1032 vec![
1033 g0.points[0].clone(),
1034 g0.points[1].clone(),
1035 g1.points[0].clone(),
1036 g1.points[1].clone(),
1037 ],
1038 "",
1039 ))
1040 } else {
1041 None
1042 }
1043 }
1044 "⌒" => sketch_build_tangent_points(doc, &geo_items).map(|pts| simple("⌒", pts, "")),
1045 "ϰ" => {
1046 sketch_build_curvature_points(doc, &geo_items).map(|pts| simple("ϰ", pts, ""))
1047 }
1048 "∿" => sketch_build_foot_tangent_points(doc, &geo_items)
1049 .map(|pts| simple("∿", pts, "")),
1050 "⋰" => {
1051 let ids = if geo_items.len() >= 2
1052 && geo_items.iter().all(|g| g.geom_type == "line")
1053 {
1054 let mut v = Vec::new();
1055 for g in &geo_items {
1056 v.push(g.points[0].clone());
1057 v.push(g.points[1].clone());
1058 }
1059 Some(v)
1060 } else if point_items.len() >= 3 {
1061 Some(point_items.clone())
1062 } else {
1063 None
1064 };
1065 ids.filter(|v| v.len() >= 3).map(|v| simple("⋰", v, ""))
1066 }
1067 "⋈" => geo_items
1068 .iter()
1069 .find(|g| g.geom_type == "line")
1070 .filter(|line| line.points.len() >= 2 && point_items.len() == 2)
1071 .map(|line| {
1072 simple(
1073 "⋈",
1074 vec![
1075 line.points[0].clone(),
1076 line.points[1].clone(),
1077 point_items[0].clone(),
1078 point_items[1].clone(),
1079 ],
1080 "",
1081 )
1082 }),
1083 "R" | "⌀" => {
1086 if selected.len() == 2 {
1087 let ds = if symbol == "⌀" { "diameter" } else { "radius" };
1088 Some(Built {
1089 store_type: "⟺".to_string(),
1090 display_style: ds,
1091 lists: vec![selected.clone()],
1092 })
1093 } else {
1094 None
1095 }
1096 }
1097 _ => {
1098 match selected.len() {
1100 1 => match symbol {
1101 "⏚" => Some(simple("⏚", selected.clone(), "")),
1102 _ => None,
1103 },
1104 2 => match symbol {
1105 "━" | "│" | "≡" => Some(simple(symbol, selected.clone(), "")),
1106 "⟺" => {
1107 let ds = if matches!(
1108 geometry_type.as_deref(),
1109 Some("arc") | Some("circle")
1110 ) {
1111 "radius"
1112 } else {
1113 ""
1114 };
1115 Some(Built {
1116 store_type: "⟺".to_string(),
1117 display_style: ds,
1118 lists: vec![selected.clone()],
1119 })
1120 }
1121 _ => None,
1122 },
1123 3 => match symbol {
1124 "⏛" => Some(simple("⏛", selected.clone(), "")),
1125 "⋯" => {
1126 let mut pts = selected.clone();
1127 if has_geometry && first_kind.as_deref() == Some("point") {
1128 pts.reverse();
1129 }
1130 Some(simple("⋯", pts, ""))
1131 }
1132 "↥" => {
1133 if geo_items.len() == 1 && point_items.len() == 1 {
1134 let line = geo_items[0];
1135 if line.geom_type == "line" && line.points.len() >= 2 {
1136 Some(simple(
1137 "↥",
1138 vec![
1139 line.points[0].clone(),
1140 line.points[1].clone(),
1141 point_items[0].clone(),
1142 ],
1143 "",
1144 ))
1145 } else {
1146 None
1147 }
1148 } else {
1149 Some(simple("↥", selected.clone(), ""))
1152 }
1153 }
1154 "⇌" => Some(simple("⇌", selected.clone(), "")),
1155 _ => None,
1156 },
1157 4 | 5 => match symbol {
1158 "⏛" => {
1159 if geo_items.len() == 2
1162 && geo_items.iter().all(|g| g.geom_type == "line")
1163 && geo_items[0].points.len() >= 2
1164 && geo_items[1].points.len() >= 2
1165 {
1166 let (g0, g1) = (geo_items[0], geo_items[1]);
1167 Some(Built {
1168 store_type: "⏛".to_string(),
1169 display_style: "",
1170 lists: vec![
1171 vec![
1172 g0.points[0].clone(),
1173 g0.points[1].clone(),
1174 g1.points[0].clone(),
1175 ],
1176 vec![
1177 g0.points[0].clone(),
1178 g0.points[1].clone(),
1179 g1.points[1].clone(),
1180 ],
1181 ],
1182 })
1183 } else {
1184 None
1185 }
1186 }
1187 "⟂" => {
1188 if selected.len() != 4 {
1189 None
1190 } else {
1191 let mut pts = selected.clone();
1192 if sketch_perpendicular_should_swap(doc, &pts) {
1193 pts.swap(0, 1);
1194 }
1195 Some(simple("⟂", pts, ""))
1196 }
1197 }
1198 "∥" => Some(simple("∥", selected.clone(), "")),
1199 "∠" => Some(simple("∠", selected.clone(), "")),
1200 "⇌" => Some(simple("⇌", selected.clone(), "")),
1201 _ => None,
1202 },
1203 _ => None,
1204 }
1205 }
1206 }
1207 }
1208 };
1209
1210 let Some(built) = built else {
1211 return false;
1212 };
1213
1214 let doc = &mut session.doc;
1217 let mut added_any = false;
1218 for pts in &built.lists {
1219 let sig = sketch_constraint_signature(&built.store_type, pts);
1220 let duplicate = doc.constraints.iter().any(|c| match c.ctype() {
1221 Some(t) => sketch_constraint_signature(t, c.points()) == sig,
1222 None => false,
1223 });
1224 if duplicate {
1225 continue;
1226 }
1227 let id = doc.next_constraint_id();
1228 let mut raw = serde_json::Map::new();
1229 raw.insert("id".to_string(), id);
1230 raw.insert(
1231 "type".to_string(),
1232 serde_json::Value::String(built.store_type.clone()),
1233 );
1234 raw.insert("points".to_string(), serde_json::Value::Array(pts.clone()));
1235 raw.insert("labelX".to_string(), serde_json::Value::from(0));
1236 raw.insert("labelY".to_string(), serde_json::Value::from(0));
1237 raw.insert(
1238 "displayStyle".to_string(),
1239 serde_json::Value::String(built.display_style.to_string()),
1240 );
1241 raw.insert("value".to_string(), serde_json::Value::Null);
1242 raw.insert("valueNeedsSetup".to_string(), serde_json::Value::Bool(true));
1243 doc.constraints.push(crate::sketch::SketchConstraint { raw });
1244 added_any = true;
1245 }
1246 added_any
1247}
1248