1use serde_json::{json, Value};
10
11use super::doc::{id_key, SketchDiagnostics, SketchDoc};
12use super::tessellate::{self, SketchTessellation};
13use super::{solve, PlaneFrame};
14use crate::style::SketchColors;
15
16pub struct SketchSession {
19 pub doc: SketchDoc,
21 pub plane: PlaneFrame,
23 pub diagnostics: SketchDiagnostics,
25
26 pub selection: Vec<Value>,
30 pub hovered: Option<Value>,
32 pub tool: Option<String>,
34 pub dim_offsets: serde_json::Map<String, Value>,
36 pub colors: SketchColors,
43 pub solver_settings: solve::SketchSolverSettings,
46}
47
48impl SketchSession {
49 pub fn new(doc: SketchDoc, plane: PlaneFrame) -> Result<Self, String> {
51 let (solved, diagnostics) = solve::solve(&doc)?;
52 Ok(Self {
53 doc: solved,
54 plane,
55 diagnostics,
56 selection: Vec::new(),
57 hovered: None,
58 tool: None,
59 dim_offsets: serde_json::Map::new(),
60 colors: SketchColors::default(),
61 solver_settings: solve::SketchSolverSettings::default(),
62 })
63 }
64
65 pub fn resolve(&mut self) -> Result<(), String> {
68 let (solved, diagnostics) = solve::solve_with(&self.doc, &self.solver_settings)?;
69 self.doc = solved;
70 self.diagnostics = diagnostics;
71 Ok(())
72 }
73
74 pub fn overlay_json(&self, world_per_pixel: f64) -> String {
77 tessellate::overlay_json(
78 &self.doc,
79 &self.diagnostics,
80 &self.plane,
81 world_per_pixel,
82 &self.colors,
83 )
84 }
85
86 pub fn tessellation(&self, world_per_pixel: f64) -> SketchTessellation {
88 tessellate::tessellate(
89 &self.doc,
90 &self.diagnostics,
91 &self.plane,
92 world_per_pixel,
93 &self.colors,
94 )
95 }
96
97 pub fn overlay_json_with_state(&self, world_per_pixel: f64) -> String {
100 tessellate::overlay_json_with_state(
101 &self.doc,
102 &self.diagnostics,
103 &self.plane,
104 world_per_pixel,
105 &self.colors,
106 self.hovered.as_ref(),
107 &self.selection,
108 )
109 }
110
111 pub fn dim_leaders_overlay_json(&self, world_per_pixel: f64) -> String {
116 super::dimensions::dimension_leaders_overlay_json(
117 &self.doc,
118 &self.plane,
119 &self.dim_offsets,
120 world_per_pixel,
121 &self.colors,
122 )
123 }
124
125 pub fn dim_leaders_overlay_json_with_state(&self, world_per_pixel: f64) -> String {
129 super::dimensions::dimension_leaders_overlay_json_with_state(
130 &self.doc,
131 &self.plane,
132 &self.dim_offsets,
133 world_per_pixel,
134 &self.colors,
135 self.hovered.as_ref(),
136 &self.selection,
137 )
138 }
139
140 pub fn constraint_glyphs_overlay_json(&self, world_per_pixel: f64) -> String {
147 super::constraint_glyphs::constraint_glyphs_overlay_json(
148 &self.doc,
149 &self.plane,
150 world_per_pixel,
151 &self.colors,
152 )
153 }
154
155 pub fn constraint_glyphs_overlay_json_with_state(&self, world_per_pixel: f64) -> String {
159 super::constraint_glyphs::constraint_glyphs_overlay_json_with_state(
160 &self.doc,
161 &self.plane,
162 world_per_pixel,
163 &self.colors,
164 self.hovered.as_ref(),
165 &self.selection,
166 )
167 }
168
169 pub fn dimension_labels(&self, world_per_pixel: f64) -> Vec<super::dimensions::DimLabel> {
174 super::dimensions::dimension_labels(
175 &self.doc,
176 &self.plane,
177 &self.dim_offsets,
178 world_per_pixel,
179 )
180 }
181
182 pub fn preview_overlay_json(
188 &self,
189 world_per_pixel: f64,
190 pending: &[Value],
191 hover_uv: Option<(f64, f64)>,
192 stroke: &[(f64, f64)],
193 ) -> String {
194 let pending_uv: Vec<[f64; 2]> = pending
195 .iter()
196 .filter_map(|id| self.doc.point(id).map(|p| [p.x, p.y]))
197 .collect();
198 let stroke_uv: Vec<[f64; 2]> = stroke.iter().map(|&(u, v)| [u, v]).collect();
199 tessellate::preview_overlay_json(
200 self.tool.as_deref(),
201 &pending_uv,
202 hover_uv,
203 &stroke_uv,
204 &self.plane,
205 world_per_pixel,
206 &self.colors,
207 )
208 }
209
210 pub fn is_selected(&self, entity_ref: &Value) -> bool {
214 self.selection.iter().any(|r| refs_equal(r, entity_ref))
215 }
216
217 pub fn toggle_selection(&mut self, entity_ref: Value) {
219 if let Some(pos) = self.selection.iter().position(|r| refs_equal(r, &entity_ref)) {
220 self.selection.remove(pos);
221 } else {
222 self.selection.push(entity_ref);
223 }
224 }
225
226 pub fn clear_selection(&mut self) {
228 self.selection.clear();
229 }
230
231 pub fn set_hover(&mut self, entity_ref: Option<Value>) {
233 self.hovered = entity_ref;
234 }
235
236 pub fn pick_entity(&self, u: f64, v: f64, radius: f64) -> Option<Value> {
243 let mut best_pt: Option<(f64, &Value)> = None;
245 for p in &self.doc.points {
246 let d = ((p.x - u).powi(2) + (p.y - v).powi(2)).sqrt();
247 if d <= radius && best_pt.map_or(true, |(bd, _)| d < bd) {
248 best_pt = Some((d, &p.id));
249 }
250 }
251 if let Some((_, id)) = best_pt {
252 return Some(point_ref(id));
253 }
254
255 let mut best_geo: Option<(f64, &Value)> = None;
257 for g in &self.doc.geometries {
258 let poly = tessellate::geometry_polyline_uv(g, &self.doc);
259 if poly.len() < 2 {
260 continue;
261 }
262 let mut dmin = f64::INFINITY;
263 for seg in poly.windows(2) {
264 let d = point_segment_distance(u, v, seg[0], seg[1]);
265 if d < dmin {
266 dmin = d;
267 }
268 }
269 if dmin <= radius && best_geo.map_or(true, |(bd, _)| dmin < bd) {
270 best_geo = Some((dmin, &g.id));
271 }
272 }
273 best_geo.map(|(_, id)| geometry_ref(id))
274 }
275
276 pub fn pick_constraint(&self, u: f64, v: f64, radius: f64, world_per_pixel: f64) -> Option<Value> {
288 let mut best: Option<(f64, &Value)> = None;
289 for c in &self.doc.constraints {
290 if c.temporary() {
291 continue;
292 }
293 let Some(id) = c.raw.get("id") else { continue };
294 let mut dmin = f64::INFINITY;
297 for (a, b) in
298 super::constraint_glyphs::constraint_glyph_segments(c, &self.doc, world_per_pixel)
299 {
300 let d = point_segment_distance(u, v, a, b);
301 if d < dmin {
302 dmin = d;
303 }
304 }
305 if let Some(segments) = super::dimensions::constraint_dim_segments(
306 c,
307 &self.doc,
308 &self.dim_offsets,
309 world_per_pixel,
310 ) {
311 for (a, b) in segments {
312 let d = point_segment_distance(u, v, a, b);
313 if d < dmin {
314 dmin = d;
315 }
316 }
317 }
318 if dmin <= radius && best.map_or(true, |(bd, _)| dmin < bd) {
319 best = Some((dmin, id));
320 }
321 }
322 best.map(|(_, id)| constraint_ref(id))
323 }
324
325 pub fn pick_draggable_point(&self, u: f64, v: f64, radius: f64) -> Option<(Value, bool)> {
330 let mut best: Option<(f64, &super::doc::SketchPoint)> = None;
331 for p in &self.doc.points {
332 let movable = self.diagnostics.point_movable(&p.id).unwrap_or(!p.fixed);
333 if !movable {
334 continue;
335 }
336 let d = ((p.x - u).powi(2) + (p.y - v).powi(2)).sqrt();
337 if d <= radius && best.map_or(true, |(bd, _)| d < bd) {
338 best = Some((d, p));
339 }
340 }
341 best.map(|(_, p)| (p.id.clone(), p.fixed))
342 }
343
344 pub fn drag_points_from_ref(&self, entity_ref: &Value) -> Option<Vec<(Value, f64, f64, bool)>> {
353 let kind = entity_ref.get("kind").and_then(Value::as_str)?;
354 let id = entity_ref.get("id")?;
355 match kind {
356 "point" => {
357 let p = self.doc.point(id)?;
358 let movable = self.diagnostics.point_movable(&p.id).unwrap_or(!p.fixed);
359 movable.then(|| vec![(p.id.clone(), p.x, p.y, p.fixed)])
360 }
361 "geometry" => {
362 let g = self
366 .doc
367 .geometries
368 .iter()
369 .find(|g| id_key(&g.id) == id_key(id))?;
370 let mut out: Vec<(Value, f64, f64, bool)> = Vec::new();
371 let mut any_movable = false;
372 for pid in &g.points {
373 if out.iter().any(|(pt_id, ..)| id_key(pt_id) == id_key(pid)) {
376 continue;
377 }
378 if let Some(p) = self.doc.point(pid) {
379 any_movable |= self.diagnostics.point_movable(&p.id).unwrap_or(!p.fixed);
380 out.push((p.id.clone(), p.x, p.y, p.fixed));
381 }
382 }
383 (any_movable && !out.is_empty()).then_some(out)
384 }
385 _ => None,
386 }
387 }
388
389 pub fn seed_rectangle_circle() -> Result<Self, String> {
395 Self::new(seed_doc(), PlaneFrame::xy())
396 }
397}
398
399fn seed_doc() -> SketchDoc {
401 let value = json!({
402 "points": [
403 { "id": 0, "x": 0.0, "y": 0.0, "fixed": false, "construction": false, "externalReference": false },
405 { "id": 1, "x": 20.0, "y": 0.0, "fixed": false, "construction": false, "externalReference": false },
406 { "id": 2, "x": 20.0, "y": 12.0, "fixed": false, "construction": false, "externalReference": false },
407 { "id": 3, "x": 0.0, "y": 12.0, "fixed": false, "construction": false, "externalReference": false },
408 { "id": 4, "x": 34.0, "y": 6.0, "fixed": false, "construction": false, "externalReference": false },
410 { "id": 5, "x": 40.0, "y": 6.0, "fixed": false, "construction": false, "externalReference": false }
411 ],
412 "geometries": [
413 { "id": 10, "type": "line", "points": [0, 1], "construction": false },
414 { "id": 11, "type": "line", "points": [1, 2], "construction": false },
415 { "id": 12, "type": "line", "points": [2, 3], "construction": false },
416 { "id": 13, "type": "line", "points": [3, 0], "construction": false },
417 { "id": 20, "type": "circle", "points": [4, 5], "construction": false }
418 ],
419 "constraints": [
420 { "id": 0, "type": "⏚", "points": [0] },
421 { "id": 1, "type": "━", "points": [0, 1] },
422 { "id": 2, "type": "⟺", "points": [0, 1], "value": 20.0 },
423 { "id": 3, "type": "│", "points": [1, 2] },
424 { "id": 4, "type": "⟺", "points": [1, 2], "value": 12.0 },
425 { "id": 5, "type": "━", "points": [2, 3] },
426 { "id": 6, "type": "│", "points": [3, 0] }
427 ]
428 });
429 serde_json::from_value(value).expect("seed sketch doc is valid")
430}
431
432pub fn point_ref(id: &Value) -> Value {
441 json!({ "kind": "point", "id": id.clone() })
442}
443
444pub fn geometry_ref(id: &Value) -> Value {
446 json!({ "kind": "geometry", "id": id.clone() })
447}
448
449pub fn constraint_ref(id: &Value) -> Value {
453 json!({ "kind": "constraint", "id": id.clone() })
454}
455
456pub fn refs_equal(a: &Value, b: &Value) -> bool {
459 a.get("kind") == b.get("kind") && a.get("id").map(id_key) == b.get("id").map(id_key)
460}
461
462pub fn entity_ref_eq(a: Option<&Value>, b: Option<&Value>) -> bool {
465 match (a, b) {
466 (None, None) => true,
467 (Some(x), Some(y)) => refs_equal(x, y),
468 _ => false,
469 }
470}
471
472fn point_segment_distance(px: f64, py: f64, a: [f64; 2], b: [f64; 2]) -> f64 {
475 let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
476 let len2 = dx * dx + dy * dy;
477 let t = if len2 <= 1e-18 {
478 0.0
479 } else {
480 (((px - a[0]) * dx + (py - a[1]) * dy) / len2).clamp(0.0, 1.0)
481 };
482 let (cx, cy) = (a[0] + t * dx, a[1] + t * dy);
483 ((px - cx).powi(2) + (py - cy).powi(2)).sqrt()
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use serde_json::json;
490
491 #[test]
492 fn refs_equal_matches_kind_and_id_under_id_key() {
493 assert!(refs_equal(&point_ref(&json!(4)), &point_ref(&json!(4.0))));
494 assert!(!refs_equal(&point_ref(&json!(4)), &geometry_ref(&json!(4))));
495 assert!(!refs_equal(&point_ref(&json!(4)), &point_ref(&json!(5))));
496 assert!(entity_ref_eq(None, None));
497 assert!(!entity_ref_eq(Some(&point_ref(&json!(4))), None));
498 assert!(entity_ref_eq(
499 Some(&point_ref(&json!(4))),
500 Some(&point_ref(&json!(4)))
501 ));
502 }
503
504 #[test]
505 fn toggle_and_is_selected_add_then_remove() {
506 let mut s = SketchSession::seed_rectangle_circle().expect("seed");
507 let r = point_ref(&json!(4));
508 assert!(!s.is_selected(&r));
509 s.toggle_selection(r.clone());
510 assert!(s.is_selected(&r));
511 assert_eq!(s.selection.len(), 1);
512 s.toggle_selection(r.clone());
513 assert!(!s.is_selected(&r));
514 assert_eq!(s.selection.len(), 0);
515 }
516
517 #[test]
518 fn pick_entity_prefers_the_nearest_point_within_radius() {
519 let s = SketchSession::seed_rectangle_circle().expect("seed");
520 let hit = s.pick_entity(34.1, 6.05, 0.5).expect("point hit");
522 assert!(refs_equal(&hit, &point_ref(&json!(4))), "hit = {hit}");
523 }
524
525 #[test]
526 fn pick_entity_returns_none_outside_radius() {
527 let s = SketchSession::seed_rectangle_circle().expect("seed");
528 assert!(s.pick_entity(100.0, 100.0, 0.5).is_none());
530 }
531
532 #[test]
533 fn drag_points_from_ref_resolves_point_and_geometry() {
534 let s = SketchSession::seed_rectangle_circle().expect("seed");
535 let pt = s
537 .drag_points_from_ref(&point_ref(&json!(4)))
538 .expect("movable point grabs");
539 assert_eq!(pt.len(), 1);
540 assert!(refs_equal(&point_ref(&pt[0].0), &point_ref(&json!(4))));
541 assert!(!pt[0].3, "p4 is not fixed");
542 assert!(s.drag_points_from_ref(&point_ref(&json!(0))).is_none());
545 let circle = s
548 .drag_points_from_ref(&geometry_ref(&json!(20)))
549 .expect("movable geometry grabs");
550 assert_eq!(circle.len(), 2, "the circle translates both its points");
551 assert!(s.drag_points_from_ref(&geometry_ref(&json!(10))).is_none());
553 assert!(s.drag_points_from_ref(&point_ref(&json!(999))).is_none());
555 assert!(s.drag_points_from_ref(&geometry_ref(&json!(999))).is_none());
556 }
557
558 #[test]
559 fn pick_entity_falls_through_to_geometry_mid_segment() {
560 let s = SketchSession::seed_rectangle_circle().expect("seed");
561 let hit = s.pick_entity(10.0, 0.05, 0.2).expect("geometry hit");
564 assert!(refs_equal(&hit, &geometry_ref(&json!(10))), "hit = {hit}");
565 }
566
567 #[test]
568 fn pick_draggable_point_skips_locked_points() {
569 let s = SketchSession::seed_rectangle_circle().expect("seed");
570 assert!(s.pick_draggable_point(0.0, 0.0, 0.5).is_none());
572 let (id, fixed) = s.pick_draggable_point(34.0, 6.0, 0.5).expect("draggable");
574 assert!(id == json!(4) && !fixed, "id = {id}, fixed = {fixed}");
575 }
576
577 #[test]
578 fn pick_constraint_finds_glyph_and_leader_but_not_far() {
579 let doc: crate::sketch::SketchDoc = serde_json::from_value(json!({
582 "points": [
583 { "id": 0, "x": 0.0, "y": 0.0, "fixed": true },
584 { "id": 1, "x": 10.0, "y": 0.0 }
585 ],
586 "geometries": [{ "id": 10, "type": "line", "points": [0, 1] }],
587 "constraints": [
588 { "id": 0, "type": "━", "points": [0, 1] },
589 { "id": 1, "type": "⟺", "points": [0, 1], "value": 10.0 }
590 ]
591 }))
592 .expect("doc");
593 let s = SketchSession::new(doc, PlaneFrame::xy()).expect("session");
594 let (wpp, radius) = (0.05, 0.3);
595 let hit = s.pick_constraint(5.0, 0.77, radius, wpp).expect("glyph hit");
597 assert_eq!(hit["kind"], "constraint", "glyph pick is a constraint ref");
598 let hit2 = s.pick_constraint(5.0, 1.0, radius, wpp).expect("leader hit");
600 assert_eq!(hit2["kind"], "constraint", "leader pick is a constraint ref");
601 assert!(s.pick_constraint(5.0, 20.0, radius, wpp).is_none(), "nothing far off");
603 }
604
605 #[test]
606 fn custom_settings_color_reaches_the_tessellation() {
607 let mut s = SketchSession::seed_rectangle_circle().expect("seed");
611 s.colors = crate::style::RenderSettings::default().sketch_colors();
612 s.colors.movable = 0x123456;
613 let tess = s.tessellation(0.05);
614 let p4 = s.doc.point(&json!(4)).unwrap();
616 let idx = tess
617 .point_positions
618 .chunks(3)
619 .position(|c| (c[0] - p4.x as f32).abs() < 1e-4 && (c[1] - p4.y as f32).abs() < 1e-4)
620 .expect("circle center among overlay points");
621 let col = &tess.point_colors[idx * 3..idx * 3 + 3];
622 assert!((col[0] - 0x12 as f32 / 255.0).abs() < 1e-3, "r wrong: {col:?}");
623 assert!((col[1] - 0x34 as f32 / 255.0).abs() < 1e-3, "g wrong: {col:?}");
624 assert!((col[2] - 0x56 as f32 / 255.0).abs() < 1e-3, "b wrong: {col:?}");
625 }
626
627 #[test]
628 fn overlay_state_colors_selected_amber_over_mobility() {
629 let mut s = SketchSession::seed_rectangle_circle().expect("seed");
630 s.toggle_selection(point_ref(&json!(4)));
631 s.set_hover(Some(point_ref(&json!(5))));
632 let tess = tessellate::tessellate_with_state(
633 &s.doc,
634 &s.diagnostics,
635 &s.plane,
636 0.05,
637 &s.colors,
638 s.hovered.as_ref(),
639 &s.selection,
640 );
641 let p4 = s.doc.point(&json!(4)).unwrap();
643 let idx4 = tess
644 .point_positions
645 .chunks(3)
646 .position(|c| (c[0] - p4.x as f32).abs() < 1e-4 && (c[1] - p4.y as f32).abs() < 1e-4)
647 .expect("p4 among points");
648 let c4 = &tess.point_colors[idx4 * 3..idx4 * 3 + 3];
649 assert!((c4[0] - 0xff as f32 / 255.0).abs() < 1e-3, "selected not amber: {c4:?}");
650 assert!((c4[1] - 0xa5 as f32 / 255.0).abs() < 1e-3, "selected not amber: {c4:?}");
651 }
652}