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: if crate::sketch::external_ref::is_origin_id(&p.id) {
136 format!("Origin ({:.1}, {:.1}){marks}", p.x, p.y)
137 } else {
138 format!("P{} ({:.1}, {:.1}){marks}", id_key(&p.id), p.x, p.y)
139 },
140 selected: Self::sketch_ref_selected(session, "point", &p.id),
141 construction: p.construction,
142 conflicting: false,
143 }
144 })
145 .collect()
146 }
147
148 pub fn sketch_geometry_rows(&self) -> Vec<SketchEntityRow> {
151 use crate::sketch::doc::id_key;
152 let Some(edit) = self.sketch_edit.as_ref() else {
153 return Vec::new();
154 };
155 let session = &edit.session;
156 session
157 .doc
158 .geometries
159 .iter()
160 .map(|g| {
161 let pts = g
162 .points
163 .iter()
164 .map(id_key)
165 .collect::<Vec<_>>()
166 .join(",");
167 let construction = g.construction();
168 let mark = if construction { " ◐" } else { "" };
169 SketchEntityRow {
170 kind: "geometry",
171 id: g.id.clone(),
172 label: format!("{}:{}{mark} [{pts}]", g.geom_type, id_key(&g.id)),
173 selected: Self::sketch_ref_selected(session, "geometry", &g.id),
174 construction,
175 conflicting: false,
176 }
177 })
178 .collect()
179 }
180
181 pub fn sketch_constraint_rows(&self) -> Vec<SketchEntityRow> {
184 use crate::sketch::doc::id_key;
185 let Some(edit) = self.sketch_edit.as_ref() else {
186 return Vec::new();
187 };
188 let session = &edit.session;
189 session
190 .doc
191 .constraints
192 .iter()
193 .filter_map(|c| {
194 let id = c.raw.get("id")?.clone();
195 let ctype = c.ctype().unwrap_or("?");
196 let value = c
197 .raw
198 .get("value")
199 .and_then(serde_json::Value::as_f64)
200 .map(|v| format!(" {v:.3}"))
201 .unwrap_or_default();
202 let pts = c
203 .points()
204 .iter()
205 .map(id_key)
206 .collect::<Vec<_>>()
207 .join(",");
208 Some(SketchEntityRow {
209 kind: "constraint",
210 id: id.clone(),
211 label: format!("{} {ctype}{value} [{pts}]", id_key(&id)),
212 selected: Self::sketch_ref_selected(session, "constraint", &id),
213 construction: false,
214 conflicting: session.diagnostics.constraint_conflicting(&id),
215 })
216 })
217 .collect()
218 }
219
220 pub fn sketch_select_entity(&mut self, kind: &str, id: serde_json::Value, additive: bool) {
225 let toggles = self.settings.multi_select == crate::style::MultiSelectMode::ClickToggles;
226 let entity_ref = serde_json::json!({ "kind": kind, "id": id });
227 let Some(edit) = self.sketch_edit.as_mut() else {
228 return;
229 };
230 if !additive && !toggles {
231 edit.session.clear_selection();
232 }
233 edit.session.toggle_selection(entity_ref);
234 self.refresh_sketch_overlay();
235 self.dirty = true;
236 }
237
238 pub fn sketch_hover_entity(&mut self, kind: &str, id: serde_json::Value) {
242 if self.sketch_edit.is_none() {
243 return;
244 }
245 let entity_ref = serde_json::json!({ "kind": kind, "id": id });
248 self.set_sketch_hover(Some(entity_ref));
249 self.sketch_list_hover_active = true;
250 }
251
252 pub fn take_sketch_list_hover(&mut self) -> bool {
256 std::mem::take(&mut self.sketch_list_hover_active)
257 }
258
259 pub fn sketch_solver_settings(&self) -> Option<crate::sketch::SketchSolverSettings> {
262 self.sketch_edit
263 .as_ref()
264 .map(|edit| edit.session.solver_settings.clone())
265 }
266
267 pub fn sketch_set_solver_settings(&mut self, settings: crate::sketch::SketchSolverSettings) {
270 if let Some(edit) = self.sketch_edit.as_mut() {
271 edit.session.solver_settings = settings;
272 } else {
273 return;
274 }
275 self.resolve_active_sketch("solver settings");
276 self.refresh_sketch_overlay();
277 self.dirty = true;
278 }
279}
280
281#[derive(Clone, Debug, PartialEq)]
285pub struct SketchConstraintAction {
286 pub symbol: String,
289 pub label: String,
291 pub dimensional: bool,
294}
295
296impl EngineState {
297 pub fn sketch_applicable_constraints(&self) -> Vec<SketchConstraintAction> {
307 match self.sketch_edit.as_ref() {
308 Some(edit) => sketch_palette_for(&edit.session),
309 None => Vec::new(),
310 }
311 }
312
313 pub fn sketch_add_constraint(&mut self, symbol: &str) -> bool {
318 let added = match self.sketch_edit.as_mut() {
321 Some(edit) => {
322 edit.record_undo();
323 sketch_build_and_add_constraint(&mut edit.session, symbol)
324 }
325 None => return false,
326 };
327 if added {
328 self.resolve_active_sketch("add-constraint");
329 self.refresh_sketch_overlay();
330 self.dirty = true;
331 } else if let Some(edit) = self.sketch_edit.as_mut() {
332 edit.undo_stack.pop();
333 }
334 added
335 }
336
337 pub fn sketch_toggle_ground(&mut self) -> bool {
343 use crate::sketch::doc::id_key;
344 use std::collections::HashSet;
345
346 let Some(edit) = self.sketch_edit.as_mut() else {
347 return false;
348 };
349 let sel_ids: Vec<serde_json::Value> = edit
350 .session
351 .selection
352 .iter()
353 .filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("point"))
354 .filter_map(|r| r.get("id").cloned())
355 .filter(|id| !crate::sketch::external_ref::is_origin_id(id))
357 .collect();
358 if sel_ids.is_empty() {
359 return false;
360 }
361 edit.record_undo();
363 let doc = &mut edit.session.doc;
364 let has_ground = |doc: &crate::sketch::SketchDoc, id: &serde_json::Value| -> bool {
365 doc.constraints.iter().any(|c| {
366 c.ctype() == Some("⏚")
367 && c.points().first().map(id_key) == Some(id_key(id))
368 })
369 };
370 let all_grounded = sel_ids.iter().all(|id| has_ground(doc, id));
371 if all_grounded {
372 let sel_keys: HashSet<String> = sel_ids.iter().map(id_key).collect();
373 doc.constraints.retain(|c| {
374 if c.ctype() != Some("⏚") {
375 return true;
376 }
377 match c.points().first() {
378 Some(p) => !sel_keys.contains(&id_key(p)),
379 None => true,
380 }
381 });
382 for id in &sel_ids {
383 if let Some(p) = doc.point_mut(id) {
384 p.fixed = false;
385 }
386 }
387 } else {
388 for id in &sel_ids {
389 if has_ground(doc, id) {
390 continue;
391 }
392 let cid = doc.next_constraint_id();
393 let mut raw = serde_json::Map::new();
394 raw.insert("id".to_string(), cid);
395 raw.insert("type".to_string(), serde_json::Value::String("⏚".to_string()));
396 raw.insert(
397 "points".to_string(),
398 serde_json::Value::Array(vec![id.clone()]),
399 );
400 doc.constraints.push(crate::sketch::SketchConstraint { raw });
401 if let Some(p) = doc.point_mut(id) {
402 p.fixed = true;
403 }
404 }
405 }
406 self.resolve_active_sketch("toggle-ground");
407 self.refresh_sketch_overlay();
408 self.dirty = true;
409 true
410 }
411
412 pub fn sketch_toggle_construction(&mut self) -> bool {
417 let Some(edit) = self.sketch_edit.as_mut() else {
418 return false;
419 };
420 let mut pt_ids: Vec<serde_json::Value> = Vec::new();
421 let mut geo_ids: Vec<serde_json::Value> = Vec::new();
422 for r in &edit.session.selection {
423 match r.get("kind").and_then(|v| v.as_str()) {
424 Some("point") => {
425 if let Some(id) = r.get("id").filter(|id| !crate::sketch::external_ref::is_origin_id(id)) {
427 pt_ids.push(id.clone());
428 }
429 }
430 Some("geometry") => {
431 if let Some(id) = r.get("id") {
432 geo_ids.push(id.clone());
433 }
434 }
435 _ => {}
436 }
437 }
438 if pt_ids.is_empty() && geo_ids.is_empty() {
439 return false;
440 }
441 edit.record_undo();
443 let doc = &mut edit.session.doc;
444 let all_construction = pt_ids
445 .iter()
446 .all(|id| doc.point(id).map_or(false, |p| p.construction))
447 && geo_ids
448 .iter()
449 .all(|id| doc.geometry(id).map_or(false, |g| g.construction()));
450 let next = !all_construction;
451 for id in &pt_ids {
452 if let Some(p) = doc.point_mut(id) {
453 p.construction = next;
454 }
455 }
456 for id in &geo_ids {
457 if let Some(g) = doc.geometry_mut(id) {
458 g.extra
459 .insert("construction".to_string(), serde_json::Value::Bool(next));
460 }
461 }
462 self.resolve_active_sketch("toggle-construction");
463 self.refresh_sketch_overlay();
464 self.dirty = true;
465 true
466 }
467
468 pub fn sketch_cleanup_unused_points(&mut self) -> bool {
472 use crate::sketch::doc::id_key;
473 use std::collections::HashSet;
474
475 let Some(edit) = self.sketch_edit.as_mut() else {
476 return false;
477 };
478 edit.record_undo();
480 let doc = &mut edit.session.doc;
481 let mut used: HashSet<String> = HashSet::new();
482 for g in &doc.geometries {
483 for pid in &g.points {
484 used.insert(id_key(pid));
485 }
486 }
487 for c in &doc.constraints {
488 for pid in c.points() {
489 used.insert(id_key(pid));
490 }
491 }
492 let before = doc.points.len();
493 doc.points.retain(|p| used.contains(&id_key(&p.id)));
494 let removed = doc.points.len() != before;
495 if !removed {
496 edit.undo_stack.pop();
497 return false;
498 }
499 self.resolve_active_sketch("cleanup");
500 self.refresh_sketch_overlay();
501 self.dirty = true;
502 true
503 }
504
505 pub fn sketch_constraint_count(&self) -> usize {
508 self.sketch_edit
509 .as_ref()
510 .map_or(0, |edit| edit.session.doc.constraints.len())
511 }
512
513 pub fn sketch_selection_all_grounded(&self) -> Option<bool> {
517 use crate::sketch::doc::id_key;
518 let edit = self.sketch_edit.as_ref()?;
519 let doc = &edit.session.doc;
520 let sel: Vec<&serde_json::Value> = edit
521 .session
522 .selection
523 .iter()
524 .filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("point"))
525 .filter_map(|r| r.get("id"))
526 .collect();
527 if sel.is_empty() {
528 return None;
529 }
530 let all = sel.iter().all(|id| {
531 doc.constraints.iter().any(|c| {
532 c.ctype() == Some("⏚")
533 && c.points().first().map(id_key) == Some(id_key(id))
534 })
535 });
536 Some(all)
537 }
538
539 pub fn sketch_selection_all_construction(&self) -> Option<bool> {
543 let edit = self.sketch_edit.as_ref()?;
544 let doc = &edit.session.doc;
545 let mut any = false;
546 let mut all = true;
547 for r in &edit.session.selection {
548 match r.get("kind").and_then(|v| v.as_str()) {
549 Some("point") => {
550 any = true;
551 if let Some(id) = r.get("id") {
552 if !doc.point(id).map_or(false, |p| p.construction) {
553 all = false;
554 }
555 }
556 }
557 Some("geometry") => {
558 any = true;
559 if let Some(id) = r.get("id") {
560 if !doc.geometry(id).map_or(false, |g| g.construction()) {
561 all = false;
562 }
563 }
564 }
565 _ => {}
566 }
567 }
568 if !any {
569 return None;
570 }
571 Some(all)
572 }
573}
574
575fn sketch_palette_for(session: &crate::sketch::SketchSession) -> Vec<SketchConstraintAction> {
579 use crate::sketch::doc::id_key;
580 use std::collections::HashSet;
581
582 let doc = &session.doc;
583 let mut sel_points: Vec<serde_json::Value> = Vec::new();
584 let mut geos: Vec<&crate::sketch::SketchGeometry> = Vec::new();
585 for r in &session.selection {
586 match r.get("kind").and_then(|v| v.as_str()) {
587 Some("point") => {
588 if let Some(id) = r.get("id") {
589 sel_points.push(id.clone());
590 }
591 }
592 Some("geometry") => {
593 if let Some(id) = r.get("id") {
594 if let Some(g) = doc.geometry(id) {
595 geos.push(g);
596 }
597 }
598 }
599 _ => {}
600 }
601 }
602
603 let mut point_set: HashSet<String> = HashSet::new();
606 for id in &sel_points {
607 point_set.insert(id_key(id));
608 }
609 for g in &geos {
610 let pts: &[serde_json::Value] = if g.geom_type == "arc" {
611 &g.points[..g.points.len().min(2)]
612 } else {
613 &g.points
614 };
615 for pid in pts {
616 point_set.insert(id_key(pid));
617 }
618 }
619 let point_count = point_set.len();
620 let selected_point_ids_len = sel_points.len();
621 let is_radial =
622 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
623
624 let mut out: Vec<SketchConstraintAction> = Vec::new();
625 let mut push = |symbol: &str, label: &str, dimensional: bool| {
626 out.push(SketchConstraintAction {
627 symbol: symbol.to_string(),
628 label: label.to_string(),
629 dimensional,
630 });
631 };
632
633 if geos.len() == 1 && is_radial(geos[0]) {
635 push("R", "Radius", true);
636 push("⌀", "Diameter", true);
637 return out;
638 }
639 if geos.len() == 2 && geos.iter().all(|g| g.geom_type == "line") {
648 push("∥", "Parallel", false);
649 push("⟂", "Perpendicular", false);
650 push("∠", "Angle", true);
651 push("⇌", "Equal distance", false);
652 push("⋰", "Collinear", false);
653 push("⏛", "Point on line", false);
654 if geos
655 .iter()
656 .any(|g| crate::sketch::spline::is_handle_guide(doc, g))
657 {
658 push("⌒", "Tangent", false);
659 }
660 if crate::sketch::spline::is_curvature_joint(doc, geos[0], geos[1]) {
666 push("ϰ", "Curvature (G2)", false);
667 }
668 return out;
669 }
670 if geos.len() == 2 && geos.iter().all(|g| is_radial(g)) {
672 push("⊜", "Equal radius", false);
673 push("◎", "Concentric", false);
674 push("⌒", "Tangent", false);
675 return out;
676 }
677 if geos.len() == 2
681 && ((geos[0].geom_type == "line" && is_radial(geos[1]))
682 || (geos[1].geom_type == "line" && is_radial(geos[0])))
683 {
684 push("⌒", "Tangent", false);
685 if geos
686 .iter()
687 .any(|g| crate::sketch::spline::is_end_handle_guide(doc, g))
688 {
689 push("ϰ", "Curvature (G2)", false);
690 }
691 return out;
692 }
693 if sketch_foot_tangent_pair(doc, &geos).is_some() {
703 push("∿", "Tangent (span point)", false);
704 return out;
705 }
706 if geos.len() == 1 && geos[0].geom_type == "line" && selected_point_ids_len == 2 {
708 push("⟂", "Perpendicular", false);
709 push("⋈", "Symmetric about line", false);
710 }
711
712 if point_count == 1 {
713 push("⏚", "Ground (fix point)", false);
714 }
715 if point_count == 2 {
716 push("━", "Horizontal", false);
717 push("│", "Vertical", false);
718 push("≡", "Coincident", false);
719 push("⟺", "Distance", true);
720 }
721 if point_count == 3 {
722 push("⋯", "Midpoint", false);
723 push("⏛", "Point on line", false);
724 push("↥", "Line to point distance", true);
725 push("∠", "Angle", true);
726 if selected_point_ids_len >= 3 {
728 push("⋰", "Collinear", false);
729 }
730 }
731 if point_count > 3 && geos.is_empty() && selected_point_ids_len >= 3 {
733 push("⋰", "Collinear", false);
734 }
735
736 out
737}
738
739fn sketch_build_tangent_points(
750 doc: &crate::sketch::SketchDoc,
751 geos: &[&crate::sketch::SketchGeometry],
752) -> Option<Vec<serde_json::Value>> {
753 if geos.len() != 2 {
754 return None;
755 }
756 let is_radial = |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
757 if geos.iter().all(|g| g.geom_type == "line") {
758 let (a, b) = (geos[0], geos[1]);
759 if a.points.len() < 2 || b.points.len() < 2 {
760 return None;
761 }
762 if !crate::sketch::spline::is_handle_guide(doc, a)
763 && !crate::sketch::spline::is_handle_guide(doc, b)
764 {
765 return None;
766 }
767 return Some(vec![
768 a.points[0].clone(),
769 a.points[1].clone(),
770 b.points[0].clone(),
771 b.points[1].clone(),
772 ]);
773 }
774 let line = geos.iter().find(|g| g.geom_type == "line");
775 let circ = geos.iter().find(|g| is_radial(g));
776 if let (Some(line), Some(circ)) = (line, circ) {
777 if line.points.len() < 2 || circ.points.len() < 2 {
778 return None;
779 }
780 return Some(vec![
781 line.points[0].clone(),
782 line.points[1].clone(),
783 circ.points[0].clone(),
784 circ.points[1].clone(),
785 ]);
786 }
787 if geos.iter().all(|g| is_radial(g)) {
788 let (a, b) = (geos[0], geos[1]);
789 if a.points.len() < 2 || b.points.len() < 2 {
790 return None;
791 }
792 return Some(vec![
793 a.points[0].clone(),
794 a.points[1].clone(),
795 b.points[0].clone(),
796 b.points[1].clone(),
797 ]);
798 }
799 None
800}
801
802fn sketch_foot_tangent_pair<'a>(
816 doc: &crate::sketch::SketchDoc,
817 geos: &[&'a crate::sketch::SketchGeometry],
818) -> Option<(&'a crate::sketch::SketchGeometry, &'a crate::sketch::SketchGeometry)> {
819 if geos.len() != 2 {
820 return None;
821 }
822 let is_spline = |g: &crate::sketch::SketchGeometry| {
823 crate::sketch::spline::is_spline_type(&g.geom_type)
824 };
825 let is_radial =
826 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
827 let spline = *geos.iter().find(|g| is_spline(g))?;
828 let target = *geos.iter().find(|g| !is_spline(g))?;
829 if spline.points.len() < 4 || target.points.len() < 2 {
830 return None;
831 }
832 let target_is_usable = if target.geom_type == "line" {
833 !crate::sketch::spline::is_handle_guide(doc, target)
834 } else {
835 is_radial(target)
836 };
837 target_is_usable.then_some((spline, target))
838}
839
840fn sketch_build_foot_tangent_points(
851 doc: &crate::sketch::SketchDoc,
852 geos: &[&crate::sketch::SketchGeometry],
853) -> Option<Vec<serde_json::Value>> {
854 let (spline, target) = sketch_foot_tangent_pair(doc, geos)?;
855 Some(vec![
856 target.points[0].clone(),
857 target.points[1].clone(),
858 spline.points[0].clone(),
859 spline.points[1].clone(),
860 ])
861}
862
863fn sketch_build_curvature_points(
876 doc: &crate::sketch::SketchDoc,
877 geos: &[&crate::sketch::SketchGeometry],
878) -> Option<Vec<serde_json::Value>> {
879 if geos.len() != 2 {
880 return None;
881 }
882 let is_radial =
883 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
884 if geos.iter().all(|g| g.geom_type == "line") {
885 if !crate::sketch::spline::is_curvature_joint(doc, geos[0], geos[1]) {
886 return None;
887 }
888 let (a, b) = (geos[0], geos[1]);
889 return Some(vec![
890 a.points[0].clone(),
891 a.points[1].clone(),
892 b.points[0].clone(),
893 b.points[1].clone(),
894 ]);
895 }
896 let guide = geos
897 .iter()
898 .find(|g| crate::sketch::spline::is_end_handle_guide(doc, g))?;
899 let circ = geos.iter().find(|g| is_radial(g))?;
900 if circ.points.len() < 2 {
901 return None;
902 }
903 Some(vec![
904 guide.points[0].clone(),
905 guide.points[1].clone(),
906 circ.points[0].clone(),
907 circ.points[1].clone(),
908 ])
909}
910
911pub(super) fn sketch_perpendicular_should_swap(
915 doc: &crate::sketch::SketchDoc,
916 pts: &[serde_json::Value],
917) -> bool {
918 let coord = |v: &serde_json::Value| doc.point(v).map(|p| (p.x, p.y));
919 let (Some(p0), Some(p1), Some(p2), Some(p3)) =
920 (coord(&pts[0]), coord(&pts[1]), coord(&pts[2]), coord(&pts[3]))
921 else {
922 return false;
923 };
924 let angle = |a: (f64, f64), b: (f64, f64)| -> f64 {
926 let deg = (b.1 - a.1).atan2(b.0 - a.0) * 180.0 / std::f64::consts::PI;
927 (deg + 360.0) % 360.0
928 };
929 let fold = |a: f64| (a + 180.0) % 360.0 - 180.0;
931 let line1_a = fold(angle(p0, p1));
932 let line1_b = fold(angle(p1, p0));
933 let line2 = fold(angle(p2, p3));
934 let diff_a = line1_a - line2;
935 let diff_b = line1_b - line2;
936 (90.0 - diff_a).abs() > (90.0 - diff_b).abs()
937}
938
939pub(super) fn sketch_constraint_signature(ctype: &str, points: &[serde_json::Value]) -> String {
942 use crate::sketch::doc::id_key;
943 let mut keys: Vec<String> = points.iter().map(id_key).collect();
944 keys.sort();
945 format!("{ctype}|{}", keys.join(","))
946}
947
948fn sketch_build_and_add_constraint(
953 session: &mut crate::sketch::SketchSession,
954 symbol: &str,
955) -> bool {
956 struct Built {
960 store_type: String,
961 display_style: &'static str,
962 lists: Vec<Vec<serde_json::Value>>,
963 }
964
965 let built: Option<Built> = {
967 let doc = &session.doc;
968 let mut selected: Vec<serde_json::Value> = Vec::new();
973 let mut geo_items: Vec<&crate::sketch::SketchGeometry> = Vec::new();
974 let mut point_items: Vec<serde_json::Value> = Vec::new();
975 let mut geometry_type: Option<String> = None;
976 let mut first_kind: Option<String> = None;
977 let mut has_geometry = false;
978 for (i, r) in session.selection.iter().enumerate() {
979 let kind = r.get("kind").and_then(|v| v.as_str());
980 let Some(id) = r.get("id") else { continue };
981 if i == 0 {
982 first_kind = kind.map(|k| k.to_string());
983 }
984 match kind {
985 Some("point") => {
986 if let Some(p) = doc.point(id) {
987 selected.push(p.id.clone());
988 point_items.push(p.id.clone());
989 }
990 }
991 Some("geometry") => {
992 if let Some(g) = doc.geometry(id) {
993 for pid in &g.points {
994 if doc.point(pid).is_some() {
995 selected.push(pid.clone());
996 }
997 }
998 if g.geom_type == "arc" {
999 selected.pop();
1000 }
1001 geometry_type = Some(g.geom_type.clone());
1002 geo_items.push(g);
1003 has_geometry = true;
1004 }
1005 }
1006 _ => {}
1007 }
1008 }
1009 if selected.is_empty() {
1010 None
1011 } else {
1012 let radial = |g: &crate::sketch::SketchGeometry| {
1013 g.geom_type == "arc" || g.geom_type == "circle"
1014 };
1015 let simple = |t: &str, list: Vec<serde_json::Value>, ds: &'static str| Built {
1016 store_type: t.to_string(),
1017 display_style: ds,
1018 lists: vec![list],
1019 };
1020
1021 match symbol {
1023 "◎" => {
1024 if geo_items.len() == 2 && geo_items.iter().all(|g| radial(g)) {
1025 Some(simple(
1026 "◎",
1027 vec![geo_items[0].points[0].clone(), geo_items[1].points[0].clone()],
1028 "",
1029 ))
1030 } else {
1031 None
1032 }
1033 }
1034 "⊜" => {
1035 if geo_items.len() == 2 && geo_items.iter().all(|g| radial(g)) {
1036 let (g0, g1) = (geo_items[0], geo_items[1]);
1037 Some(simple(
1038 "⊜",
1039 vec![
1040 g0.points[0].clone(),
1041 g0.points[1].clone(),
1042 g1.points[0].clone(),
1043 g1.points[1].clone(),
1044 ],
1045 "",
1046 ))
1047 } else {
1048 None
1049 }
1050 }
1051 "⌒" => sketch_build_tangent_points(doc, &geo_items).map(|pts| simple("⌒", pts, "")),
1052 "ϰ" => {
1053 sketch_build_curvature_points(doc, &geo_items).map(|pts| simple("ϰ", pts, ""))
1054 }
1055 "∿" => sketch_build_foot_tangent_points(doc, &geo_items)
1056 .map(|pts| simple("∿", pts, "")),
1057 "⋰" => {
1058 let ids = if geo_items.len() >= 2
1059 && geo_items.iter().all(|g| g.geom_type == "line")
1060 {
1061 let mut v = Vec::new();
1062 for g in &geo_items {
1063 v.push(g.points[0].clone());
1064 v.push(g.points[1].clone());
1065 }
1066 Some(v)
1067 } else if point_items.len() >= 3 {
1068 Some(point_items.clone())
1069 } else {
1070 None
1071 };
1072 ids.filter(|v| v.len() >= 3).map(|v| simple("⋰", v, ""))
1073 }
1074 "⋈" => geo_items
1075 .iter()
1076 .find(|g| g.geom_type == "line")
1077 .filter(|line| line.points.len() >= 2 && point_items.len() == 2)
1078 .map(|line| {
1079 simple(
1080 "⋈",
1081 vec![
1082 line.points[0].clone(),
1083 line.points[1].clone(),
1084 point_items[0].clone(),
1085 point_items[1].clone(),
1086 ],
1087 "",
1088 )
1089 }),
1090 "R" | "⌀" => {
1093 if selected.len() == 2 {
1094 let ds = if symbol == "⌀" { "diameter" } else { "radius" };
1095 Some(Built {
1096 store_type: "⟺".to_string(),
1097 display_style: ds,
1098 lists: vec![selected.clone()],
1099 })
1100 } else {
1101 None
1102 }
1103 }
1104 _ => {
1105 match selected.len() {
1107 1 => match symbol {
1108 "⏚" => Some(simple("⏚", selected.clone(), "")),
1109 _ => None,
1110 },
1111 2 => match symbol {
1112 "━" | "│" | "≡" => Some(simple(symbol, selected.clone(), "")),
1113 "⟺" => {
1114 let ds = if matches!(
1115 geometry_type.as_deref(),
1116 Some("arc") | Some("circle")
1117 ) {
1118 "radius"
1119 } else {
1120 ""
1121 };
1122 Some(Built {
1123 store_type: "⟺".to_string(),
1124 display_style: ds,
1125 lists: vec![selected.clone()],
1126 })
1127 }
1128 _ => None,
1129 },
1130 3 => match symbol {
1131 "⏛" => Some(simple("⏛", selected.clone(), "")),
1132 "⋯" => {
1133 let mut pts = selected.clone();
1134 if has_geometry && first_kind.as_deref() == Some("point") {
1135 pts.reverse();
1136 }
1137 Some(simple("⋯", pts, ""))
1138 }
1139 "↥" => {
1140 if geo_items.len() == 1 && point_items.len() == 1 {
1141 let line = geo_items[0];
1142 if line.geom_type == "line" && line.points.len() >= 2 {
1143 Some(simple(
1144 "↥",
1145 vec![
1146 line.points[0].clone(),
1147 line.points[1].clone(),
1148 point_items[0].clone(),
1149 ],
1150 "",
1151 ))
1152 } else {
1153 None
1154 }
1155 } else {
1156 Some(simple("↥", selected.clone(), ""))
1159 }
1160 }
1161 "⇌" => Some(simple("⇌", selected.clone(), "")),
1162 _ => None,
1163 },
1164 4 | 5 => match symbol {
1165 "⏛" => {
1166 if geo_items.len() == 2
1169 && geo_items.iter().all(|g| g.geom_type == "line")
1170 && geo_items[0].points.len() >= 2
1171 && geo_items[1].points.len() >= 2
1172 {
1173 let (g0, g1) = (geo_items[0], geo_items[1]);
1174 Some(Built {
1175 store_type: "⏛".to_string(),
1176 display_style: "",
1177 lists: vec![
1178 vec![
1179 g0.points[0].clone(),
1180 g0.points[1].clone(),
1181 g1.points[0].clone(),
1182 ],
1183 vec![
1184 g0.points[0].clone(),
1185 g0.points[1].clone(),
1186 g1.points[1].clone(),
1187 ],
1188 ],
1189 })
1190 } else {
1191 None
1192 }
1193 }
1194 "⟂" => {
1195 if selected.len() != 4 {
1196 None
1197 } else {
1198 let mut pts = selected.clone();
1199 if sketch_perpendicular_should_swap(doc, &pts) {
1200 pts.swap(0, 1);
1201 }
1202 Some(simple("⟂", pts, ""))
1203 }
1204 }
1205 "∥" => Some(simple("∥", selected.clone(), "")),
1206 "∠" => Some(simple("∠", selected.clone(), "")),
1207 "⇌" => Some(simple("⇌", selected.clone(), "")),
1208 _ => None,
1209 },
1210 _ => None,
1211 }
1212 }
1213 }
1214 }
1215 };
1216
1217 let Some(built) = built else {
1218 return false;
1219 };
1220
1221 let doc = &mut session.doc;
1224 let mut added_any = false;
1225 for pts in &built.lists {
1226 let sig = sketch_constraint_signature(&built.store_type, pts);
1227 let duplicate = doc.constraints.iter().any(|c| match c.ctype() {
1228 Some(t) => sketch_constraint_signature(t, c.points()) == sig,
1229 None => false,
1230 });
1231 if duplicate {
1232 continue;
1233 }
1234 let id = doc.next_constraint_id();
1235 let mut raw = serde_json::Map::new();
1236 raw.insert("id".to_string(), id);
1237 raw.insert(
1238 "type".to_string(),
1239 serde_json::Value::String(built.store_type.clone()),
1240 );
1241 raw.insert("points".to_string(), serde_json::Value::Array(pts.clone()));
1242 raw.insert("labelX".to_string(), serde_json::Value::from(0));
1243 raw.insert("labelY".to_string(), serde_json::Value::from(0));
1244 raw.insert(
1245 "displayStyle".to_string(),
1246 serde_json::Value::String(built.display_style.to_string()),
1247 );
1248 raw.insert("value".to_string(), serde_json::Value::Null);
1249 raw.insert("valueNeedsSetup".to_string(), serde_json::Value::Bool(true));
1250 doc.constraints.push(crate::sketch::SketchConstraint { raw });
1251 added_any = true;
1252 }
1253 added_any
1254}
1255