1use std::collections::HashMap;
23use std::f64::consts::PI;
24
25use serde_json::Value;
26
27use super::doc::{id_key, SketchConstraint, SketchDoc, SketchPoint};
28use super::PlaneFrame;
29use crate::style::SketchColors;
30
31pub const OVERLAY_DIM_LEADERS: &str = "sketch-dim-leaders";
34
35#[derive(Clone, Debug)]
37pub struct DimLabel {
38 pub id: Value,
40 pub text: String,
42 pub world: [f64; 3],
44 pub value: Option<f64>,
46 pub value_expr: Option<String>,
48 pub mode: &'static str,
50}
51
52struct DimGeometry {
54 segments: Vec<([f64; 2], [f64; 2])>,
56 label_uv: [f64; 2],
58 text: String,
59 value: Option<f64>,
60 value_expr: Option<String>,
61 mode: &'static str,
62}
63
64pub(super) fn point_index(doc: &SketchDoc) -> HashMap<String, &SketchPoint> {
68 let mut by_id: HashMap<String, &SketchPoint> = HashMap::with_capacity(doc.points.len());
69 for p in &doc.points {
70 by_id.insert(id_key(&p.id), p);
71 }
72 by_id
73}
74
75fn is_radial_dimension(c: &SketchConstraint) -> bool {
78 if c.ctype() != Some("⟺") || c.points().len() < 2 {
79 return false;
80 }
81 matches!(
82 c.raw.get("displayStyle").and_then(Value::as_str),
83 Some("radius") | Some("diameter")
84 )
85}
86
87fn saved_offset(dim_offsets: &serde_json::Map<String, Value>, id: &Value) -> Option<[f64; 2]> {
90 let entry = dim_offsets.get(&id_key(id))?;
91 let du = entry.get("du").and_then(Value::as_f64);
92 let dv = entry.get("dv").and_then(Value::as_f64);
93 match (du, dv) {
94 (Some(du), Some(dv)) if du.is_finite() && dv.is_finite() => Some([du, dv]),
95 (Some(du), None) if du.is_finite() => Some([du, 0.0]),
97 (None, Some(dv)) if dv.is_finite() => Some([0.0, dv]),
98 _ => None,
99 }
100}
101
102fn linear_endpoints(
108 c: &SketchConstraint,
109 by_id: &HashMap<String, &SketchPoint>,
110) -> Option<([f64; 2], [f64; 2], [f64; 2])> {
111 let get = |id: &Value| by_id.get(&id_key(id)).copied();
112 let pts = c.points();
113 match c.ctype() {
114 Some("⟺") if pts.len() >= 2 && !is_radial_dimension(c) => {
115 let p0 = get(&pts[0])?;
116 let p1 = get(&pts[1])?;
117 let a = [p0.x, p0.y];
118 Some((a, [p1.x, p1.y], a))
119 }
120 Some("↥") if pts.len() >= 3 => {
121 let a = get(&pts[0])?;
122 let b = get(&pts[1])?;
123 let cpt = get(&pts[2])?;
124 let dx = b.x - a.x;
125 let dy = b.y - a.y;
126 let len_sq = dx * dx + dy * dy;
127 if !(len_sq > 1e-12) {
128 return Some(([a.x, a.y], [cpt.x, cpt.y], [a.x, a.y]));
129 }
130 let t_raw = ((cpt.x - a.x) * dx + (cpt.y - a.y) * dy) / len_sq;
131 let t_raw = if t_raw.is_finite() { t_raw } else { 0.0 };
132 let t_seg = t_raw.clamp(0.0, 1.0);
133 Some((
134 [a.x + t_raw * dx, a.y + t_raw * dy],
135 [cpt.x, cpt.y],
136 [a.x + t_seg * dx, a.y + t_seg * dy],
137 ))
138 }
139 _ => None,
140 }
141}
142
143fn radial_default_offset(pc: [f64; 2], pr: [f64; 2]) -> [f64; 2] {
146 let vx = pr[0] - pc[0];
147 let vy = pr[1] - pc[1];
148 let l = vx.hypot(vy);
149 let (rx, ry) = if l > 1e-9 { (vx / l, vy / l) } else { (1.0, 0.0) };
150 let (nx, ny) = (-ry, rx);
151 let base = (l * 0.35).max(0.2);
152 [
153 vx + rx * base + nx * base * 0.35,
154 vy + ry * base + ny * base * 0.35,
155 ]
156}
157
158fn push_arrow(
161 segments: &mut Vec<([f64; 2], [f64; 2])>,
162 tip: [f64; 2],
163 dx: f64,
164 dy: f64,
165 ah: f64,
166 s: f64,
167) {
168 let len = dx.hypot(dy);
169 if !(len > 1e-9) {
170 return;
171 }
172 let (tx, ty) = (dx / len, dy / len);
173 let (wx, wy) = (-ty, tx);
174 let a = [tip[0] + tx * ah + wx * ah * s, tip[1] + ty * ah + wy * ah * s];
175 let b = [tip[0] + tx * ah - wx * ah * s, tip[1] + ty * ah - wy * ah * s];
176 segments.push((tip, a));
177 segments.push((tip, b));
178}
179
180fn dim_geometry(
183 c: &SketchConstraint,
184 by_id: &HashMap<String, &SketchPoint>,
185 dim_offsets: &serde_json::Map<String, Value>,
186 wpp: f64,
187) -> Option<DimGeometry> {
188 let ctype = c.ctype()?;
189 let id = c.raw.get("id")?;
190 let value = c
191 .raw
192 .get("value")
193 .and_then(Value::as_f64)
194 .filter(|v| v.is_finite());
195 let value_expr = c
196 .raw
197 .get("valueExpr")
198 .and_then(Value::as_str)
199 .filter(|s| !s.is_empty())
200 .map(str::to_string);
201
202 let ah = (wpp * 6.0).max(0.06);
204 let arrow_s = 0.6;
205
206 if is_radial_dimension(c) {
207 let pts = c.points();
209 let pc = by_id.get(&id_key(&pts[0])).copied()?;
210 let pr = by_id.get(&id_key(&pts[1])).copied()?;
211 let center = [pc.x, pc.y];
212 let radius = (pr.x - pc.x).hypot(pr.y - pc.y);
213 if !(radius > 1e-9) {
214 return None;
215 }
216 let diameter = c.raw.get("displayStyle").and_then(Value::as_str) == Some("diameter");
217 let off = saved_offset(dim_offsets, id).unwrap_or_else(|| radial_default_offset(center, [pr.x, pr.y]));
218 let label_uv = [center[0] + off[0], center[1] + off[1]];
219
220 let (mut dx, mut dy) = (label_uv[0] - center[0], label_uv[1] - center[1]);
222 let mut l = dx.hypot(dy);
223 if !(l > 1e-9) {
224 dx = pr.x - pc.x;
225 dy = pr.y - pc.y;
226 l = dx.hypot(dy).max(1e-9);
227 }
228 let (ux, uy) = (dx / l, dy / l);
229 let near = [center[0] + ux * radius, center[1] + uy * radius];
230 let far = [center[0] - ux * radius, center[1] - uy * radius];
231
232 let mut segments = Vec::new();
233 if diameter {
234 segments.push((far, near));
235 segments.push((near, label_uv));
236 push_arrow(&mut segments, near, -ux, -uy, ah, arrow_s);
238 push_arrow(&mut segments, far, ux, uy, ah, arrow_s);
239 } else {
240 segments.push((center, near));
241 segments.push((near, label_uv));
242 push_arrow(&mut segments, near, -ux, -uy, ah, arrow_s);
243 }
244
245 let safe = value.unwrap_or(0.0);
246 let (text, mode) = if diameter {
247 (format!("⌀{:.3}", 2.0 * safe), "diameter")
248 } else {
249 (format!("R{safe:.3}"), "radius")
250 };
251 return Some(DimGeometry {
252 segments,
253 label_uv,
254 text,
255 value,
256 value_expr,
257 mode,
258 });
259 }
260
261 if ctype == "⟺" || ctype == "↥" {
262 let (a, b, anchor_a) = linear_endpoints(c, by_id)?;
264 let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
265 let len = dx.hypot(dy).max(1e-9);
266 let (tx, ty) = (dx / len, dy / len);
267 let (nx, ny) = (-ty, tx);
268 let base = (wpp * 20.0).max(0.1);
269 let mid = [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0];
270 let off = saved_offset(dim_offsets, id).unwrap_or([nx * base, ny * base]);
272 let label_uv = [mid[0] + off[0], mid[1] + off[1]];
273 let off_n = off[0] * nx + off[1] * ny;
277 let ou = nx * off_n;
278 let ov = ny * off_n;
279
280 let a_off = [a[0] + ou, a[1] + ov];
281 let b_off = [b[0] + ou, b[1] + ov];
282 let mut segments = vec![
283 (a_off, b_off), (anchor_a, a_off), (b, b_off), ];
287 push_arrow(&mut segments, a_off, tx, ty, ah, arrow_s);
289 push_arrow(&mut segments, b_off, -tx, -ty, ah, arrow_s);
290
291 let t_lbl = (label_uv[0] - a_off[0]) * tx + (label_uv[1] - a_off[1]) * ty;
297 let foot = {
298 let t = t_lbl.clamp(0.0, len);
299 [a_off[0] + tx * t, a_off[1] + ty * t]
300 };
301 if (label_uv[0] - foot[0]).hypot(label_uv[1] - foot[1]) > wpp * 2.0 {
302 segments.push((foot, label_uv));
303 }
304
305 let text = format!("{:.3}", value.unwrap_or(0.0));
306 return Some(DimGeometry {
307 segments,
308 label_uv,
309 text,
310 value,
311 value_expr,
312 mode: "distance",
313 });
314 }
315
316 if ctype == "∠" && c.points().len() >= 4 {
317 let pts = c.points();
319 let get = |i: usize| by_id.get(&id_key(&pts[i])).copied();
320 let (p0, p1, p2, p3) = (get(0)?, get(1)?, get(2)?, get(3)?);
321 let inter = line_intersection([p0.x, p0.y], [p1.x, p1.y], [p2.x, p2.y], [p3.x, p3.y]);
322 let (d1x, d1y) = (p1.x - p0.x, p1.y - p0.y);
323 let (d2x, d2y) = (p3.x - p2.x, p3.y - p2.y);
324 if (d1x * d1x + d1y * d1y) < 1e-12 || (d2x * d2x + d2y * d2y) < 1e-12 {
325 return None;
326 }
327 let a0 = d1y.atan2(d1x);
328 let a1 = d2y.atan2(d2x);
329 let mut signed = a1 - a0;
330 while signed <= -PI {
331 signed += 2.0 * PI;
332 }
333 while signed > PI {
334 signed -= 2.0 * PI;
335 }
336 let default_deg = (a1 - a0).to_degrees().rem_euclid(360.0);
337 let mut target_deg = match value {
338 Some(v) => {
339 let abs = v.abs();
340 let mut t = abs % 360.0;
341 if t < 1e-6 && abs > 0.0 {
342 t = 360.0;
343 }
344 t
345 }
346 None => default_deg,
347 };
348 if target_deg < 1e-6 {
349 target_deg = 1e-6;
350 }
351 let mut dir_sign = if signed == 0.0 { 1.0 } else { signed.signum() };
352 if target_deg > 180.0 && target_deg < 360.0 - 1e-6 {
353 dir_sign = -dir_sign;
354 }
355 let mut d = target_deg.to_radians() * dir_sign;
356 let two_pi = 2.0 * PI;
357 if d.abs() > two_pi {
358 d = d.signum() * (two_pi - 1e-6);
359 }
360
361 let base_r = (wpp * 24.0).max(0.3);
362 let off = saved_offset(dim_offsets, id).unwrap_or([0.0, 0.0]);
363 let r = base_r + off[0].hypot(off[1]);
364 let (cx, cy) = (inter[0], inter[1]);
365
366 let mut a_start = a0;
368 if off[0] != 0.0 || off[1] != 0.0 {
369 let label_ang = off[1].atan2(off[0]);
370 let norm = |a: f64| {
371 let t = two_pi;
372 let m = a % t;
373 if m < 0.0 {
374 m + t
375 } else {
376 m
377 }
378 };
379 let ang_diff = |a: f64, b: f64| {
380 let mut x = norm(a - b);
381 if x > PI {
382 x = two_pi - x;
383 }
384 x.abs()
385 };
386 let bisector = a_start + d * 0.5;
387 let bisector_opp = bisector + PI;
388 if ang_diff(label_ang, bisector_opp) + 1e-6 < ang_diff(label_ang, bisector) {
389 a_start += PI;
390 }
391 }
392
393 let segs = 48usize;
395 let mut segments = Vec::with_capacity(segs + 4);
396 let mut prev = [cx + a_start.cos() * r, cy + a_start.sin() * r];
397 for i in 1..=segs {
398 let t = a_start + d * (i as f64 / segs as f64);
399 let cur = [cx + t.cos() * r, cy + t.sin() * r];
400 segments.push((prev, cur));
401 prev = cur;
402 }
403 let dir_start = if d >= 0.0 { 1.0 } else { -1.0 };
405 let add_arc_arrow = |segments: &mut Vec<([f64; 2], [f64; 2])>, t: f64, dir: f64| {
406 let tip = [cx + t.cos() * r, cy + t.sin() * r];
407 let (tx, ty) = (-t.sin() * dir, t.cos() * dir);
408 push_arrow(segments, tip, tx, ty, ah, arrow_s);
409 };
410 add_arc_arrow(&mut segments, a_start, dir_start);
411 add_arc_arrow(&mut segments, a_start + d, -dir_start);
412
413 let mid_ang = a_start + d * 0.5;
415 let label_uv = [cx + mid_ang.cos() * r, cy + mid_ang.sin() * r];
416
417 let text = format!("{:.1}°", value.unwrap_or(default_deg));
420 return Some(DimGeometry {
421 segments,
422 label_uv,
423 text,
424 value,
425 value_expr,
426 mode: "angle",
427 });
428 }
429
430 None
431}
432
433pub(super) fn line_intersection(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> [f64; 2] {
436 let r = [b[0] - a[0], b[1] - a[1]];
437 let s = [d[0] - c[0], d[1] - c[1]];
438 let rxs = r[0] * s[1] - r[1] * s[0];
439 if rxs.abs() < 1e-12 {
440 return a;
441 }
442 let t = ((c[0] - a[0]) * s[1] - (c[1] - a[1]) * s[0]) / rxs;
443 [a[0] + t * r[0], a[1] + t * r[1]]
444}
445
446pub fn constraint_dim_segments(
452 c: &SketchConstraint,
453 doc: &SketchDoc,
454 dim_offsets: &serde_json::Map<String, Value>,
455 world_per_pixel: f64,
456) -> Option<Vec<([f64; 2], [f64; 2])>> {
457 let by_id = point_index(doc);
458 dim_geometry(c, &by_id, dim_offsets, world_per_pixel).map(|g| g.segments)
459}
460
461pub fn dimension_leaders_buffers(
465 doc: &SketchDoc,
466 plane: &PlaneFrame,
467 dim_offsets: &serde_json::Map<String, Value>,
468 world_per_pixel: f64,
469 colors: &SketchColors,
470) -> (Vec<f32>, Vec<f32>) {
471 dimension_leaders_buffers_with_state(doc, plane, dim_offsets, world_per_pixel, colors, None, &[])
472}
473
474pub fn dimension_leaders_buffers_with_state(
479 doc: &SketchDoc,
480 plane: &PlaneFrame,
481 dim_offsets: &serde_json::Map<String, Value>,
482 world_per_pixel: f64,
483 colors: &SketchColors,
484 hovered: Option<&Value>,
485 selection: &[Value],
486) -> (Vec<f32>, Vec<f32>) {
487 let by_id = point_index(doc);
488 let mut positions: Vec<f32> = Vec::new();
489 let mut color_buf: Vec<f32> = Vec::new();
490 for c in &doc.constraints {
491 let Some(geom) = dim_geometry(c, &by_id, dim_offsets, world_per_pixel) else {
492 continue;
493 };
494 let color = match c.raw.get("id") {
495 Some(id) => super::tessellate::interaction_color(
496 colors,
497 colors.constraint,
498 hovered,
499 selection,
500 "constraint",
501 id,
502 ),
503 None => colors.constraint,
504 };
505 let rgb = super::tessellate::rgb(color);
506 for (a, b) in geom.segments {
507 let wa = plane.to_world(a[0], a[1]);
508 let wb = plane.to_world(b[0], b[1]);
509 positions.extend_from_slice(&[
510 wa[0] as f32,
511 wa[1] as f32,
512 wa[2] as f32,
513 wb[0] as f32,
514 wb[1] as f32,
515 wb[2] as f32,
516 ]);
517 color_buf.extend_from_slice(&[rgb[0], rgb[1], rgb[2], rgb[0], rgb[1], rgb[2]]);
518 }
519 }
520 (positions, color_buf)
521}
522
523pub fn dimension_leaders_overlay_json(
526 doc: &SketchDoc,
527 plane: &PlaneFrame,
528 dim_offsets: &serde_json::Map<String, Value>,
529 world_per_pixel: f64,
530 colors: &SketchColors,
531) -> String {
532 dimension_leaders_overlay_json_with_state(
533 doc,
534 plane,
535 dim_offsets,
536 world_per_pixel,
537 colors,
538 None,
539 &[],
540 )
541}
542
543pub fn dimension_leaders_overlay_json_with_state(
546 doc: &SketchDoc,
547 plane: &PlaneFrame,
548 dim_offsets: &serde_json::Map<String, Value>,
549 world_per_pixel: f64,
550 colors: &SketchColors,
551 hovered: Option<&Value>,
552 selection: &[Value],
553) -> String {
554 let (positions, color_buf) = dimension_leaders_buffers_with_state(
555 doc,
556 plane,
557 dim_offsets,
558 world_per_pixel,
559 colors,
560 hovered,
561 selection,
562 );
563 serde_json::json!({
564 "groups": [
565 {
566 "name": OVERLAY_DIM_LEADERS,
567 "renderOrder": 10003,
568 "lines": { "positions": positions, "colors": color_buf },
569 }
570 ]
571 })
572 .to_string()
573}
574
575pub fn dimension_labels(
578 doc: &SketchDoc,
579 plane: &PlaneFrame,
580 dim_offsets: &serde_json::Map<String, Value>,
581 world_per_pixel: f64,
582) -> Vec<DimLabel> {
583 let by_id = point_index(doc);
584 let mut out = Vec::new();
585 for c in &doc.constraints {
586 let Some(geom) = dim_geometry(c, &by_id, dim_offsets, world_per_pixel) else {
587 continue;
588 };
589 let Some(id) = c.raw.get("id").cloned() else {
590 continue;
591 };
592 out.push(DimLabel {
593 id,
594 text: geom.text,
595 world: plane.to_world(geom.label_uv[0], geom.label_uv[1]),
596 value: geom.value,
597 value_expr: geom.value_expr,
598 mode: geom.mode,
599 });
600 }
601 out
602}
603
604pub fn dimension_anchor_uv(
609 c: &SketchConstraint,
610 doc: &SketchDoc,
611) -> Option<[f64; 2]> {
612 let by_id = point_index(doc);
613 let get = |id: &Value| by_id.get(&id_key(id)).copied();
614 if is_radial_dimension(c) {
615 let pc = get(&c.points()[0])?;
616 return Some([pc.x, pc.y]);
617 }
618 match c.ctype() {
619 Some("⟺") | Some("↥") => {
620 let (a, b, _) = linear_endpoints(c, &by_id)?;
621 Some([(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0])
622 }
623 Some("∠") if c.points().len() >= 4 => {
624 let pts = c.points();
625 let g = |i: usize| get(&pts[i]);
626 let (p0, p1, p2, p3) = (g(0)?, g(1)?, g(2)?, g(3)?);
627 Some(line_intersection(
628 [p0.x, p0.y],
629 [p1.x, p1.y],
630 [p2.x, p2.y],
631 [p3.x, p3.y],
632 ))
633 }
634 _ => None,
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641 use serde_json::json;
642
643 fn doc_from(value: Value) -> SketchDoc {
644 serde_json::from_value(value).expect("sketch doc")
645 }
646
647 fn no_offsets() -> serde_json::Map<String, Value> {
648 serde_json::Map::new()
649 }
650
651 #[test]
654 fn distance_dim_builds_leader_and_midpoint_label() {
655 let doc = doc_from(json!({
656 "points": [
657 { "id": 0, "x": 0.0, "y": 0.0 },
658 { "id": 1, "x": 10.0, "y": 0.0 }
659 ],
660 "geometries": [{ "id": 10, "type": "line", "points": [0, 1] }],
661 "constraints": [
662 { "id": 0, "type": "⟺", "points": [0, 1], "value": 10.0, "displayStyle": "" }
663 ]
664 }));
665 let plane = PlaneFrame::xy();
666 let (pos, col) = dimension_leaders_buffers(&doc, &plane, &no_offsets(), 0.05, &Default::default());
667 assert!(!pos.is_empty(), "distance dim should emit leader segments");
668 assert_eq!(pos.len(), col.len());
669 assert!(pos.iter().all(|f| f.is_finite()), "leader positions finite");
670
671 let labels = dimension_labels(&doc, &plane, &no_offsets(), 0.05);
672 assert_eq!(labels.len(), 1);
673 let l = &labels[0];
674 assert_eq!(l.mode, "distance");
675 assert_eq!(l.text, "10.000");
676 assert!(l.world.iter().all(|f| f.is_finite()));
677 assert!((l.world[0] - 5.0).abs() < 1e-6, "label x = {}", l.world[0]);
679 assert!(l.world[1] > 0.0, "label pushed off the segment: y = {}", l.world[1]);
680 }
681
682 #[test]
686 fn dragged_linear_label_gets_a_leader_back_to_the_dimension() {
687 let doc = doc_from(json!({
688 "points": [{ "id": 0, "x": 0.0, "y": 0.0 }, { "id": 1, "x": 20.0, "y": 0.0 }],
689 "geometries": [{ "id": 10, "type": "line", "points": [0, 1] }],
690 "constraints": [{ "id": 0, "type": "⟺", "points": [0, 1], "value": 20.0, "displayStyle": "" }]
691 }));
692 let by_id: std::collections::HashMap<String, &SketchPoint> =
693 doc.points.iter().map(|p| (id_key(&p.id), p)).collect();
694 let wpp = 0.05;
695 let touches_label = |g: &DimGeometry| {
696 g.segments.iter().any(|(a, b)| {
697 let eq = |p: &[f64; 2]| {
698 (p[0] - g.label_uv[0]).abs() < 1e-6 && (p[1] - g.label_uv[1]).abs() < 1e-6
699 };
700 eq(a) || eq(b)
701 })
702 };
703
704 let centered = dim_geometry(&doc.constraints[0], &by_id, &no_offsets(), wpp).unwrap();
706 assert!(!touches_label(¢ered), "a centered label needs no leader");
707
708 let mut offsets = serde_json::Map::new();
710 offsets.insert("0".to_string(), json!({ "du": -30.0, "dv": 5.0 }));
711 let dragged = dim_geometry(&doc.constraints[0], &by_id, &offsets, wpp).unwrap();
712 assert!(
713 touches_label(&dragged),
714 "a label dragged past the span should get a leader to the dimension"
715 );
716 }
717
718 #[test]
721 fn angle_dim_text_is_degrees_with_symbol() {
722 let doc = doc_from(json!({
723 "points": [
724 { "id": 0, "x": 0.0, "y": 0.0 },
725 { "id": 1, "x": 10.0, "y": 0.0 },
726 { "id": 2, "x": 0.0, "y": 0.0 },
727 { "id": 3, "x": 0.0, "y": 10.0 }
728 ],
729 "geometries": [
730 { "id": 10, "type": "line", "points": [0, 1] },
731 { "id": 11, "type": "line", "points": [2, 3] }
732 ],
733 "constraints": [
734 { "id": 0, "type": "∠", "points": [0, 1, 2, 3], "value": 90.0 }
735 ]
736 }));
737 let labels = dimension_labels(&doc, &PlaneFrame::xy(), &no_offsets(), 0.05);
738 assert_eq!(labels.len(), 1);
739 assert_eq!(labels[0].text, "90.0°", "angle label is degrees + ° symbol");
740 }
741
742 #[test]
744 fn diameter_dim_doubles_the_stored_radius_in_the_text() {
745 let doc = doc_from(json!({
746 "points": [
747 { "id": 0, "x": 0.0, "y": 0.0 },
748 { "id": 1, "x": 3.0, "y": 0.0 }
749 ],
750 "geometries": [{ "id": 10, "type": "circle", "points": [0, 1] }],
751 "constraints": [
752 { "id": 0, "type": "⟺", "points": [0, 1], "value": 3.0, "displayStyle": "diameter" }
753 ]
754 }));
755 let labels = dimension_labels(&doc, &PlaneFrame::xy(), &no_offsets(), 0.05);
756 assert_eq!(labels.len(), 1);
757 assert_eq!(labels[0].mode, "diameter");
758 assert_eq!(labels[0].text, "⌀6.000");
759 let (pos, _) = dimension_leaders_buffers(&doc, &PlaneFrame::xy(), &no_offsets(), 0.05, &Default::default());
760 assert!(!pos.is_empty());
761 }
762
763 #[test]
766 fn saved_offset_places_the_label() {
767 let doc = doc_from(json!({
768 "points": [
769 { "id": 0, "x": 0.0, "y": 0.0 },
770 { "id": 1, "x": 2.0, "y": 0.0 }
771 ],
772 "geometries": [{ "id": 10, "type": "circle", "points": [0, 1] }],
773 "constraints": [
774 { "id": 7, "type": "⟺", "points": [0, 1], "value": 2.0, "displayStyle": "radius" }
775 ]
776 }));
777 let mut offsets = serde_json::Map::new();
778 offsets.insert("7".to_string(), json!({ "du": 5.0, "dv": 4.0 }));
779 let labels = dimension_labels(&doc, &PlaneFrame::xy(), &offsets, 0.05);
780 assert_eq!(labels.len(), 1);
781 assert!((labels[0].world[0] - 5.0).abs() < 1e-6);
782 assert!((labels[0].world[1] - 4.0).abs() < 1e-6);
783 }
784
785 #[test]
787 fn non_dimensional_constraints_are_skipped() {
788 let doc = doc_from(json!({
789 "points": [{ "id": 0, "x": 0.0, "y": 0.0 }, { "id": 1, "x": 1.0, "y": 0.0 }],
790 "geometries": [{ "id": 10, "type": "line", "points": [0, 1] }],
791 "constraints": [
792 { "id": 0, "type": "━", "points": [0, 1] },
793 { "id": 1, "type": "⏚", "points": [0] }
794 ]
795 }));
796 let (pos, _) = dimension_leaders_buffers(&doc, &PlaneFrame::xy(), &no_offsets(), 0.05, &Default::default());
797 assert!(pos.is_empty());
798 assert!(dimension_labels(&doc, &PlaneFrame::xy(), &no_offsets(), 0.05).is_empty());
799 }
800
801 #[test]
802 fn anchor_uv_is_the_midpoint_for_a_distance_dim() {
803 let doc = doc_from(json!({
804 "points": [{ "id": 0, "x": 0.0, "y": 0.0 }, { "id": 1, "x": 8.0, "y": 2.0 }],
805 "geometries": [{ "id": 10, "type": "line", "points": [0, 1] }],
806 "constraints": [{ "id": 0, "type": "⟺", "points": [0, 1], "value": 8.0 }]
807 }));
808 let c = &doc.constraints[0];
809 let anchor = dimension_anchor_uv(c, &doc).unwrap();
810 assert!((anchor[0] - 4.0).abs() < 1e-9 && (anchor[1] - 1.0).abs() < 1e-9);
811 }
812}