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 let note = g.extra.get("note").and_then(serde_json::Value::as_str).map(|n| format!(" \u{2014} {n}")).unwrap_or_default();
171 SketchEntityRow {
172 kind: "geometry",
173 id: g.id.clone(),
174 label: format!("{}:{}{mark} [{pts}]{note}", g.geom_type, id_key(&g.id)),
175 selected: Self::sketch_ref_selected(session, "geometry", &g.id),
176 construction,
177 conflicting: false,
178 }
179 })
180 .collect()
181 }
182
183 pub fn sketch_constraint_rows(&self) -> Vec<SketchEntityRow> {
186 use crate::sketch::doc::id_key;
187 let Some(edit) = self.sketch_edit.as_ref() else {
188 return Vec::new();
189 };
190 let session = &edit.session;
191 session
192 .doc
193 .constraints
194 .iter()
195 .filter_map(|c| {
196 let id = c.raw.get("id")?.clone();
197 let ctype = c.ctype().unwrap_or("?");
198 let value = c
199 .raw
200 .get("value")
201 .and_then(serde_json::Value::as_f64)
202 .map(|v| format!(" {v:.3}"))
203 .unwrap_or_default();
204 let pts = c
205 .points()
206 .iter()
207 .map(id_key)
208 .collect::<Vec<_>>()
209 .join(",");
210 Some(SketchEntityRow {
211 kind: "constraint",
212 id: id.clone(),
213 label: format!("{} {ctype}{value} [{pts}]", id_key(&id)),
214 selected: Self::sketch_ref_selected(session, "constraint", &id),
215 construction: false,
216 conflicting: session.diagnostics.constraint_conflicting(&id),
217 })
218 })
219 .collect()
220 }
221
222 pub fn sketch_select_entity(&mut self, kind: &str, id: serde_json::Value, additive: bool) {
227 let toggles = self.settings.multi_select == crate::style::MultiSelectMode::ClickToggles;
228 let entity_ref = serde_json::json!({ "kind": kind, "id": id });
229 let Some(edit) = self.sketch_edit.as_mut() else {
230 return;
231 };
232 if !additive && !toggles {
233 edit.session.clear_selection();
234 }
235 edit.session.toggle_selection(entity_ref);
236 self.refresh_sketch_overlay();
237 self.dirty = true;
238 }
239
240 pub fn sketch_hover_entity(&mut self, kind: &str, id: serde_json::Value) {
244 if self.sketch_edit.is_none() {
245 return;
246 }
247 let entity_ref = serde_json::json!({ "kind": kind, "id": id });
250 self.set_sketch_hover(Some(entity_ref));
251 self.sketch_list_hover_active = true;
252 }
253
254 pub fn take_sketch_list_hover(&mut self) -> bool {
258 std::mem::take(&mut self.sketch_list_hover_active)
259 }
260
261 pub fn sketch_solver_settings(&self) -> Option<crate::sketch::SketchSolverSettings> {
264 self.sketch_edit
265 .as_ref()
266 .map(|edit| edit.session.solver_settings.clone())
267 }
268
269 pub fn sketch_set_solver_settings(&mut self, settings: crate::sketch::SketchSolverSettings) {
272 if let Some(edit) = self.sketch_edit.as_mut() {
273 edit.session.solver_settings = settings;
274 } else {
275 return;
276 }
277 self.resolve_active_sketch("solver settings");
278 self.refresh_sketch_overlay();
279 self.dirty = true;
280 }
281}
282
283#[derive(Clone, Debug, PartialEq)]
287pub struct SketchConstraintAction {
288 pub symbol: String,
291 pub label: String,
293 pub dimensional: bool,
296}
297
298impl EngineState {
299 pub fn sketch_applicable_constraints(&self) -> Vec<SketchConstraintAction> {
309 match self.sketch_edit.as_ref() {
310 Some(edit) => sketch_palette_for(&edit.session),
311 None => Vec::new(),
312 }
313 }
314
315 pub fn sketch_add_constraint(&mut self, symbol: &str) -> bool {
320 let added = match self.sketch_edit.as_mut() {
323 Some(edit) => {
324 edit.record_undo();
325 sketch_build_and_add_constraint(&mut edit.session, symbol)
326 }
327 None => return false,
328 };
329 if added {
330 self.resolve_active_sketch("add-constraint");
331 self.refresh_sketch_overlay();
332 self.dirty = true;
333 } else if let Some(edit) = self.sketch_edit.as_mut() {
334 edit.undo_stack.pop();
335 }
336 added
337 }
338
339 pub fn sketch_toggle_ground(&mut self) -> bool {
345 use crate::sketch::doc::id_key;
346 use std::collections::HashSet;
347
348 let Some(edit) = self.sketch_edit.as_mut() else {
349 return false;
350 };
351 let sel_ids: Vec<serde_json::Value> = edit
352 .session
353 .selection
354 .iter()
355 .filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("point"))
356 .filter_map(|r| r.get("id").cloned())
357 .filter(|id| !crate::sketch::external_ref::is_origin_id(id))
359 .collect();
360 if sel_ids.is_empty() {
361 return false;
362 }
363 edit.record_undo();
365 let doc = &mut edit.session.doc;
366 let has_ground = |doc: &crate::sketch::SketchDoc, id: &serde_json::Value| -> bool {
367 doc.constraints.iter().any(|c| {
368 c.ctype() == Some("⏚")
369 && c.points().first().map(id_key) == Some(id_key(id))
370 })
371 };
372 let all_grounded = sel_ids.iter().all(|id| has_ground(doc, id));
373 if all_grounded {
374 let sel_keys: HashSet<String> = sel_ids.iter().map(id_key).collect();
375 doc.constraints.retain(|c| {
376 if c.ctype() != Some("⏚") {
377 return true;
378 }
379 match c.points().first() {
380 Some(p) => !sel_keys.contains(&id_key(p)),
381 None => true,
382 }
383 });
384 for id in &sel_ids {
385 if let Some(p) = doc.point_mut(id) {
386 p.fixed = false;
387 }
388 }
389 } else {
390 for id in &sel_ids {
391 if has_ground(doc, id) {
392 continue;
393 }
394 let cid = doc.next_constraint_id();
395 let mut raw = serde_json::Map::new();
396 raw.insert("id".to_string(), cid);
397 raw.insert("type".to_string(), serde_json::Value::String("⏚".to_string()));
398 raw.insert(
399 "points".to_string(),
400 serde_json::Value::Array(vec![id.clone()]),
401 );
402 doc.constraints.push(crate::sketch::SketchConstraint { raw });
403 if let Some(p) = doc.point_mut(id) {
404 p.fixed = true;
405 }
406 }
407 }
408 self.resolve_active_sketch("toggle-ground");
409 self.refresh_sketch_overlay();
410 self.dirty = true;
411 true
412 }
413
414 pub fn sketch_toggle_construction(&mut self) -> bool {
419 let Some(edit) = self.sketch_edit.as_mut() else {
420 return false;
421 };
422 let mut pt_ids: Vec<serde_json::Value> = Vec::new();
423 let mut geo_ids: Vec<serde_json::Value> = Vec::new();
424 for r in &edit.session.selection {
425 match r.get("kind").and_then(|v| v.as_str()) {
426 Some("point") => {
427 if let Some(id) = r.get("id").filter(|id| !crate::sketch::external_ref::is_origin_id(id)) {
429 pt_ids.push(id.clone());
430 }
431 }
432 Some("geometry") => {
433 if let Some(id) = r.get("id") {
434 geo_ids.push(id.clone());
435 }
436 }
437 _ => {}
438 }
439 }
440 if pt_ids.is_empty() && geo_ids.is_empty() {
441 return false;
442 }
443 edit.record_undo();
445 let doc = &mut edit.session.doc;
446 let all_construction = pt_ids
447 .iter()
448 .all(|id| doc.point(id).map_or(false, |p| p.construction))
449 && geo_ids
450 .iter()
451 .all(|id| doc.geometry(id).map_or(false, |g| g.construction()));
452 let next = !all_construction;
453 for id in &pt_ids {
454 if let Some(p) = doc.point_mut(id) {
455 p.construction = next;
456 }
457 }
458 for id in &geo_ids {
459 if let Some(g) = doc.geometry_mut(id) {
460 g.extra
461 .insert("construction".to_string(), serde_json::Value::Bool(next));
462 }
463 }
464 self.resolve_active_sketch("toggle-construction");
465 self.refresh_sketch_overlay();
466 self.dirty = true;
467 true
468 }
469
470 pub fn sketch_cleanup_unused_points(&mut self) -> bool {
474 use crate::sketch::doc::id_key;
475 use std::collections::HashSet;
476
477 let Some(edit) = self.sketch_edit.as_mut() else {
478 return false;
479 };
480 edit.record_undo();
482 let doc = &mut edit.session.doc;
483 let mut used: HashSet<String> = HashSet::new();
484 for g in &doc.geometries {
485 for pid in &g.points {
486 used.insert(id_key(pid));
487 }
488 }
489 for c in &doc.constraints {
490 for pid in c.points() {
491 used.insert(id_key(pid));
492 }
493 }
494 let before = doc.points.len();
495 doc.points.retain(|p| used.contains(&id_key(&p.id)));
496 let removed = doc.points.len() != before;
497 if !removed {
498 edit.undo_stack.pop();
499 return false;
500 }
501 self.resolve_active_sketch("cleanup");
502 self.refresh_sketch_overlay();
503 self.dirty = true;
504 true
505 }
506
507 pub fn sketch_constraint_count(&self) -> usize {
510 self.sketch_edit
511 .as_ref()
512 .map_or(0, |edit| edit.session.doc.constraints.len())
513 }
514
515 pub fn sketch_selection_all_grounded(&self) -> Option<bool> {
519 use crate::sketch::doc::id_key;
520 let edit = self.sketch_edit.as_ref()?;
521 let doc = &edit.session.doc;
522 let sel: Vec<&serde_json::Value> = edit
523 .session
524 .selection
525 .iter()
526 .filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("point"))
527 .filter_map(|r| r.get("id"))
528 .collect();
529 if sel.is_empty() {
530 return None;
531 }
532 let all = sel.iter().all(|id| {
533 doc.constraints.iter().any(|c| {
534 c.ctype() == Some("⏚")
535 && c.points().first().map(id_key) == Some(id_key(id))
536 })
537 });
538 Some(all)
539 }
540
541 pub fn sketch_selection_all_construction(&self) -> Option<bool> {
545 let edit = self.sketch_edit.as_ref()?;
546 let doc = &edit.session.doc;
547 let mut any = false;
548 let mut all = true;
549 for r in &edit.session.selection {
550 match r.get("kind").and_then(|v| v.as_str()) {
551 Some("point") => {
552 any = true;
553 if let Some(id) = r.get("id") {
554 if !doc.point(id).map_or(false, |p| p.construction) {
555 all = false;
556 }
557 }
558 }
559 Some("geometry") => {
560 any = true;
561 if let Some(id) = r.get("id") {
562 if !doc.geometry(id).map_or(false, |g| g.construction()) {
563 all = false;
564 }
565 }
566 }
567 _ => {}
568 }
569 }
570 if !any {
571 return None;
572 }
573 Some(all)
574 }
575}
576
577fn sketch_palette_for(session: &crate::sketch::SketchSession) -> Vec<SketchConstraintAction> {
581 use crate::sketch::doc::id_key;
582 use std::collections::HashSet;
583
584 let doc = &session.doc;
585 let mut sel_points: Vec<serde_json::Value> = Vec::new();
586 let mut geos: Vec<&crate::sketch::SketchGeometry> = Vec::new();
587 for r in &session.selection {
588 match r.get("kind").and_then(|v| v.as_str()) {
589 Some("point") => {
590 if let Some(id) = r.get("id") {
591 sel_points.push(id.clone());
592 }
593 }
594 Some("geometry") => {
595 if let Some(id) = r.get("id") {
596 if let Some(g) = doc.geometry(id) {
597 geos.push(g);
598 }
599 }
600 }
601 _ => {}
602 }
603 }
604
605 let mut point_set: HashSet<String> = HashSet::new();
608 for id in &sel_points {
609 point_set.insert(id_key(id));
610 }
611 for g in &geos {
612 let pts: &[serde_json::Value] = if g.geom_type == "arc" {
613 &g.points[..g.points.len().min(2)]
614 } else {
615 &g.points
616 };
617 for pid in pts {
618 point_set.insert(id_key(pid));
619 }
620 }
621 let point_count = point_set.len();
622 let selected_point_ids_len = sel_points.len();
623 let is_radial =
624 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
625
626 let mut out: Vec<SketchConstraintAction> = Vec::new();
627 let mut push = |symbol: &str, label: &str, dimensional: bool| {
628 out.push(SketchConstraintAction {
629 symbol: symbol.to_string(),
630 label: label.to_string(),
631 dimensional,
632 });
633 };
634
635 if geos.len() == 1 && is_radial(geos[0]) {
637 push("R", "Radius", true);
638 push("⌀", "Diameter", true);
639 return out;
640 }
641 if geos.len() == 2 && geos.iter().all(|g| g.geom_type == "line") {
650 push("∥", "Parallel", false);
651 push("⟂", "Perpendicular", false);
652 push("∠", "Angle", true);
653 push("⇌", "Equal distance", false);
654 push("⋰", "Collinear", false);
655 push("⏛", "Point on line", false);
656 if geos
657 .iter()
658 .any(|g| crate::sketch::spline::is_handle_guide(doc, g))
659 {
660 push("⌒", "Tangent", false);
661 }
662 if crate::sketch::spline::is_curvature_joint(doc, geos[0], geos[1]) {
668 push("ϰ", "Curvature (G2)", false);
669 }
670 return out;
671 }
672 if geos.len() == 2 && geos.iter().all(|g| is_radial(g)) {
674 push("⊜", "Equal radius", false);
675 push("◎", "Concentric", false);
676 push("⌒", "Tangent", false);
677 return out;
678 }
679 if geos.len() > 2 && sel_points.is_empty() {
683 if geos.iter().all(|g| g.geom_type == "line") {
684 push("⇌", "Equal distance", false);
685 return out;
686 }
687 if geos.iter().all(|g| is_radial(g)) {
688 push("⊜", "Equal radius", false);
689 return out;
690 }
691 }
692 if geos.len() == 2
696 && ((geos[0].geom_type == "line" && is_radial(geos[1]))
697 || (geos[1].geom_type == "line" && is_radial(geos[0])))
698 {
699 push("⌒", "Tangent", false);
700 if geos
701 .iter()
702 .any(|g| crate::sketch::spline::is_end_handle_guide(doc, g))
703 {
704 push("ϰ", "Curvature (G2)", false);
705 }
706 return out;
707 }
708 if sketch_foot_tangent_pair(doc, &geos).is_some() {
718 push("∿", "Tangent (span point)", false);
719 return out;
720 }
721 if geos.len() == 1 && geos[0].geom_type == "line" && selected_point_ids_len == 2 {
723 push("⟂", "Perpendicular", false);
724 push("⋈", "Symmetric about line", false);
725 }
726
727 if point_count == 1 {
728 push("⏚", "Ground (fix point)", false);
729 }
730 if point_count == 2 {
731 push("━", "Horizontal", false);
732 push("│", "Vertical", false);
733 push("≡", "Coincident", false);
734 push("⟺", "Distance", true);
735 }
736 if point_count == 3 {
737 push("⋯", "Midpoint", false);
738 push("⏛", "Point on line", false);
739 push("↥", "Line to point distance", true);
740 push("∠", "Angle", true);
741 if selected_point_ids_len >= 3 {
743 push("⋰", "Collinear", false);
744 }
745 }
746 if point_count > 3 && geos.is_empty() && selected_point_ids_len >= 3 {
748 push("⋰", "Collinear", false);
749 }
750
751 out
752}
753
754fn sketch_build_tangent_points(
765 doc: &crate::sketch::SketchDoc,
766 geos: &[&crate::sketch::SketchGeometry],
767) -> Option<Vec<serde_json::Value>> {
768 if geos.len() != 2 {
769 return None;
770 }
771 let is_radial = |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
772 if geos.iter().all(|g| g.geom_type == "line") {
773 let (a, b) = (geos[0], geos[1]);
774 if a.points.len() < 2 || b.points.len() < 2 {
775 return None;
776 }
777 if !crate::sketch::spline::is_handle_guide(doc, a)
778 && !crate::sketch::spline::is_handle_guide(doc, b)
779 {
780 return None;
781 }
782 return Some(vec![
783 a.points[0].clone(),
784 a.points[1].clone(),
785 b.points[0].clone(),
786 b.points[1].clone(),
787 ]);
788 }
789 let line = geos.iter().find(|g| g.geom_type == "line");
790 let circ = geos.iter().find(|g| is_radial(g));
791 if let (Some(line), Some(circ)) = (line, circ) {
792 if line.points.len() < 2 || circ.points.len() < 2 {
793 return None;
794 }
795 return Some(vec![
796 line.points[0].clone(),
797 line.points[1].clone(),
798 circ.points[0].clone(),
799 circ.points[1].clone(),
800 ]);
801 }
802 if geos.iter().all(|g| is_radial(g)) {
803 let (a, b) = (geos[0], geos[1]);
804 if a.points.len() < 2 || b.points.len() < 2 {
805 return None;
806 }
807 return Some(vec![
808 a.points[0].clone(),
809 a.points[1].clone(),
810 b.points[0].clone(),
811 b.points[1].clone(),
812 ]);
813 }
814 None
815}
816
817fn sketch_foot_tangent_pair<'a>(
831 doc: &crate::sketch::SketchDoc,
832 geos: &[&'a crate::sketch::SketchGeometry],
833) -> Option<(&'a crate::sketch::SketchGeometry, &'a crate::sketch::SketchGeometry)> {
834 if geos.len() != 2 {
835 return None;
836 }
837 let is_spline = |g: &crate::sketch::SketchGeometry| {
838 crate::sketch::spline::is_spline_type(&g.geom_type)
839 };
840 let is_radial =
841 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
842 let spline = *geos.iter().find(|g| is_spline(g))?;
843 let target = *geos.iter().find(|g| !is_spline(g))?;
844 if spline.points.len() < 4 || target.points.len() < 2 {
845 return None;
846 }
847 let target_is_usable = if target.geom_type == "line" {
848 !crate::sketch::spline::is_handle_guide(doc, target)
849 } else {
850 is_radial(target)
851 };
852 target_is_usable.then_some((spline, target))
853}
854
855fn sketch_build_foot_tangent_points(
866 doc: &crate::sketch::SketchDoc,
867 geos: &[&crate::sketch::SketchGeometry],
868) -> Option<Vec<serde_json::Value>> {
869 let (spline, target) = sketch_foot_tangent_pair(doc, geos)?;
870 Some(vec![
871 target.points[0].clone(),
872 target.points[1].clone(),
873 spline.points[0].clone(),
874 spline.points[1].clone(),
875 ])
876}
877
878fn sketch_build_curvature_points(
891 doc: &crate::sketch::SketchDoc,
892 geos: &[&crate::sketch::SketchGeometry],
893) -> Option<Vec<serde_json::Value>> {
894 if geos.len() != 2 {
895 return None;
896 }
897 let is_radial =
898 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
899 if geos.iter().all(|g| g.geom_type == "line") {
900 if !crate::sketch::spline::is_curvature_joint(doc, geos[0], geos[1]) {
901 return None;
902 }
903 let (a, b) = (geos[0], geos[1]);
904 return Some(vec![
905 a.points[0].clone(),
906 a.points[1].clone(),
907 b.points[0].clone(),
908 b.points[1].clone(),
909 ]);
910 }
911 let guide = geos
912 .iter()
913 .find(|g| crate::sketch::spline::is_end_handle_guide(doc, g))?;
914 let circ = geos.iter().find(|g| is_radial(g))?;
915 if circ.points.len() < 2 {
916 return None;
917 }
918 Some(vec![
919 guide.points[0].clone(),
920 guide.points[1].clone(),
921 circ.points[0].clone(),
922 circ.points[1].clone(),
923 ])
924}
925
926pub(super) fn sketch_perpendicular_should_swap(
930 doc: &crate::sketch::SketchDoc,
931 pts: &[serde_json::Value],
932) -> bool {
933 let coord = |v: &serde_json::Value| doc.point(v).map(|p| (p.x, p.y));
934 let (Some(p0), Some(p1), Some(p2), Some(p3)) =
935 (coord(&pts[0]), coord(&pts[1]), coord(&pts[2]), coord(&pts[3]))
936 else {
937 return false;
938 };
939 let angle = |a: (f64, f64), b: (f64, f64)| -> f64 {
941 let deg = (b.1 - a.1).atan2(b.0 - a.0) * 180.0 / std::f64::consts::PI;
942 (deg + 360.0) % 360.0
943 };
944 let fold = |a: f64| (a + 180.0) % 360.0 - 180.0;
946 let line1_a = fold(angle(p0, p1));
947 let line1_b = fold(angle(p1, p0));
948 let line2 = fold(angle(p2, p3));
949 let diff_a = line1_a - line2;
950 let diff_b = line1_b - line2;
951 (90.0 - diff_a).abs() > (90.0 - diff_b).abs()
952}
953
954pub(super) fn sketch_constraint_signature(ctype: &str, points: &[serde_json::Value]) -> String {
957 use crate::sketch::doc::id_key;
958 let mut keys: Vec<String> = points.iter().map(id_key).collect();
959 keys.sort();
960 format!("{ctype}|{}", keys.join(","))
961}
962
963fn sketch_build_and_add_constraint(
968 session: &mut crate::sketch::SketchSession,
969 symbol: &str,
970) -> bool {
971 struct Built {
975 store_type: String,
976 display_style: &'static str,
977 lists: Vec<Vec<serde_json::Value>>,
978 }
979
980 fn sketch_equal_to_first(t: &str, geos: &[&crate::sketch::SketchGeometry]) -> Built {
983 let first = geos[0];
984 Built {
985 store_type: t.to_string(),
986 display_style: "",
987 lists: geos[1..]
988 .iter()
989 .map(|g| {
990 vec![
991 first.points[0].clone(),
992 first.points[1].clone(),
993 g.points[0].clone(),
994 g.points[1].clone(),
995 ]
996 })
997 .collect(),
998 }
999 }
1000
1001 let built: Option<Built> = {
1003 let doc = &session.doc;
1004 let mut selected: Vec<serde_json::Value> = Vec::new();
1009 let mut geo_items: Vec<&crate::sketch::SketchGeometry> = Vec::new();
1010 let mut point_items: Vec<serde_json::Value> = Vec::new();
1011 let mut geometry_type: Option<String> = None;
1012 let mut first_kind: Option<String> = None;
1013 let mut has_geometry = false;
1014 for (i, r) in session.selection.iter().enumerate() {
1015 let kind = r.get("kind").and_then(|v| v.as_str());
1016 let Some(id) = r.get("id") else { continue };
1017 if i == 0 {
1018 first_kind = kind.map(|k| k.to_string());
1019 }
1020 match kind {
1021 Some("point") => {
1022 if let Some(p) = doc.point(id) {
1023 selected.push(p.id.clone());
1024 point_items.push(p.id.clone());
1025 }
1026 }
1027 Some("geometry") => {
1028 if let Some(g) = doc.geometry(id) {
1029 for pid in &g.points {
1030 if doc.point(pid).is_some() {
1031 selected.push(pid.clone());
1032 }
1033 }
1034 if g.geom_type == "arc" {
1035 selected.pop();
1036 }
1037 geometry_type = Some(g.geom_type.clone());
1038 geo_items.push(g);
1039 has_geometry = true;
1040 }
1041 }
1042 _ => {}
1043 }
1044 }
1045 if selected.is_empty() {
1046 None
1047 } else {
1048 let radial = |g: &crate::sketch::SketchGeometry| {
1049 g.geom_type == "arc" || g.geom_type == "circle"
1050 };
1051 let simple = |t: &str, list: Vec<serde_json::Value>, ds: &'static str| Built {
1052 store_type: t.to_string(),
1053 display_style: ds,
1054 lists: vec![list],
1055 };
1056
1057 match symbol {
1059 "◎" => {
1060 if geo_items.len() == 2 && geo_items.iter().all(|g| radial(g)) {
1061 Some(simple(
1062 "◎",
1063 vec![geo_items[0].points[0].clone(), geo_items[1].points[0].clone()],
1064 "",
1065 ))
1066 } else {
1067 None
1068 }
1069 }
1070 "⊜" => {
1074 if geo_items.len() >= 2
1075 && geo_items.iter().all(|g| radial(g) && g.points.len() >= 2)
1076 {
1077 Some(sketch_equal_to_first("⊜", &geo_items))
1078 } else {
1079 None
1080 }
1081 }
1082 "⇌" if geo_items.len() >= 2
1085 && point_items.is_empty()
1086 && geo_items
1087 .iter()
1088 .all(|g| g.geom_type == "line" && g.points.len() >= 2) =>
1089 {
1090 Some(sketch_equal_to_first("⇌", &geo_items))
1091 }
1092 "⌒" => sketch_build_tangent_points(doc, &geo_items).map(|pts| simple("⌒", pts, "")),
1093 "ϰ" => {
1094 sketch_build_curvature_points(doc, &geo_items).map(|pts| simple("ϰ", pts, ""))
1095 }
1096 "∿" => sketch_build_foot_tangent_points(doc, &geo_items)
1097 .map(|pts| simple("∿", pts, "")),
1098 "⋰" => {
1099 let ids = if geo_items.len() >= 2
1100 && geo_items.iter().all(|g| g.geom_type == "line")
1101 {
1102 let mut v = Vec::new();
1103 for g in &geo_items {
1104 v.push(g.points[0].clone());
1105 v.push(g.points[1].clone());
1106 }
1107 Some(v)
1108 } else if point_items.len() >= 3 {
1109 Some(point_items.clone())
1110 } else {
1111 None
1112 };
1113 ids.filter(|v| v.len() >= 3).map(|v| simple("⋰", v, ""))
1114 }
1115 "⋈" => geo_items
1116 .iter()
1117 .find(|g| g.geom_type == "line")
1118 .filter(|line| line.points.len() >= 2 && point_items.len() == 2)
1119 .map(|line| {
1120 simple(
1121 "⋈",
1122 vec![
1123 line.points[0].clone(),
1124 line.points[1].clone(),
1125 point_items[0].clone(),
1126 point_items[1].clone(),
1127 ],
1128 "",
1129 )
1130 }),
1131 "R" | "⌀" => {
1134 if selected.len() == 2 {
1135 let ds = if symbol == "⌀" { "diameter" } else { "radius" };
1136 Some(Built {
1137 store_type: "⟺".to_string(),
1138 display_style: ds,
1139 lists: vec![selected.clone()],
1140 })
1141 } else {
1142 None
1143 }
1144 }
1145 _ => {
1146 match selected.len() {
1148 1 => match symbol {
1149 "⏚" => Some(simple("⏚", selected.clone(), "")),
1150 _ => None,
1151 },
1152 2 => match symbol {
1153 "━" | "│" | "≡" => Some(simple(symbol, selected.clone(), "")),
1154 "⟺" => {
1155 let ds = if matches!(
1156 geometry_type.as_deref(),
1157 Some("arc") | Some("circle")
1158 ) {
1159 "radius"
1160 } else {
1161 ""
1162 };
1163 Some(Built {
1164 store_type: "⟺".to_string(),
1165 display_style: ds,
1166 lists: vec![selected.clone()],
1167 })
1168 }
1169 _ => None,
1170 },
1171 3 => match symbol {
1172 "⏛" => Some(simple("⏛", selected.clone(), "")),
1173 "⋯" => {
1174 let mut pts = selected.clone();
1175 if has_geometry && first_kind.as_deref() == Some("point") {
1176 pts.reverse();
1177 }
1178 Some(simple("⋯", pts, ""))
1179 }
1180 "↥" => {
1181 if geo_items.len() == 1 && point_items.len() == 1 {
1182 let line = geo_items[0];
1183 if line.geom_type == "line" && line.points.len() >= 2 {
1184 Some(simple(
1185 "↥",
1186 vec![
1187 line.points[0].clone(),
1188 line.points[1].clone(),
1189 point_items[0].clone(),
1190 ],
1191 "",
1192 ))
1193 } else {
1194 None
1195 }
1196 } else {
1197 Some(simple("↥", selected.clone(), ""))
1200 }
1201 }
1202 "⇌" => Some(simple("⇌", selected.clone(), "")),
1203 _ => None,
1204 },
1205 4 | 5 => match symbol {
1206 "⏛" => {
1207 if geo_items.len() == 2
1210 && geo_items.iter().all(|g| g.geom_type == "line")
1211 && geo_items[0].points.len() >= 2
1212 && geo_items[1].points.len() >= 2
1213 {
1214 let (g0, g1) = (geo_items[0], geo_items[1]);
1215 Some(Built {
1216 store_type: "⏛".to_string(),
1217 display_style: "",
1218 lists: vec![
1219 vec![
1220 g0.points[0].clone(),
1221 g0.points[1].clone(),
1222 g1.points[0].clone(),
1223 ],
1224 vec![
1225 g0.points[0].clone(),
1226 g0.points[1].clone(),
1227 g1.points[1].clone(),
1228 ],
1229 ],
1230 })
1231 } else {
1232 None
1233 }
1234 }
1235 "⟂" => {
1236 if selected.len() != 4 {
1237 None
1238 } else {
1239 let mut pts = selected.clone();
1240 if sketch_perpendicular_should_swap(doc, &pts) {
1241 pts.swap(0, 1);
1242 }
1243 Some(simple("⟂", pts, ""))
1244 }
1245 }
1246 "∥" => Some(simple("∥", selected.clone(), "")),
1247 "∠" => Some(simple("∠", selected.clone(), "")),
1248 "⇌" => Some(simple("⇌", selected.clone(), "")),
1249 _ => None,
1250 },
1251 _ => None,
1252 }
1253 }
1254 }
1255 }
1256 };
1257
1258 let Some(built) = built else {
1259 return false;
1260 };
1261
1262 let doc = &mut session.doc;
1265 let mut added_any = false;
1266 for pts in &built.lists {
1267 let sig = sketch_constraint_signature(&built.store_type, pts);
1268 let duplicate = doc.constraints.iter().any(|c| match c.ctype() {
1269 Some(t) => sketch_constraint_signature(t, c.points()) == sig,
1270 None => false,
1271 });
1272 if duplicate {
1273 continue;
1274 }
1275 let id = doc.next_constraint_id();
1276 let mut raw = serde_json::Map::new();
1277 raw.insert("id".to_string(), id);
1278 raw.insert(
1279 "type".to_string(),
1280 serde_json::Value::String(built.store_type.clone()),
1281 );
1282 raw.insert("points".to_string(), serde_json::Value::Array(pts.clone()));
1283 raw.insert("labelX".to_string(), serde_json::Value::from(0));
1284 raw.insert("labelY".to_string(), serde_json::Value::from(0));
1285 raw.insert(
1286 "displayStyle".to_string(),
1287 serde_json::Value::String(built.display_style.to_string()),
1288 );
1289 raw.insert("value".to_string(), serde_json::Value::Null);
1290 raw.insert("valueNeedsSetup".to_string(), serde_json::Value::Bool(true));
1291 doc.constraints.push(crate::sketch::SketchConstraint { raw });
1292 added_any = true;
1293 }
1294 added_any
1295}
1296