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 && sel_points.is_empty() {
681 if geos.iter().all(|g| g.geom_type == "line") {
682 push("⇌", "Equal distance", false);
683 return out;
684 }
685 if geos.iter().all(|g| is_radial(g)) {
686 push("⊜", "Equal radius", false);
687 return out;
688 }
689 }
690 if geos.len() == 2
694 && ((geos[0].geom_type == "line" && is_radial(geos[1]))
695 || (geos[1].geom_type == "line" && is_radial(geos[0])))
696 {
697 push("⌒", "Tangent", false);
698 if geos
699 .iter()
700 .any(|g| crate::sketch::spline::is_end_handle_guide(doc, g))
701 {
702 push("ϰ", "Curvature (G2)", false);
703 }
704 return out;
705 }
706 if sketch_foot_tangent_pair(doc, &geos).is_some() {
716 push("∿", "Tangent (span point)", false);
717 return out;
718 }
719 if geos.len() == 1 && geos[0].geom_type == "line" && selected_point_ids_len == 2 {
721 push("⟂", "Perpendicular", false);
722 push("⋈", "Symmetric about line", false);
723 }
724
725 if point_count == 1 {
726 push("⏚", "Ground (fix point)", false);
727 }
728 if point_count == 2 {
729 push("━", "Horizontal", false);
730 push("│", "Vertical", false);
731 push("≡", "Coincident", false);
732 push("⟺", "Distance", true);
733 }
734 if point_count == 3 {
735 push("⋯", "Midpoint", false);
736 push("⏛", "Point on line", false);
737 push("↥", "Line to point distance", true);
738 push("∠", "Angle", true);
739 if selected_point_ids_len >= 3 {
741 push("⋰", "Collinear", false);
742 }
743 }
744 if point_count > 3 && geos.is_empty() && selected_point_ids_len >= 3 {
746 push("⋰", "Collinear", false);
747 }
748
749 out
750}
751
752fn sketch_build_tangent_points(
763 doc: &crate::sketch::SketchDoc,
764 geos: &[&crate::sketch::SketchGeometry],
765) -> Option<Vec<serde_json::Value>> {
766 if geos.len() != 2 {
767 return None;
768 }
769 let is_radial = |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
770 if geos.iter().all(|g| g.geom_type == "line") {
771 let (a, b) = (geos[0], geos[1]);
772 if a.points.len() < 2 || b.points.len() < 2 {
773 return None;
774 }
775 if !crate::sketch::spline::is_handle_guide(doc, a)
776 && !crate::sketch::spline::is_handle_guide(doc, b)
777 {
778 return None;
779 }
780 return Some(vec![
781 a.points[0].clone(),
782 a.points[1].clone(),
783 b.points[0].clone(),
784 b.points[1].clone(),
785 ]);
786 }
787 let line = geos.iter().find(|g| g.geom_type == "line");
788 let circ = geos.iter().find(|g| is_radial(g));
789 if let (Some(line), Some(circ)) = (line, circ) {
790 if line.points.len() < 2 || circ.points.len() < 2 {
791 return None;
792 }
793 return Some(vec![
794 line.points[0].clone(),
795 line.points[1].clone(),
796 circ.points[0].clone(),
797 circ.points[1].clone(),
798 ]);
799 }
800 if geos.iter().all(|g| is_radial(g)) {
801 let (a, b) = (geos[0], geos[1]);
802 if a.points.len() < 2 || b.points.len() < 2 {
803 return None;
804 }
805 return Some(vec![
806 a.points[0].clone(),
807 a.points[1].clone(),
808 b.points[0].clone(),
809 b.points[1].clone(),
810 ]);
811 }
812 None
813}
814
815fn sketch_foot_tangent_pair<'a>(
829 doc: &crate::sketch::SketchDoc,
830 geos: &[&'a crate::sketch::SketchGeometry],
831) -> Option<(&'a crate::sketch::SketchGeometry, &'a crate::sketch::SketchGeometry)> {
832 if geos.len() != 2 {
833 return None;
834 }
835 let is_spline = |g: &crate::sketch::SketchGeometry| {
836 crate::sketch::spline::is_spline_type(&g.geom_type)
837 };
838 let is_radial =
839 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
840 let spline = *geos.iter().find(|g| is_spline(g))?;
841 let target = *geos.iter().find(|g| !is_spline(g))?;
842 if spline.points.len() < 4 || target.points.len() < 2 {
843 return None;
844 }
845 let target_is_usable = if target.geom_type == "line" {
846 !crate::sketch::spline::is_handle_guide(doc, target)
847 } else {
848 is_radial(target)
849 };
850 target_is_usable.then_some((spline, target))
851}
852
853fn sketch_build_foot_tangent_points(
864 doc: &crate::sketch::SketchDoc,
865 geos: &[&crate::sketch::SketchGeometry],
866) -> Option<Vec<serde_json::Value>> {
867 let (spline, target) = sketch_foot_tangent_pair(doc, geos)?;
868 Some(vec![
869 target.points[0].clone(),
870 target.points[1].clone(),
871 spline.points[0].clone(),
872 spline.points[1].clone(),
873 ])
874}
875
876fn sketch_build_curvature_points(
889 doc: &crate::sketch::SketchDoc,
890 geos: &[&crate::sketch::SketchGeometry],
891) -> Option<Vec<serde_json::Value>> {
892 if geos.len() != 2 {
893 return None;
894 }
895 let is_radial =
896 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
897 if geos.iter().all(|g| g.geom_type == "line") {
898 if !crate::sketch::spline::is_curvature_joint(doc, geos[0], geos[1]) {
899 return None;
900 }
901 let (a, b) = (geos[0], geos[1]);
902 return Some(vec![
903 a.points[0].clone(),
904 a.points[1].clone(),
905 b.points[0].clone(),
906 b.points[1].clone(),
907 ]);
908 }
909 let guide = geos
910 .iter()
911 .find(|g| crate::sketch::spline::is_end_handle_guide(doc, g))?;
912 let circ = geos.iter().find(|g| is_radial(g))?;
913 if circ.points.len() < 2 {
914 return None;
915 }
916 Some(vec![
917 guide.points[0].clone(),
918 guide.points[1].clone(),
919 circ.points[0].clone(),
920 circ.points[1].clone(),
921 ])
922}
923
924pub(super) fn sketch_perpendicular_should_swap(
928 doc: &crate::sketch::SketchDoc,
929 pts: &[serde_json::Value],
930) -> bool {
931 let coord = |v: &serde_json::Value| doc.point(v).map(|p| (p.x, p.y));
932 let (Some(p0), Some(p1), Some(p2), Some(p3)) =
933 (coord(&pts[0]), coord(&pts[1]), coord(&pts[2]), coord(&pts[3]))
934 else {
935 return false;
936 };
937 let angle = |a: (f64, f64), b: (f64, f64)| -> f64 {
939 let deg = (b.1 - a.1).atan2(b.0 - a.0) * 180.0 / std::f64::consts::PI;
940 (deg + 360.0) % 360.0
941 };
942 let fold = |a: f64| (a + 180.0) % 360.0 - 180.0;
944 let line1_a = fold(angle(p0, p1));
945 let line1_b = fold(angle(p1, p0));
946 let line2 = fold(angle(p2, p3));
947 let diff_a = line1_a - line2;
948 let diff_b = line1_b - line2;
949 (90.0 - diff_a).abs() > (90.0 - diff_b).abs()
950}
951
952pub(super) fn sketch_constraint_signature(ctype: &str, points: &[serde_json::Value]) -> String {
955 use crate::sketch::doc::id_key;
956 let mut keys: Vec<String> = points.iter().map(id_key).collect();
957 keys.sort();
958 format!("{ctype}|{}", keys.join(","))
959}
960
961fn sketch_build_and_add_constraint(
966 session: &mut crate::sketch::SketchSession,
967 symbol: &str,
968) -> bool {
969 struct Built {
973 store_type: String,
974 display_style: &'static str,
975 lists: Vec<Vec<serde_json::Value>>,
976 }
977
978 fn sketch_equal_to_first(t: &str, geos: &[&crate::sketch::SketchGeometry]) -> Built {
981 let first = geos[0];
982 Built {
983 store_type: t.to_string(),
984 display_style: "",
985 lists: geos[1..]
986 .iter()
987 .map(|g| {
988 vec![
989 first.points[0].clone(),
990 first.points[1].clone(),
991 g.points[0].clone(),
992 g.points[1].clone(),
993 ]
994 })
995 .collect(),
996 }
997 }
998
999 let built: Option<Built> = {
1001 let doc = &session.doc;
1002 let mut selected: Vec<serde_json::Value> = Vec::new();
1007 let mut geo_items: Vec<&crate::sketch::SketchGeometry> = Vec::new();
1008 let mut point_items: Vec<serde_json::Value> = Vec::new();
1009 let mut geometry_type: Option<String> = None;
1010 let mut first_kind: Option<String> = None;
1011 let mut has_geometry = false;
1012 for (i, r) in session.selection.iter().enumerate() {
1013 let kind = r.get("kind").and_then(|v| v.as_str());
1014 let Some(id) = r.get("id") else { continue };
1015 if i == 0 {
1016 first_kind = kind.map(|k| k.to_string());
1017 }
1018 match kind {
1019 Some("point") => {
1020 if let Some(p) = doc.point(id) {
1021 selected.push(p.id.clone());
1022 point_items.push(p.id.clone());
1023 }
1024 }
1025 Some("geometry") => {
1026 if let Some(g) = doc.geometry(id) {
1027 for pid in &g.points {
1028 if doc.point(pid).is_some() {
1029 selected.push(pid.clone());
1030 }
1031 }
1032 if g.geom_type == "arc" {
1033 selected.pop();
1034 }
1035 geometry_type = Some(g.geom_type.clone());
1036 geo_items.push(g);
1037 has_geometry = true;
1038 }
1039 }
1040 _ => {}
1041 }
1042 }
1043 if selected.is_empty() {
1044 None
1045 } else {
1046 let radial = |g: &crate::sketch::SketchGeometry| {
1047 g.geom_type == "arc" || g.geom_type == "circle"
1048 };
1049 let simple = |t: &str, list: Vec<serde_json::Value>, ds: &'static str| Built {
1050 store_type: t.to_string(),
1051 display_style: ds,
1052 lists: vec![list],
1053 };
1054
1055 match symbol {
1057 "◎" => {
1058 if geo_items.len() == 2 && geo_items.iter().all(|g| radial(g)) {
1059 Some(simple(
1060 "◎",
1061 vec![geo_items[0].points[0].clone(), geo_items[1].points[0].clone()],
1062 "",
1063 ))
1064 } else {
1065 None
1066 }
1067 }
1068 "⊜" => {
1072 if geo_items.len() >= 2
1073 && geo_items.iter().all(|g| radial(g) && g.points.len() >= 2)
1074 {
1075 Some(sketch_equal_to_first("⊜", &geo_items))
1076 } else {
1077 None
1078 }
1079 }
1080 "⇌" if geo_items.len() >= 2
1083 && point_items.is_empty()
1084 && geo_items
1085 .iter()
1086 .all(|g| g.geom_type == "line" && g.points.len() >= 2) =>
1087 {
1088 Some(sketch_equal_to_first("⇌", &geo_items))
1089 }
1090 "⌒" => sketch_build_tangent_points(doc, &geo_items).map(|pts| simple("⌒", pts, "")),
1091 "ϰ" => {
1092 sketch_build_curvature_points(doc, &geo_items).map(|pts| simple("ϰ", pts, ""))
1093 }
1094 "∿" => sketch_build_foot_tangent_points(doc, &geo_items)
1095 .map(|pts| simple("∿", pts, "")),
1096 "⋰" => {
1097 let ids = if geo_items.len() >= 2
1098 && geo_items.iter().all(|g| g.geom_type == "line")
1099 {
1100 let mut v = Vec::new();
1101 for g in &geo_items {
1102 v.push(g.points[0].clone());
1103 v.push(g.points[1].clone());
1104 }
1105 Some(v)
1106 } else if point_items.len() >= 3 {
1107 Some(point_items.clone())
1108 } else {
1109 None
1110 };
1111 ids.filter(|v| v.len() >= 3).map(|v| simple("⋰", v, ""))
1112 }
1113 "⋈" => geo_items
1114 .iter()
1115 .find(|g| g.geom_type == "line")
1116 .filter(|line| line.points.len() >= 2 && point_items.len() == 2)
1117 .map(|line| {
1118 simple(
1119 "⋈",
1120 vec![
1121 line.points[0].clone(),
1122 line.points[1].clone(),
1123 point_items[0].clone(),
1124 point_items[1].clone(),
1125 ],
1126 "",
1127 )
1128 }),
1129 "R" | "⌀" => {
1132 if selected.len() == 2 {
1133 let ds = if symbol == "⌀" { "diameter" } else { "radius" };
1134 Some(Built {
1135 store_type: "⟺".to_string(),
1136 display_style: ds,
1137 lists: vec![selected.clone()],
1138 })
1139 } else {
1140 None
1141 }
1142 }
1143 _ => {
1144 match selected.len() {
1146 1 => match symbol {
1147 "⏚" => Some(simple("⏚", selected.clone(), "")),
1148 _ => None,
1149 },
1150 2 => match symbol {
1151 "━" | "│" | "≡" => Some(simple(symbol, selected.clone(), "")),
1152 "⟺" => {
1153 let ds = if matches!(
1154 geometry_type.as_deref(),
1155 Some("arc") | Some("circle")
1156 ) {
1157 "radius"
1158 } else {
1159 ""
1160 };
1161 Some(Built {
1162 store_type: "⟺".to_string(),
1163 display_style: ds,
1164 lists: vec![selected.clone()],
1165 })
1166 }
1167 _ => None,
1168 },
1169 3 => match symbol {
1170 "⏛" => Some(simple("⏛", selected.clone(), "")),
1171 "⋯" => {
1172 let mut pts = selected.clone();
1173 if has_geometry && first_kind.as_deref() == Some("point") {
1174 pts.reverse();
1175 }
1176 Some(simple("⋯", pts, ""))
1177 }
1178 "↥" => {
1179 if geo_items.len() == 1 && point_items.len() == 1 {
1180 let line = geo_items[0];
1181 if line.geom_type == "line" && line.points.len() >= 2 {
1182 Some(simple(
1183 "↥",
1184 vec![
1185 line.points[0].clone(),
1186 line.points[1].clone(),
1187 point_items[0].clone(),
1188 ],
1189 "",
1190 ))
1191 } else {
1192 None
1193 }
1194 } else {
1195 Some(simple("↥", selected.clone(), ""))
1198 }
1199 }
1200 "⇌" => Some(simple("⇌", selected.clone(), "")),
1201 _ => None,
1202 },
1203 4 | 5 => match symbol {
1204 "⏛" => {
1205 if geo_items.len() == 2
1208 && geo_items.iter().all(|g| g.geom_type == "line")
1209 && geo_items[0].points.len() >= 2
1210 && geo_items[1].points.len() >= 2
1211 {
1212 let (g0, g1) = (geo_items[0], geo_items[1]);
1213 Some(Built {
1214 store_type: "⏛".to_string(),
1215 display_style: "",
1216 lists: vec![
1217 vec![
1218 g0.points[0].clone(),
1219 g0.points[1].clone(),
1220 g1.points[0].clone(),
1221 ],
1222 vec![
1223 g0.points[0].clone(),
1224 g0.points[1].clone(),
1225 g1.points[1].clone(),
1226 ],
1227 ],
1228 })
1229 } else {
1230 None
1231 }
1232 }
1233 "⟂" => {
1234 if selected.len() != 4 {
1235 None
1236 } else {
1237 let mut pts = selected.clone();
1238 if sketch_perpendicular_should_swap(doc, &pts) {
1239 pts.swap(0, 1);
1240 }
1241 Some(simple("⟂", pts, ""))
1242 }
1243 }
1244 "∥" => Some(simple("∥", selected.clone(), "")),
1245 "∠" => Some(simple("∠", selected.clone(), "")),
1246 "⇌" => Some(simple("⇌", selected.clone(), "")),
1247 _ => None,
1248 },
1249 _ => None,
1250 }
1251 }
1252 }
1253 }
1254 };
1255
1256 let Some(built) = built else {
1257 return false;
1258 };
1259
1260 let doc = &mut session.doc;
1263 let mut added_any = false;
1264 for pts in &built.lists {
1265 let sig = sketch_constraint_signature(&built.store_type, pts);
1266 let duplicate = doc.constraints.iter().any(|c| match c.ctype() {
1267 Some(t) => sketch_constraint_signature(t, c.points()) == sig,
1268 None => false,
1269 });
1270 if duplicate {
1271 continue;
1272 }
1273 let id = doc.next_constraint_id();
1274 let mut raw = serde_json::Map::new();
1275 raw.insert("id".to_string(), id);
1276 raw.insert(
1277 "type".to_string(),
1278 serde_json::Value::String(built.store_type.clone()),
1279 );
1280 raw.insert("points".to_string(), serde_json::Value::Array(pts.clone()));
1281 raw.insert("labelX".to_string(), serde_json::Value::from(0));
1282 raw.insert("labelY".to_string(), serde_json::Value::from(0));
1283 raw.insert(
1284 "displayStyle".to_string(),
1285 serde_json::Value::String(built.display_style.to_string()),
1286 );
1287 raw.insert("value".to_string(), serde_json::Value::Null);
1288 raw.insert("valueNeedsSetup".to_string(), serde_json::Value::Bool(true));
1289 doc.constraints.push(crate::sketch::SketchConstraint { raw });
1290 added_any = true;
1291 }
1292 added_any
1293}
1294