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) {
183 let entity_ref = serde_json::json!({ "kind": kind, "id": id });
184 let Some(edit) = self.sketch_edit.as_mut() else {
185 return;
186 };
187 if !additive {
188 edit.session.clear_selection();
189 }
190 edit.session.toggle_selection(entity_ref);
191 self.refresh_sketch_overlay();
192 self.dirty = true;
193 }
194
195 pub fn sketch_hover_entity(&mut self, kind: &str, id: serde_json::Value) {
199 if self.sketch_edit.is_none() {
200 return;
201 }
202 let entity_ref = serde_json::json!({ "kind": kind, "id": id });
205 self.set_sketch_hover(Some(entity_ref));
206 self.sketch_list_hover_active = true;
207 }
208
209 pub fn take_sketch_list_hover(&mut self) -> bool {
213 std::mem::take(&mut self.sketch_list_hover_active)
214 }
215
216 pub fn sketch_solver_settings(&self) -> Option<crate::sketch::SketchSolverSettings> {
219 self.sketch_edit
220 .as_ref()
221 .map(|edit| edit.session.solver_settings.clone())
222 }
223
224 pub fn sketch_set_solver_settings(&mut self, settings: crate::sketch::SketchSolverSettings) {
227 if let Some(edit) = self.sketch_edit.as_mut() {
228 edit.session.solver_settings = settings;
229 } else {
230 return;
231 }
232 self.resolve_active_sketch("solver settings");
233 self.refresh_sketch_overlay();
234 self.dirty = true;
235 }
236}
237
238#[derive(Clone, Debug, PartialEq)]
242pub struct SketchConstraintAction {
243 pub symbol: String,
246 pub label: String,
248 pub dimensional: bool,
251}
252
253impl EngineState {
254 pub fn sketch_applicable_constraints(&self) -> Vec<SketchConstraintAction> {
264 match self.sketch_edit.as_ref() {
265 Some(edit) => sketch_palette_for(&edit.session),
266 None => Vec::new(),
267 }
268 }
269
270 pub fn sketch_add_constraint(&mut self, symbol: &str) -> bool {
275 let added = match self.sketch_edit.as_mut() {
278 Some(edit) => {
279 edit.record_undo();
280 sketch_build_and_add_constraint(&mut edit.session, symbol)
281 }
282 None => return false,
283 };
284 if added {
285 self.resolve_active_sketch("add-constraint");
286 self.refresh_sketch_overlay();
287 self.dirty = true;
288 } else if let Some(edit) = self.sketch_edit.as_mut() {
289 edit.undo_stack.pop();
290 }
291 added
292 }
293
294 pub fn sketch_toggle_ground(&mut self) -> bool {
300 use crate::sketch::doc::id_key;
301 use std::collections::HashSet;
302
303 let Some(edit) = self.sketch_edit.as_mut() else {
304 return false;
305 };
306 let sel_ids: Vec<serde_json::Value> = edit
307 .session
308 .selection
309 .iter()
310 .filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("point"))
311 .filter_map(|r| r.get("id").cloned())
312 .collect();
313 if sel_ids.is_empty() {
314 return false;
315 }
316 edit.record_undo();
318 let doc = &mut edit.session.doc;
319 let has_ground = |doc: &crate::sketch::SketchDoc, id: &serde_json::Value| -> bool {
320 doc.constraints.iter().any(|c| {
321 c.ctype() == Some("⏚")
322 && c.points().first().map(id_key) == Some(id_key(id))
323 })
324 };
325 let all_grounded = sel_ids.iter().all(|id| has_ground(doc, id));
326 if all_grounded {
327 let sel_keys: HashSet<String> = sel_ids.iter().map(id_key).collect();
328 doc.constraints.retain(|c| {
329 if c.ctype() != Some("⏚") {
330 return true;
331 }
332 match c.points().first() {
333 Some(p) => !sel_keys.contains(&id_key(p)),
334 None => true,
335 }
336 });
337 for id in &sel_ids {
338 if let Some(p) = doc.point_mut(id) {
339 p.fixed = false;
340 }
341 }
342 } else {
343 for id in &sel_ids {
344 if has_ground(doc, id) {
345 continue;
346 }
347 let cid = doc.next_constraint_id();
348 let mut raw = serde_json::Map::new();
349 raw.insert("id".to_string(), cid);
350 raw.insert("type".to_string(), serde_json::Value::String("⏚".to_string()));
351 raw.insert(
352 "points".to_string(),
353 serde_json::Value::Array(vec![id.clone()]),
354 );
355 doc.constraints.push(crate::sketch::SketchConstraint { raw });
356 if let Some(p) = doc.point_mut(id) {
357 p.fixed = true;
358 }
359 }
360 }
361 self.resolve_active_sketch("toggle-ground");
362 self.refresh_sketch_overlay();
363 self.dirty = true;
364 true
365 }
366
367 pub fn sketch_toggle_construction(&mut self) -> bool {
372 let Some(edit) = self.sketch_edit.as_mut() else {
373 return false;
374 };
375 let mut pt_ids: Vec<serde_json::Value> = Vec::new();
376 let mut geo_ids: Vec<serde_json::Value> = Vec::new();
377 for r in &edit.session.selection {
378 match r.get("kind").and_then(|v| v.as_str()) {
379 Some("point") => {
380 if let Some(id) = r.get("id") {
381 pt_ids.push(id.clone());
382 }
383 }
384 Some("geometry") => {
385 if let Some(id) = r.get("id") {
386 geo_ids.push(id.clone());
387 }
388 }
389 _ => {}
390 }
391 }
392 if pt_ids.is_empty() && geo_ids.is_empty() {
393 return false;
394 }
395 edit.record_undo();
397 let doc = &mut edit.session.doc;
398 let all_construction = pt_ids
399 .iter()
400 .all(|id| doc.point(id).map_or(false, |p| p.construction))
401 && geo_ids
402 .iter()
403 .all(|id| doc.geometry(id).map_or(false, |g| g.construction()));
404 let next = !all_construction;
405 for id in &pt_ids {
406 if let Some(p) = doc.point_mut(id) {
407 p.construction = next;
408 }
409 }
410 for id in &geo_ids {
411 if let Some(g) = doc.geometry_mut(id) {
412 g.extra
413 .insert("construction".to_string(), serde_json::Value::Bool(next));
414 }
415 }
416 self.resolve_active_sketch("toggle-construction");
417 self.refresh_sketch_overlay();
418 self.dirty = true;
419 true
420 }
421
422 pub fn sketch_cleanup_unused_points(&mut self) -> bool {
426 use crate::sketch::doc::id_key;
427 use std::collections::HashSet;
428
429 let Some(edit) = self.sketch_edit.as_mut() else {
430 return false;
431 };
432 edit.record_undo();
434 let doc = &mut edit.session.doc;
435 let mut used: HashSet<String> = HashSet::new();
436 for g in &doc.geometries {
437 for pid in &g.points {
438 used.insert(id_key(pid));
439 }
440 }
441 for c in &doc.constraints {
442 for pid in c.points() {
443 used.insert(id_key(pid));
444 }
445 }
446 let before = doc.points.len();
447 doc.points.retain(|p| used.contains(&id_key(&p.id)));
448 let removed = doc.points.len() != before;
449 if !removed {
450 edit.undo_stack.pop();
451 return false;
452 }
453 self.resolve_active_sketch("cleanup");
454 self.refresh_sketch_overlay();
455 self.dirty = true;
456 true
457 }
458
459 pub fn sketch_constraint_count(&self) -> usize {
462 self.sketch_edit
463 .as_ref()
464 .map_or(0, |edit| edit.session.doc.constraints.len())
465 }
466
467 pub fn sketch_selection_all_grounded(&self) -> Option<bool> {
471 use crate::sketch::doc::id_key;
472 let edit = self.sketch_edit.as_ref()?;
473 let doc = &edit.session.doc;
474 let sel: Vec<&serde_json::Value> = edit
475 .session
476 .selection
477 .iter()
478 .filter(|r| r.get("kind").and_then(|v| v.as_str()) == Some("point"))
479 .filter_map(|r| r.get("id"))
480 .collect();
481 if sel.is_empty() {
482 return None;
483 }
484 let all = sel.iter().all(|id| {
485 doc.constraints.iter().any(|c| {
486 c.ctype() == Some("⏚")
487 && c.points().first().map(id_key) == Some(id_key(id))
488 })
489 });
490 Some(all)
491 }
492
493 pub fn sketch_selection_all_construction(&self) -> Option<bool> {
497 let edit = self.sketch_edit.as_ref()?;
498 let doc = &edit.session.doc;
499 let mut any = false;
500 let mut all = true;
501 for r in &edit.session.selection {
502 match r.get("kind").and_then(|v| v.as_str()) {
503 Some("point") => {
504 any = true;
505 if let Some(id) = r.get("id") {
506 if !doc.point(id).map_or(false, |p| p.construction) {
507 all = false;
508 }
509 }
510 }
511 Some("geometry") => {
512 any = true;
513 if let Some(id) = r.get("id") {
514 if !doc.geometry(id).map_or(false, |g| g.construction()) {
515 all = false;
516 }
517 }
518 }
519 _ => {}
520 }
521 }
522 if !any {
523 return None;
524 }
525 Some(all)
526 }
527}
528
529fn sketch_palette_for(session: &crate::sketch::SketchSession) -> Vec<SketchConstraintAction> {
533 use crate::sketch::doc::id_key;
534 use std::collections::HashSet;
535
536 let doc = &session.doc;
537 let mut sel_points: Vec<serde_json::Value> = Vec::new();
538 let mut geos: Vec<&crate::sketch::SketchGeometry> = Vec::new();
539 for r in &session.selection {
540 match r.get("kind").and_then(|v| v.as_str()) {
541 Some("point") => {
542 if let Some(id) = r.get("id") {
543 sel_points.push(id.clone());
544 }
545 }
546 Some("geometry") => {
547 if let Some(id) = r.get("id") {
548 if let Some(g) = doc.geometry(id) {
549 geos.push(g);
550 }
551 }
552 }
553 _ => {}
554 }
555 }
556
557 let mut point_set: HashSet<String> = HashSet::new();
560 for id in &sel_points {
561 point_set.insert(id_key(id));
562 }
563 for g in &geos {
564 let pts: &[serde_json::Value] = if g.geom_type == "arc" {
565 &g.points[..g.points.len().min(2)]
566 } else {
567 &g.points
568 };
569 for pid in pts {
570 point_set.insert(id_key(pid));
571 }
572 }
573 let point_count = point_set.len();
574 let selected_point_ids_len = sel_points.len();
575 let is_radial =
576 |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
577
578 let mut out: Vec<SketchConstraintAction> = Vec::new();
579 let mut push = |symbol: &str, label: &str, dimensional: bool| {
580 out.push(SketchConstraintAction {
581 symbol: symbol.to_string(),
582 label: label.to_string(),
583 dimensional,
584 });
585 };
586
587 if geos.len() == 1 && is_radial(geos[0]) {
589 push("R", "Radius", true);
590 push("⌀", "Diameter", true);
591 return out;
592 }
593 if geos.len() == 2 && geos.iter().all(|g| g.geom_type == "line") {
595 push("∥", "Parallel", false);
596 push("⟂", "Perpendicular", false);
597 push("∠", "Angle", true);
598 push("⇌", "Equal distance", false);
599 push("⋰", "Collinear", false);
600 push("⏛", "Point on line", false);
601 return out;
602 }
603 if geos.len() == 2 && geos.iter().all(|g| is_radial(g)) {
605 push("⊜", "Equal radius", false);
606 push("◎", "Concentric", false);
607 push("⌒", "Tangent", false);
608 return out;
609 }
610 if geos.len() == 2
612 && ((geos[0].geom_type == "line" && is_radial(geos[1]))
613 || (geos[1].geom_type == "line" && is_radial(geos[0])))
614 {
615 push("⌒", "Tangent", false);
616 return out;
617 }
618 if geos.len() == 1 && geos[0].geom_type == "line" && selected_point_ids_len == 2 {
620 push("⟂", "Perpendicular", false);
621 push("⋈", "Symmetric about line", false);
622 }
623
624 if point_count == 1 {
625 push("⏚", "Ground (fix point)", false);
626 }
627 if point_count == 2 {
628 push("━", "Horizontal", false);
629 push("│", "Vertical", false);
630 push("≡", "Coincident", false);
631 push("⟺", "Distance", true);
632 }
633 if point_count == 3 {
634 push("⋯", "Midpoint", false);
635 push("⏛", "Point on line", false);
636 push("↥", "Line to point distance", true);
637 push("∠", "Angle", true);
638 if selected_point_ids_len >= 3 {
640 push("⋰", "Collinear", false);
641 }
642 }
643 if point_count > 3 && geos.is_empty() && selected_point_ids_len >= 3 {
645 push("⋰", "Collinear", false);
646 }
647
648 out
649}
650
651fn sketch_build_tangent_points(
655 geos: &[&crate::sketch::SketchGeometry],
656) -> Option<Vec<serde_json::Value>> {
657 if geos.len() != 2 {
658 return None;
659 }
660 let is_radial = |g: &crate::sketch::SketchGeometry| g.geom_type == "arc" || g.geom_type == "circle";
661 let line = geos.iter().find(|g| g.geom_type == "line");
662 let circ = geos.iter().find(|g| is_radial(g));
663 if let (Some(line), Some(circ)) = (line, circ) {
664 if line.points.len() < 2 || circ.points.len() < 2 {
665 return None;
666 }
667 return Some(vec![
668 line.points[0].clone(),
669 line.points[1].clone(),
670 circ.points[0].clone(),
671 circ.points[1].clone(),
672 ]);
673 }
674 if geos.iter().all(|g| is_radial(g)) {
675 let (a, b) = (geos[0], geos[1]);
676 if a.points.len() < 2 || b.points.len() < 2 {
677 return None;
678 }
679 return Some(vec![
680 a.points[0].clone(),
681 a.points[1].clone(),
682 b.points[0].clone(),
683 b.points[1].clone(),
684 ]);
685 }
686 None
687}
688
689pub(super) fn sketch_perpendicular_should_swap(
693 doc: &crate::sketch::SketchDoc,
694 pts: &[serde_json::Value],
695) -> bool {
696 let coord = |v: &serde_json::Value| doc.point(v).map(|p| (p.x, p.y));
697 let (Some(p0), Some(p1), Some(p2), Some(p3)) =
698 (coord(&pts[0]), coord(&pts[1]), coord(&pts[2]), coord(&pts[3]))
699 else {
700 return false;
701 };
702 let angle = |a: (f64, f64), b: (f64, f64)| -> f64 {
704 let deg = (b.1 - a.1).atan2(b.0 - a.0) * 180.0 / std::f64::consts::PI;
705 (deg + 360.0) % 360.0
706 };
707 let fold = |a: f64| (a + 180.0) % 360.0 - 180.0;
709 let line1_a = fold(angle(p0, p1));
710 let line1_b = fold(angle(p1, p0));
711 let line2 = fold(angle(p2, p3));
712 let diff_a = line1_a - line2;
713 let diff_b = line1_b - line2;
714 (90.0 - diff_a).abs() > (90.0 - diff_b).abs()
715}
716
717pub(super) fn sketch_constraint_signature(ctype: &str, points: &[serde_json::Value]) -> String {
720 use crate::sketch::doc::id_key;
721 let mut keys: Vec<String> = points.iter().map(id_key).collect();
722 keys.sort();
723 format!("{ctype}|{}", keys.join(","))
724}
725
726fn sketch_build_and_add_constraint(
731 session: &mut crate::sketch::SketchSession,
732 symbol: &str,
733) -> bool {
734 struct Built {
738 store_type: String,
739 display_style: &'static str,
740 lists: Vec<Vec<serde_json::Value>>,
741 }
742
743 let built: Option<Built> = {
745 let doc = &session.doc;
746 let mut selected: Vec<serde_json::Value> = Vec::new();
751 let mut geo_items: Vec<&crate::sketch::SketchGeometry> = Vec::new();
752 let mut point_items: Vec<serde_json::Value> = Vec::new();
753 let mut geometry_type: Option<String> = None;
754 let mut first_kind: Option<String> = None;
755 let mut has_geometry = false;
756 for (i, r) in session.selection.iter().enumerate() {
757 let kind = r.get("kind").and_then(|v| v.as_str());
758 let Some(id) = r.get("id") else { continue };
759 if i == 0 {
760 first_kind = kind.map(|k| k.to_string());
761 }
762 match kind {
763 Some("point") => {
764 if let Some(p) = doc.point(id) {
765 selected.push(p.id.clone());
766 point_items.push(p.id.clone());
767 }
768 }
769 Some("geometry") => {
770 if let Some(g) = doc.geometry(id) {
771 for pid in &g.points {
772 if doc.point(pid).is_some() {
773 selected.push(pid.clone());
774 }
775 }
776 if g.geom_type == "arc" {
777 selected.pop();
778 }
779 geometry_type = Some(g.geom_type.clone());
780 geo_items.push(g);
781 has_geometry = true;
782 }
783 }
784 _ => {}
785 }
786 }
787 if selected.is_empty() {
788 None
789 } else {
790 let radial = |g: &crate::sketch::SketchGeometry| {
791 g.geom_type == "arc" || g.geom_type == "circle"
792 };
793 let simple = |t: &str, list: Vec<serde_json::Value>, ds: &'static str| Built {
794 store_type: t.to_string(),
795 display_style: ds,
796 lists: vec![list],
797 };
798
799 match symbol {
801 "◎" => {
802 if geo_items.len() == 2 && geo_items.iter().all(|g| radial(g)) {
803 Some(simple(
804 "◎",
805 vec![geo_items[0].points[0].clone(), geo_items[1].points[0].clone()],
806 "",
807 ))
808 } else {
809 None
810 }
811 }
812 "⊜" => {
813 if geo_items.len() == 2 && geo_items.iter().all(|g| radial(g)) {
814 let (g0, g1) = (geo_items[0], geo_items[1]);
815 Some(simple(
816 "⊜",
817 vec![
818 g0.points[0].clone(),
819 g0.points[1].clone(),
820 g1.points[0].clone(),
821 g1.points[1].clone(),
822 ],
823 "",
824 ))
825 } else {
826 None
827 }
828 }
829 "⌒" => sketch_build_tangent_points(&geo_items).map(|pts| simple("⌒", pts, "")),
830 "⋰" => {
831 let ids = if geo_items.len() >= 2
832 && geo_items.iter().all(|g| g.geom_type == "line")
833 {
834 let mut v = Vec::new();
835 for g in &geo_items {
836 v.push(g.points[0].clone());
837 v.push(g.points[1].clone());
838 }
839 Some(v)
840 } else if point_items.len() >= 3 {
841 Some(point_items.clone())
842 } else {
843 None
844 };
845 ids.filter(|v| v.len() >= 3).map(|v| simple("⋰", v, ""))
846 }
847 "⋈" => geo_items
848 .iter()
849 .find(|g| g.geom_type == "line")
850 .filter(|line| line.points.len() >= 2 && point_items.len() == 2)
851 .map(|line| {
852 simple(
853 "⋈",
854 vec![
855 line.points[0].clone(),
856 line.points[1].clone(),
857 point_items[0].clone(),
858 point_items[1].clone(),
859 ],
860 "",
861 )
862 }),
863 "R" | "⌀" => {
866 if selected.len() == 2 {
867 let ds = if symbol == "⌀" { "diameter" } else { "radius" };
868 Some(Built {
869 store_type: "⟺".to_string(),
870 display_style: ds,
871 lists: vec![selected.clone()],
872 })
873 } else {
874 None
875 }
876 }
877 _ => {
878 match selected.len() {
880 1 => match symbol {
881 "⏚" => Some(simple("⏚", selected.clone(), "")),
882 _ => None,
883 },
884 2 => match symbol {
885 "━" | "│" | "≡" => Some(simple(symbol, selected.clone(), "")),
886 "⟺" => {
887 let ds = if matches!(
888 geometry_type.as_deref(),
889 Some("arc") | Some("circle")
890 ) {
891 "radius"
892 } else {
893 ""
894 };
895 Some(Built {
896 store_type: "⟺".to_string(),
897 display_style: ds,
898 lists: vec![selected.clone()],
899 })
900 }
901 _ => None,
902 },
903 3 => match symbol {
904 "⏛" => Some(simple("⏛", selected.clone(), "")),
905 "⋯" => {
906 let mut pts = selected.clone();
907 if has_geometry && first_kind.as_deref() == Some("point") {
908 pts.reverse();
909 }
910 Some(simple("⋯", pts, ""))
911 }
912 "↥" => {
913 if geo_items.len() == 1 && point_items.len() == 1 {
914 let line = geo_items[0];
915 if line.geom_type == "line" && line.points.len() >= 2 {
916 Some(simple(
917 "↥",
918 vec![
919 line.points[0].clone(),
920 line.points[1].clone(),
921 point_items[0].clone(),
922 ],
923 "",
924 ))
925 } else {
926 None
927 }
928 } else {
929 Some(simple("↥", selected.clone(), ""))
932 }
933 }
934 "⇌" => Some(simple("⇌", selected.clone(), "")),
935 _ => None,
936 },
937 4 | 5 => match symbol {
938 "⏛" => {
939 if geo_items.len() == 2
942 && geo_items.iter().all(|g| g.geom_type == "line")
943 && geo_items[0].points.len() >= 2
944 && geo_items[1].points.len() >= 2
945 {
946 let (g0, g1) = (geo_items[0], geo_items[1]);
947 Some(Built {
948 store_type: "⏛".to_string(),
949 display_style: "",
950 lists: vec![
951 vec![
952 g0.points[0].clone(),
953 g0.points[1].clone(),
954 g1.points[0].clone(),
955 ],
956 vec![
957 g0.points[0].clone(),
958 g0.points[1].clone(),
959 g1.points[1].clone(),
960 ],
961 ],
962 })
963 } else {
964 None
965 }
966 }
967 "⟂" => {
968 if selected.len() != 4 {
969 None
970 } else {
971 let mut pts = selected.clone();
972 if sketch_perpendicular_should_swap(doc, &pts) {
973 pts.swap(0, 1);
974 }
975 Some(simple("⟂", pts, ""))
976 }
977 }
978 "∥" => Some(simple("∥", selected.clone(), "")),
979 "∠" => Some(simple("∠", selected.clone(), "")),
980 "⇌" => Some(simple("⇌", selected.clone(), "")),
981 _ => None,
982 },
983 _ => None,
984 }
985 }
986 }
987 }
988 };
989
990 let Some(built) = built else {
991 return false;
992 };
993
994 let doc = &mut session.doc;
997 let mut added_any = false;
998 for pts in &built.lists {
999 let sig = sketch_constraint_signature(&built.store_type, pts);
1000 let duplicate = doc.constraints.iter().any(|c| match c.ctype() {
1001 Some(t) => sketch_constraint_signature(t, c.points()) == sig,
1002 None => false,
1003 });
1004 if duplicate {
1005 continue;
1006 }
1007 let id = doc.next_constraint_id();
1008 let mut raw = serde_json::Map::new();
1009 raw.insert("id".to_string(), id);
1010 raw.insert(
1011 "type".to_string(),
1012 serde_json::Value::String(built.store_type.clone()),
1013 );
1014 raw.insert("points".to_string(), serde_json::Value::Array(pts.clone()));
1015 raw.insert("labelX".to_string(), serde_json::Value::from(0));
1016 raw.insert("labelY".to_string(), serde_json::Value::from(0));
1017 raw.insert(
1018 "displayStyle".to_string(),
1019 serde_json::Value::String(built.display_style.to_string()),
1020 );
1021 raw.insert("value".to_string(), serde_json::Value::Null);
1022 raw.insert("valueNeedsSetup".to_string(), serde_json::Value::Bool(true));
1023 doc.constraints.push(crate::sketch::SketchConstraint { raw });
1024 added_any = true;
1025 }
1026 added_any
1027}
1028