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
26impl EngineState {
27 pub fn push_notice(&mut self, message: impl Into<String>) {
30 let message = message.into();
31 #[cfg(not(target_arch = "wasm32"))]
32 eprintln!("{message}");
33 self.notices.push(message);
34 if self.notices.len() > 8 {
35 let overflow = self.notices.len() - 8;
36 self.notices.drain(0..overflow);
37 }
38 }
39
40 pub fn take_notices(&mut self) -> Vec<String> {
43 std::mem::take(&mut self.notices)
44 }
45
46 pub(super) fn resolve_active_sketch(&mut self, context: &str) {
51 let error = self
52 .sketch_edit
53 .as_mut()
54 .and_then(|edit| edit.session.resolve().err());
55 if let Some(error) = error {
56 self.push_notice(format!("Sketch solve failed ({context}): {error}"));
57 }
58 }
59
60 fn sketch_ref_selected(session: &crate::sketch::SketchSession, kind: &str, id: &serde_json::Value) -> bool {
62 use crate::sketch::doc::id_key;
63 let key = id_key(id);
64 session.selection.iter().any(|r| {
65 r.get("kind").and_then(serde_json::Value::as_str) == Some(kind)
66 && r.get("id").map(id_key).as_deref() == Some(key.as_str())
67 })
68 }
69
70 pub fn sketch_point_rows(&self) -> Vec<SketchEntityRow> {
73 use crate::sketch::doc::id_key;
74 let Some(edit) = self.sketch_edit.as_ref() else {
75 return Vec::new();
76 };
77 let session = &edit.session;
78 session
79 .doc
80 .points
81 .iter()
82 .map(|p| {
83 let grounded = session.doc.constraints.iter().any(|c| {
84 c.ctype() == Some("⏚")
85 && c.points().first().map(id_key).as_deref() == Some(id_key(&p.id).as_str())
86 });
87 let mut marks = String::new();
88 if p.external_reference {
89 marks.push_str(" \u{26D3}"); }
91 if p.construction {
92 marks.push_str(" ◐");
93 }
94 if grounded {
95 marks.push_str(" ⏚");
96 }
97 SketchEntityRow {
98 kind: "point",
99 id: p.id.clone(),
100 label: format!("P{} ({:.1}, {:.1}){marks}", id_key(&p.id), p.x, p.y),
101 selected: Self::sketch_ref_selected(session, "point", &p.id),
102 construction: p.construction,
103 conflicting: false,
104 }
105 })
106 .collect()
107 }
108
109 pub fn sketch_geometry_rows(&self) -> Vec<SketchEntityRow> {
112 use crate::sketch::doc::id_key;
113 let Some(edit) = self.sketch_edit.as_ref() else {
114 return Vec::new();
115 };
116 let session = &edit.session;
117 session
118 .doc
119 .geometries
120 .iter()
121 .map(|g| {
122 let pts = g
123 .points
124 .iter()
125 .map(id_key)
126 .collect::<Vec<_>>()
127 .join(",");
128 let construction = g.construction();
129 let mark = if construction { " ◐" } else { "" };
130 SketchEntityRow {
131 kind: "geometry",
132 id: g.id.clone(),
133 label: format!("{}:{}{mark} [{pts}]", g.geom_type, id_key(&g.id)),
134 selected: Self::sketch_ref_selected(session, "geometry", &g.id),
135 construction,
136 conflicting: false,
137 }
138 })
139 .collect()
140 }
141
142 pub fn sketch_constraint_rows(&self) -> Vec<SketchEntityRow> {
145 use crate::sketch::doc::id_key;
146 let Some(edit) = self.sketch_edit.as_ref() else {
147 return Vec::new();
148 };
149 let session = &edit.session;
150 session
151 .doc
152 .constraints
153 .iter()
154 .filter_map(|c| {
155 let id = c.raw.get("id")?.clone();
156 let ctype = c.ctype().unwrap_or("?");
157 let value = c
158 .raw
159 .get("value")
160 .and_then(serde_json::Value::as_f64)
161 .map(|v| format!(" {v:.3}"))
162 .unwrap_or_default();
163 let pts = c
164 .points()
165 .iter()
166 .map(id_key)
167 .collect::<Vec<_>>()
168 .join(",");
169 Some(SketchEntityRow {
170 kind: "constraint",
171 id: id.clone(),
172 label: format!("{} {ctype}{value} [{pts}]", id_key(&id)),
173 selected: Self::sketch_ref_selected(session, "constraint", &id),
174 construction: false,
175 conflicting: session.diagnostics.constraint_conflicting(&id),
176 })
177 })
178 .collect()
179 }
180
181 pub fn sketch_select_entity(&mut self, kind: &str, id: serde_json::Value, additive: bool) {
186 let toggles = self.settings.multi_select == crate::style::MultiSelectMode::ClickToggles;
187 let entity_ref = serde_json::json!({ "kind": kind, "id": id });
188 let Some(edit) = self.sketch_edit.as_mut() else {
189 return;
190 };
191 if !additive && !toggles {
192 edit.session.clear_selection();
193 }
194 edit.session.toggle_selection(entity_ref);
195 self.refresh_sketch_overlay();
196 self.dirty = true;
197 }
198
199 pub fn sketch_hover_entity(&mut self, kind: &str, id: serde_json::Value) {
203 if self.sketch_edit.is_none() {
204 return;
205 }
206 let entity_ref = serde_json::json!({ "kind": kind, "id": id });
209 self.set_sketch_hover(Some(entity_ref));
210 self.sketch_list_hover_active = true;
211 }
212
213 pub fn take_sketch_list_hover(&mut self) -> bool {
217 std::mem::take(&mut self.sketch_list_hover_active)
218 }
219
220 pub fn sketch_solver_settings(&self) -> Option<crate::sketch::SketchSolverSettings> {
223 self.sketch_edit
224 .as_ref()
225 .map(|edit| edit.session.solver_settings.clone())
226 }
227
228 pub fn sketch_set_solver_settings(&mut self, settings: crate::sketch::SketchSolverSettings) {
231 if let Some(edit) = self.sketch_edit.as_mut() {
232 edit.session.solver_settings = settings;
233 } else {
234 return;
235 }
236 self.resolve_active_sketch("solver settings");
237 self.refresh_sketch_overlay();
238 self.dirty = true;
239 }
240}
241
242#[derive(Clone, Debug, PartialEq)]
246pub struct SketchConstraintAction {
247 pub symbol: String,
250 pub label: String,
252 pub dimensional: bool,
255}
256
257impl EngineState {
258 pub fn sketch_applicable_constraints(&self) -> Vec<SketchConstraintAction> {
268 match self.sketch_edit.as_ref() {
269 Some(edit) => sketch_palette_for(&edit.session),
270 None => Vec::new(),
271 }
272 }
273
274 pub fn sketch_add_constraint(&mut self, symbol: &str) -> bool {
279 let added = match self.sketch_edit.as_mut() {
282 Some(edit) => {
283 edit.record_undo();
284 sketch_build_and_add_constraint(&mut edit.session, symbol)
285 }
286 None => return false,
287 };
288 if added {
289 self.resolve_active_sketch("add-constraint");
290 self.refresh_sketch_overlay();
291 self.dirty = true;
292 } else if let Some(edit) = self.sketch_edit.as_mut() {
293 edit.undo_stack.pop();
294 }
295 added
296 }
297
298 pub fn sketch_toggle_ground(&mut self) -> bool {
304 use crate::sketch::doc::id_key;
305 use std::collections::HashSet;
306
307 let Some(edit) = self.sketch_edit.as_mut() else {
308 return false;
309 };
310 let sel_ids: Vec<serde_json::Value> = edit
311 .session
312 .selection
313 .iter()
314 .filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("point"))
315 .filter_map(|r| r.get("id").cloned())
316 .collect();
317 if sel_ids.is_empty() {
318 return false;
319 }
320 edit.record_undo();
322 let doc = &mut edit.session.doc;
323 let has_ground = |doc: &crate::sketch::SketchDoc, id: &serde_json::Value| -> bool {
324 doc.constraints.iter().any(|c| {
325 c.ctype() == Some("⏚")
326 && c.points().first().map(id_key) == Some(id_key(id))
327 })
328 };
329 let all_grounded = sel_ids.iter().all(|id| has_ground(doc, id));
330 if all_grounded {
331 let sel_keys: HashSet<String> = sel_ids.iter().map(id_key).collect();
332 doc.constraints.retain(|c| {
333 if c.ctype() != Some("⏚") {
334 return true;
335 }
336 match c.points().first() {
337 Some(p) => !sel_keys.contains(&id_key(p)),
338 None => true,
339 }
340 });
341 for id in &sel_ids {
342 if let Some(p) = doc.point_mut(id) {
343 p.fixed = false;
344 }
345 }
346 } else {
347 for id in &sel_ids {
348 if has_ground(doc, id) {
349 continue;
350 }
351 let cid = doc.next_constraint_id();
352 let mut raw = serde_json::Map::new();
353 raw.insert("id".to_string(), cid);
354 raw.insert("type".to_string(), serde_json::Value::String("⏚".to_string()));
355 raw.insert(
356 "points".to_string(),
357 serde_json::Value::Array(vec![id.clone()]),
358 );
359 doc.constraints.push(crate::sketch::SketchConstraint { raw });
360 if let Some(p) = doc.point_mut(id) {
361 p.fixed = true;
362 }
363 }
364 }
365 self.resolve_active_sketch("toggle-ground");
366 self.refresh_sketch_overlay();
367 self.dirty = true;
368 true
369 }
370
371 pub fn sketch_toggle_construction(&mut self) -> bool {
376 let Some(edit) = self.sketch_edit.as_mut() else {
377 return false;
378 };
379 let mut pt_ids: Vec<serde_json::Value> = Vec::new();
380 let mut geo_ids: Vec<serde_json::Value> = Vec::new();
381 for r in &edit.session.selection {
382 match r.get("kind").and_then(|v| v.as_str()) {
383 Some("point") => {
384 if let Some(id) = r.get("id") {
385 pt_ids.push(id.clone());
386 }
387 }
388 Some("geometry") => {
389 if let Some(id) = r.get("id") {
390 geo_ids.push(id.clone());
391 }
392 }
393 _ => {}
394 }
395 }
396 if pt_ids.is_empty() && geo_ids.is_empty() {
397 return false;
398 }
399 edit.record_undo();
401 let doc = &mut edit.session.doc;
402 let all_construction = pt_ids
403 .iter()
404 .all(|id| doc.point(id).map_or(false, |p| p.construction))
405 && geo_ids
406 .iter()
407 .all(|id| doc.geometry(id).map_or(false, |g| g.construction()));
408 let next = !all_construction;
409 for id in &pt_ids {
410 if let Some(p) = doc.point_mut(id) {
411 p.construction = next;
412 }
413 }
414 for id in &geo_ids {
415 if let Some(g) = doc.geometry_mut(id) {
416 g.extra
417 .insert("construction".to_string(), serde_json::Value::Bool(next));
418 }
419 }
420 self.resolve_active_sketch("toggle-construction");
421 self.refresh_sketch_overlay();
422 self.dirty = true;
423 true
424 }
425
426 pub fn sketch_cleanup_unused_points(&mut self) -> bool {
430 use crate::sketch::doc::id_key;
431 use std::collections::HashSet;
432
433 let Some(edit) = self.sketch_edit.as_mut() else {
434 return false;
435 };
436 edit.record_undo();
438 let doc = &mut edit.session.doc;
439 let mut used: HashSet<String> = HashSet::new();
440 for g in &doc.geometries {
441 for pid in &g.points {
442 used.insert(id_key(pid));
443 }
444 }
445 for c in &doc.constraints {
446 for pid in c.points() {
447 used.insert(id_key(pid));
448 }
449 }
450 let before = doc.points.len();
451 doc.points.retain(|p| used.contains(&id_key(&p.id)));
452 let removed = doc.points.len() != before;
453 if !removed {
454 edit.undo_stack.pop();
455 return false;
456 }
457 self.resolve_active_sketch("cleanup");
458 self.refresh_sketch_overlay();
459 self.dirty = true;
460 true
461 }
462
463 pub fn sketch_constraint_count(&self) -> usize {
466 self.sketch_edit
467 .as_ref()
468 .map_or(0, |edit| edit.session.doc.constraints.len())
469 }
470
471 pub fn sketch_selection_all_grounded(&self) -> Option<bool> {
475 use crate::sketch::doc::id_key;
476 let edit = self.sketch_edit.as_ref()?;
477 let doc = &edit.session.doc;
478 let sel: Vec<&serde_json::Value> = edit
479 .session
480 .selection
481 .iter()
482 .filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("point"))
483 .filter_map(|r| r.get("id"))
484 .collect();
485 if sel.is_empty() {
486 return None;
487 }
488 let all = sel.iter().all(|id| {
489 doc.constraints.iter().any(|c| {
490 c.ctype() == Some("⏚")
491 && c.points().first().map(id_key) == Some(id_key(id))
492 })
493 });
494 Some(all)
495 }
496
497 pub fn sketch_selection_all_construction(&self) -> Option<bool> {
501 let edit = self.sketch_edit.as_ref()?;
502 let doc = &edit.session.doc;
503 let mut any = false;
504 let mut all = true;
505 for r in &edit.session.selection {
506 match r.get("kind").and_then(|v| v.as_str()) {
507 Some("point") => {
508 any = true;
509 if let Some(id) = r.get("id") {
510 if !doc.point(id).map_or(false, |p| p.construction) {
511 all = false;
512 }
513 }
514 }
515 Some("geometry") => {
516 any = true;
517 if let Some(id) = r.get("id") {
518 if !doc.geometry(id).map_or(false, |g| g.construction()) {
519 all = false;
520 }
521 }
522 }
523 _ => {}
524 }
525 }
526 if !any {
527 return None;
528 }
529 Some(all)
530 }
531}
532
533fn sketch_palette_for(session: &crate::sketch::SketchSession) -> Vec<SketchConstraintAction> {
537 use crate::sketch::doc::id_key;
538 use std::collections::HashSet;
539
540 let doc = &session.doc;
541 let mut sel_points: Vec<serde_json::Value> = Vec::new();
542 let mut geos: Vec<&crate::sketch::SketchGeometry> = Vec::new();
543 for r in &session.selection {
544 match r.get("kind").and_then(|v| v.as_str()) {
545 Some("point") => {
546 if let Some(id) = r.get("id") {
547 sel_points.push(id.clone());
548 }
549 }
550 Some("geometry") => {
551 if let Some(id) = r.get("id") {
552 if let Some(g) = doc.geometry(id) {
553 geos.push(g);
554 }
555 }
556 }
557 _ => {}
558 }
559 }
560
561 let mut point_set: HashSet<String> = HashSet::new();
564 for id in &sel_points {
565 point_set.insert(id_key(id));
566 }
567 for g in &geos {
568 let pts: &[serde_json::Value] = if g.geom_type == "arc" {
569 &g.points[..g.points.len().min(2)]
570 } else {
571 &g.points
572 };
573 for pid in pts {
574 point_set.insert(id_key(pid));
575 }
576 }
577 let point_count = point_set.len();
578 let selected_point_ids_len = sel_points.len();
579 let is_radial =
580 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
581
582 let mut out: Vec<SketchConstraintAction> = Vec::new();
583 let mut push = |symbol: &str, label: &str, dimensional: bool| {
584 out.push(SketchConstraintAction {
585 symbol: symbol.to_string(),
586 label: label.to_string(),
587 dimensional,
588 });
589 };
590
591 if geos.len() == 1 && is_radial(geos[0]) {
593 push("R", "Radius", true);
594 push("⌀", "Diameter", true);
595 return out;
596 }
597 if geos.len() == 2 && geos.iter().all(|g| g.geom_type == "line") {
599 push("∥", "Parallel", false);
600 push("⟂", "Perpendicular", false);
601 push("∠", "Angle", true);
602 push("⇌", "Equal distance", false);
603 push("⋰", "Collinear", false);
604 push("⏛", "Point on line", false);
605 return out;
606 }
607 if geos.len() == 2 && geos.iter().all(|g| is_radial(g)) {
609 push("⊜", "Equal radius", false);
610 push("◎", "Concentric", false);
611 push("⌒", "Tangent", false);
612 return out;
613 }
614 if geos.len() == 2
616 && ((geos[0].geom_type == "line" && is_radial(geos[1]))
617 || (geos[1].geom_type == "line" && is_radial(geos[0])))
618 {
619 push("⌒", "Tangent", false);
620 return out;
621 }
622 if geos.len() == 1 && geos[0].geom_type == "line" && selected_point_ids_len == 2 {
624 push("⟂", "Perpendicular", false);
625 push("⋈", "Symmetric about line", false);
626 }
627
628 if point_count == 1 {
629 push("⏚", "Ground (fix point)", false);
630 }
631 if point_count == 2 {
632 push("━", "Horizontal", false);
633 push("│", "Vertical", false);
634 push("≡", "Coincident", false);
635 push("⟺", "Distance", true);
636 }
637 if point_count == 3 {
638 push("⋯", "Midpoint", false);
639 push("⏛", "Point on line", false);
640 push("↥", "Line to point distance", true);
641 push("∠", "Angle", true);
642 if selected_point_ids_len >= 3 {
644 push("⋰", "Collinear", false);
645 }
646 }
647 if point_count > 3 && geos.is_empty() && selected_point_ids_len >= 3 {
649 push("⋰", "Collinear", false);
650 }
651
652 out
653}
654
655fn sketch_build_tangent_points(
659 geos: &[&crate::sketch::SketchGeometry],
660) -> Option<Vec<serde_json::Value>> {
661 if geos.len() != 2 {
662 return None;
663 }
664 let is_radial = |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
665 let line = geos.iter().find(|g| g.geom_type == "line");
666 let circ = geos.iter().find(|g| is_radial(g));
667 if let (Some(line), Some(circ)) = (line, circ) {
668 if line.points.len() < 2 || circ.points.len() < 2 {
669 return None;
670 }
671 return Some(vec![
672 line.points[0].clone(),
673 line.points[1].clone(),
674 circ.points[0].clone(),
675 circ.points[1].clone(),
676 ]);
677 }
678 if geos.iter().all(|g| is_radial(g)) {
679 let (a, b) = (geos[0], geos[1]);
680 if a.points.len() < 2 || b.points.len() < 2 {
681 return None;
682 }
683 return Some(vec![
684 a.points[0].clone(),
685 a.points[1].clone(),
686 b.points[0].clone(),
687 b.points[1].clone(),
688 ]);
689 }
690 None
691}
692
693pub(super) fn sketch_perpendicular_should_swap(
697 doc: &crate::sketch::SketchDoc,
698 pts: &[serde_json::Value],
699) -> bool {
700 let coord = |v: &serde_json::Value| doc.point(v).map(|p| (p.x, p.y));
701 let (Some(p0), Some(p1), Some(p2), Some(p3)) =
702 (coord(&pts[0]), coord(&pts[1]), coord(&pts[2]), coord(&pts[3]))
703 else {
704 return false;
705 };
706 let angle = |a: (f64, f64), b: (f64, f64)| -> f64 {
708 let deg = (b.1 - a.1).atan2(b.0 - a.0) * 180.0 / std::f64::consts::PI;
709 (deg + 360.0) % 360.0
710 };
711 let fold = |a: f64| (a + 180.0) % 360.0 - 180.0;
713 let line1_a = fold(angle(p0, p1));
714 let line1_b = fold(angle(p1, p0));
715 let line2 = fold(angle(p2, p3));
716 let diff_a = line1_a - line2;
717 let diff_b = line1_b - line2;
718 (90.0 - diff_a).abs() > (90.0 - diff_b).abs()
719}
720
721pub(super) fn sketch_constraint_signature(ctype: &str, points: &[serde_json::Value]) -> String {
724 use crate::sketch::doc::id_key;
725 let mut keys: Vec<String> = points.iter().map(id_key).collect();
726 keys.sort();
727 format!("{ctype}|{}", keys.join(","))
728}
729
730fn sketch_build_and_add_constraint(
735 session: &mut crate::sketch::SketchSession,
736 symbol: &str,
737) -> bool {
738 struct Built {
742 store_type: String,
743 display_style: &'static str,
744 lists: Vec<Vec<serde_json::Value>>,
745 }
746
747 let built: Option<Built> = {
749 let doc = &session.doc;
750 let mut selected: Vec<serde_json::Value> = Vec::new();
755 let mut geo_items: Vec<&crate::sketch::SketchGeometry> = Vec::new();
756 let mut point_items: Vec<serde_json::Value> = Vec::new();
757 let mut geometry_type: Option<String> = None;
758 let mut first_kind: Option<String> = None;
759 let mut has_geometry = false;
760 for (i, r) in session.selection.iter().enumerate() {
761 let kind = r.get("kind").and_then(|v| v.as_str());
762 let Some(id) = r.get("id") else { continue };
763 if i == 0 {
764 first_kind = kind.map(|k| k.to_string());
765 }
766 match kind {
767 Some("point") => {
768 if let Some(p) = doc.point(id) {
769 selected.push(p.id.clone());
770 point_items.push(p.id.clone());
771 }
772 }
773 Some("geometry") => {
774 if let Some(g) = doc.geometry(id) {
775 for pid in &g.points {
776 if doc.point(pid).is_some() {
777 selected.push(pid.clone());
778 }
779 }
780 if g.geom_type == "arc" {
781 selected.pop();
782 }
783 geometry_type = Some(g.geom_type.clone());
784 geo_items.push(g);
785 has_geometry = true;
786 }
787 }
788 _ => {}
789 }
790 }
791 if selected.is_empty() {
792 None
793 } else {
794 let radial = |g: &crate::sketch::SketchGeometry| {
795 g.geom_type == "arc" || g.geom_type == "circle"
796 };
797 let simple = |t: &str, list: Vec<serde_json::Value>, ds: &'static str| Built {
798 store_type: t.to_string(),
799 display_style: ds,
800 lists: vec![list],
801 };
802
803 match symbol {
805 "◎" => {
806 if geo_items.len() == 2 && geo_items.iter().all(|g| radial(g)) {
807 Some(simple(
808 "◎",
809 vec![geo_items[0].points[0].clone(), geo_items[1].points[0].clone()],
810 "",
811 ))
812 } else {
813 None
814 }
815 }
816 "⊜" => {
817 if geo_items.len() == 2 && geo_items.iter().all(|g| radial(g)) {
818 let (g0, g1) = (geo_items[0], geo_items[1]);
819 Some(simple(
820 "⊜",
821 vec![
822 g0.points[0].clone(),
823 g0.points[1].clone(),
824 g1.points[0].clone(),
825 g1.points[1].clone(),
826 ],
827 "",
828 ))
829 } else {
830 None
831 }
832 }
833 "⌒" => sketch_build_tangent_points(&geo_items).map(|pts| simple("⌒", pts, "")),
834 "⋰" => {
835 let ids = if geo_items.len() >= 2
836 && geo_items.iter().all(|g| g.geom_type == "line")
837 {
838 let mut v = Vec::new();
839 for g in &geo_items {
840 v.push(g.points[0].clone());
841 v.push(g.points[1].clone());
842 }
843 Some(v)
844 } else if point_items.len() >= 3 {
845 Some(point_items.clone())
846 } else {
847 None
848 };
849 ids.filter(|v| v.len() >= 3).map(|v| simple("⋰", v, ""))
850 }
851 "⋈" => geo_items
852 .iter()
853 .find(|g| g.geom_type == "line")
854 .filter(|line| line.points.len() >= 2 && point_items.len() == 2)
855 .map(|line| {
856 simple(
857 "⋈",
858 vec![
859 line.points[0].clone(),
860 line.points[1].clone(),
861 point_items[0].clone(),
862 point_items[1].clone(),
863 ],
864 "",
865 )
866 }),
867 "R" | "⌀" => {
870 if selected.len() == 2 {
871 let ds = if symbol == "⌀" { "diameter" } else { "radius" };
872 Some(Built {
873 store_type: "⟺".to_string(),
874 display_style: ds,
875 lists: vec![selected.clone()],
876 })
877 } else {
878 None
879 }
880 }
881 _ => {
882 match selected.len() {
884 1 => match symbol {
885 "⏚" => Some(simple("⏚", selected.clone(), "")),
886 _ => None,
887 },
888 2 => match symbol {
889 "━" | "│" | "≡" => Some(simple(symbol, selected.clone(), "")),
890 "⟺" => {
891 let ds = if matches!(
892 geometry_type.as_deref(),
893 Some("arc") | Some("circle")
894 ) {
895 "radius"
896 } else {
897 ""
898 };
899 Some(Built {
900 store_type: "⟺".to_string(),
901 display_style: ds,
902 lists: vec![selected.clone()],
903 })
904 }
905 _ => None,
906 },
907 3 => match symbol {
908 "⏛" => Some(simple("⏛", selected.clone(), "")),
909 "⋯" => {
910 let mut pts = selected.clone();
911 if has_geometry && first_kind.as_deref() == Some("point") {
912 pts.reverse();
913 }
914 Some(simple("⋯", pts, ""))
915 }
916 "↥" => {
917 if geo_items.len() == 1 && point_items.len() == 1 {
918 let line = geo_items[0];
919 if line.geom_type == "line" && line.points.len() >= 2 {
920 Some(simple(
921 "↥",
922 vec![
923 line.points[0].clone(),
924 line.points[1].clone(),
925 point_items[0].clone(),
926 ],
927 "",
928 ))
929 } else {
930 None
931 }
932 } else {
933 Some(simple("↥", selected.clone(), ""))
936 }
937 }
938 "⇌" => Some(simple("⇌", selected.clone(), "")),
939 _ => None,
940 },
941 4 | 5 => match symbol {
942 "⏛" => {
943 if geo_items.len() == 2
946 && geo_items.iter().all(|g| g.geom_type == "line")
947 && geo_items[0].points.len() >= 2
948 && geo_items[1].points.len() >= 2
949 {
950 let (g0, g1) = (geo_items[0], geo_items[1]);
951 Some(Built {
952 store_type: "⏛".to_string(),
953 display_style: "",
954 lists: vec![
955 vec![
956 g0.points[0].clone(),
957 g0.points[1].clone(),
958 g1.points[0].clone(),
959 ],
960 vec![
961 g0.points[0].clone(),
962 g0.points[1].clone(),
963 g1.points[1].clone(),
964 ],
965 ],
966 })
967 } else {
968 None
969 }
970 }
971 "⟂" => {
972 if selected.len() != 4 {
973 None
974 } else {
975 let mut pts = selected.clone();
976 if sketch_perpendicular_should_swap(doc, &pts) {
977 pts.swap(0, 1);
978 }
979 Some(simple("⟂", pts, ""))
980 }
981 }
982 "∥" => Some(simple("∥", selected.clone(), "")),
983 "∠" => Some(simple("∠", selected.clone(), "")),
984 "⇌" => Some(simple("⇌", selected.clone(), "")),
985 _ => None,
986 },
987 _ => None,
988 }
989 }
990 }
991 }
992 };
993
994 let Some(built) = built else {
995 return false;
996 };
997
998 let doc = &mut session.doc;
1001 let mut added_any = false;
1002 for pts in &built.lists {
1003 let sig = sketch_constraint_signature(&built.store_type, pts);
1004 let duplicate = doc.constraints.iter().any(|c| match c.ctype() {
1005 Some(t) => sketch_constraint_signature(t, c.points()) == sig,
1006 None => false,
1007 });
1008 if duplicate {
1009 continue;
1010 }
1011 let id = doc.next_constraint_id();
1012 let mut raw = serde_json::Map::new();
1013 raw.insert("id".to_string(), id);
1014 raw.insert(
1015 "type".to_string(),
1016 serde_json::Value::String(built.store_type.clone()),
1017 );
1018 raw.insert("points".to_string(), serde_json::Value::Array(pts.clone()));
1019 raw.insert("labelX".to_string(), serde_json::Value::from(0));
1020 raw.insert("labelY".to_string(), serde_json::Value::from(0));
1021 raw.insert(
1022 "displayStyle".to_string(),
1023 serde_json::Value::String(built.display_style.to_string()),
1024 );
1025 raw.insert("value".to_string(), serde_json::Value::Null);
1026 raw.insert("valueNeedsSetup".to_string(), serde_json::Value::Bool(true));
1027 doc.constraints.push(crate::sketch::SketchConstraint { raw });
1028 added_any = true;
1029 }
1030 added_any
1031}
1032
1033