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