1use std::collections::HashMap;
9use std::f64::consts::PI;
10
11use serde_json::Value;
12
13use super::doc::{id_key, point_index, SketchConstraint, SketchDiagnostics, SketchDoc, SketchPoint};
14use super::PlaneFrame;
15use crate::style::SketchColors;
16
17pub const OVERLAY_DIM_LEADERS: &str = "sketch-dim-leaders";
20
21#[derive(Clone, Debug)]
23pub struct DimLabel {
24 pub id: Value,
26 pub text: String,
28 pub world: [f64; 3],
30 pub value: Option<f64>,
32 pub value_expr: Option<String>,
34 pub mode: &'static str,
36 pub conflicting: bool,
40}
41
42struct DimGeometry {
44 segments: Vec<([f64; 2], [f64; 2])>,
46 label_uv: [f64; 2],
48 text: String,
49 value: Option<f64>,
50 value_expr: Option<String>,
51 mode: &'static str,
52}
53
54fn is_radial_dimension(c: &SketchConstraint) -> bool {
57 if c.ctype() != Some("⟺") || c.points().len() < 2 {
58 return false;
59 }
60 matches!(
61 c.raw.get("displayStyle").and_then(Value::as_str),
62 Some("radius") | Some("diameter")
63 )
64}
65
66fn saved_offset(dim_offsets: &serde_json::Map<String, Value>, id: &Value) -> Option<[f64; 2]> {
69 let entry = dim_offsets.get(&id_key(id))?;
70 let du = entry.get("du").and_then(Value::as_f64);
71 let dv = entry.get("dv").and_then(Value::as_f64);
72 match (du, dv) {
73 (Some(du), Some(dv)) if du.is_finite() && dv.is_finite() => Some([du, dv]),
74 (Some(du), None) if du.is_finite() => Some([du, 0.0]),
76 (None, Some(dv)) if dv.is_finite() => Some([0.0, dv]),
77 _ => None,
78 }
79}
80
81fn linear_endpoints(
87 c: &SketchConstraint,
88 by_id: &HashMap<String, &SketchPoint>,
89) -> Option<([f64; 2], [f64; 2], [f64; 2])> {
90 let get = |id: &Value| by_id.get(&id_key(id)).copied();
91 let pts = c.points();
92 match c.ctype() {
93 Some("⟺") if pts.len() >= 2 && !is_radial_dimension(c) => {
94 let p0 = get(&pts[0])?;
95 let p1 = get(&pts[1])?;
96 let a = [p0.x, p0.y];
97 Some((a, [p1.x, p1.y], a))
98 }
99 Some("↥") if pts.len() >= 3 => {
100 let a = get(&pts[0])?;
101 let b = get(&pts[1])?;
102 let cpt = get(&pts[2])?;
103 let dx = b.x - a.x;
104 let dy = b.y - a.y;
105 let len_sq = dx * dx + dy * dy;
106 if !(len_sq > 1e-12) {
107 return Some(([a.x, a.y], [cpt.x, cpt.y], [a.x, a.y]));
108 }
109 let t_raw = ((cpt.x - a.x) * dx + (cpt.y - a.y) * dy) / len_sq;
110 let t_raw = if t_raw.is_finite() { t_raw } else { 0.0 };
111 let t_seg = t_raw.clamp(0.0, 1.0);
112 Some((
113 [a.x + t_raw * dx, a.y + t_raw * dy],
114 [cpt.x, cpt.y],
115 [a.x + t_seg * dx, a.y + t_seg * dy],
116 ))
117 }
118 _ => None,
119 }
120}
121
122fn radial_default_offset(pc: [f64; 2], pr: [f64; 2]) -> [f64; 2] {
125 let vx = pr[0] - pc[0];
126 let vy = pr[1] - pc[1];
127 let l = vx.hypot(vy);
128 let (rx, ry) = if l > 1e-9 { (vx / l, vy / l) } else { (1.0, 0.0) };
129 let (nx, ny) = (-ry, rx);
130 let base = (l * 0.35).max(0.2);
131 [
132 vx + rx * base + nx * base * 0.35,
133 vy + ry * base + ny * base * 0.35,
134 ]
135}
136
137fn push_arrow(
140 segments: &mut Vec<([f64; 2], [f64; 2])>,
141 tip: [f64; 2],
142 dx: f64,
143 dy: f64,
144 ah: f64,
145 s: f64,
146) {
147 let len = dx.hypot(dy);
148 if !(len > 1e-9) {
149 return;
150 }
151 let (tx, ty) = (dx / len, dy / len);
152 let (wx, wy) = (-ty, tx);
153 let a = [tip[0] + tx * ah + wx * ah * s, tip[1] + ty * ah + wy * ah * s];
154 let b = [tip[0] + tx * ah - wx * ah * s, tip[1] + ty * ah - wy * ah * s];
155 segments.push((tip, a));
156 segments.push((tip, b));
157}
158
159fn dim_geometry(
162 c: &SketchConstraint,
163 by_id: &HashMap<String, &SketchPoint>,
164 dim_offsets: &serde_json::Map<String, Value>,
165 wpp: f64,
166) -> Option<DimGeometry> {
167 let ctype = c.ctype()?;
168 let id = c.raw.get("id")?;
169 let value = c
170 .raw
171 .get("value")
172 .and_then(Value::as_f64)
173 .filter(|v| v.is_finite());
174 let value_expr = c
175 .raw
176 .get("valueExpr")
177 .and_then(Value::as_str)
178 .filter(|s| !s.is_empty())
179 .map(str::to_string);
180
181 let ah = (wpp * 6.0).max(0.06);
183 let arrow_s = 0.6;
184
185 if is_radial_dimension(c) {
186 let pts = c.points();
188 let pc = by_id.get(&id_key(&pts[0])).copied()?;
189 let pr = by_id.get(&id_key(&pts[1])).copied()?;
190 let center = [pc.x, pc.y];
191 let radius = (pr.x - pc.x).hypot(pr.y - pc.y);
192 if !(radius > 1e-9) {
193 return None;
194 }
195 let diameter = c.raw.get("displayStyle").and_then(Value::as_str) == Some("diameter");
196 let off = saved_offset(dim_offsets, id).unwrap_or_else(|| radial_default_offset(center, [pr.x, pr.y]));
197 let label_uv = [center[0] + off[0], center[1] + off[1]];
198
199 let (mut dx, mut dy) = (label_uv[0] - center[0], label_uv[1] - center[1]);
201 let mut l = dx.hypot(dy);
202 if !(l > 1e-9) {
203 dx = pr.x - pc.x;
204 dy = pr.y - pc.y;
205 l = dx.hypot(dy).max(1e-9);
206 }
207 let (ux, uy) = (dx / l, dy / l);
208 let near = [center[0] + ux * radius, center[1] + uy * radius];
209 let far = [center[0] - ux * radius, center[1] - uy * radius];
210
211 let mut segments = Vec::new();
212 if diameter {
213 segments.push((far, near));
214 segments.push((near, label_uv));
215 push_arrow(&mut segments, near, -ux, -uy, ah, arrow_s);
217 push_arrow(&mut segments, far, ux, uy, ah, arrow_s);
218 } else {
219 segments.push((center, near));
220 segments.push((near, label_uv));
221 push_arrow(&mut segments, near, -ux, -uy, ah, arrow_s);
222 }
223
224 let safe = value.unwrap_or(0.0);
225 let (text, mode) = if diameter {
226 (format!("⌀{:.3}", 2.0 * safe), "diameter")
227 } else {
228 (format!("R{safe:.3}"), "radius")
229 };
230 return Some(DimGeometry {
231 segments,
232 label_uv,
233 text,
234 value,
235 value_expr,
236 mode,
237 });
238 }
239
240 if ctype == "⟺" || ctype == "↥" {
241 let (a, b, anchor_a) = linear_endpoints(c, by_id)?;
243 let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
244 let len = dx.hypot(dy).max(1e-9);
245 let (tx, ty) = (dx / len, dy / len);
246 let (nx, ny) = (-ty, tx);
247 let base = (wpp * 20.0).max(0.1);
248 let mid = [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0];
249 let off = saved_offset(dim_offsets, id).unwrap_or([nx * base, ny * base]);
251 let label_uv = [mid[0] + off[0], mid[1] + off[1]];
252 let off_n = off[0] * nx + off[1] * ny;
256 let ou = nx * off_n;
257 let ov = ny * off_n;
258
259 let a_off = [a[0] + ou, a[1] + ov];
260 let b_off = [b[0] + ou, b[1] + ov];
261 let mut segments = vec![
262 (a_off, b_off), (anchor_a, a_off), (b, b_off), ];
266 push_arrow(&mut segments, a_off, tx, ty, ah, arrow_s);
268 push_arrow(&mut segments, b_off, -tx, -ty, ah, arrow_s);
269
270 let t_lbl = (label_uv[0] - a_off[0]) * tx + (label_uv[1] - a_off[1]) * ty;
276 let foot = {
277 let t = t_lbl.clamp(0.0, len);
278 [a_off[0] + tx * t, a_off[1] + ty * t]
279 };
280 if (label_uv[0] - foot[0]).hypot(label_uv[1] - foot[1]) > wpp * 2.0 {
281 segments.push((foot, label_uv));
282 }
283
284 let text = format!("{:.3}", value.unwrap_or(0.0));
285 return Some(DimGeometry {
286 segments,
287 label_uv,
288 text,
289 value,
290 value_expr,
291 mode: "distance",
292 });
293 }
294
295 if ctype == "∠" && c.points().len() >= 4 {
296 let pts = c.points();
298 let get = |i: usize| by_id.get(&id_key(&pts[i])).copied();
299 let (p0, p1, p2, p3) = (get(0)?, get(1)?, get(2)?, get(3)?);
300 let inter = line_intersection([p0.x, p0.y], [p1.x, p1.y], [p2.x, p2.y], [p3.x, p3.y]);
301 let (d1x, d1y) = (p1.x - p0.x, p1.y - p0.y);
302 let (d2x, d2y) = (p3.x - p2.x, p3.y - p2.y);
303 if (d1x * d1x + d1y * d1y) < 1e-12 || (d2x * d2x + d2y * d2y) < 1e-12 {
304 return None;
305 }
306 let a0 = d1y.atan2(d1x);
307 let a1 = d2y.atan2(d2x);
308 let mut signed = a1 - a0;
309 while signed <= -PI {
310 signed += 2.0 * PI;
311 }
312 while signed > PI {
313 signed -= 2.0 * PI;
314 }
315 let default_deg = (a1 - a0).to_degrees().rem_euclid(360.0);
316 let mut target_deg = match value {
317 Some(v) => {
318 let abs = v.abs();
319 let mut t = abs % 360.0;
320 if t < 1e-6 && abs > 0.0 {
321 t = 360.0;
322 }
323 t
324 }
325 None => default_deg,
326 };
327 if target_deg < 1e-6 {
328 target_deg = 1e-6;
329 }
330 let mut dir_sign = if signed == 0.0 { 1.0 } else { signed.signum() };
331 if target_deg > 180.0 && target_deg < 360.0 - 1e-6 {
332 dir_sign = -dir_sign;
333 }
334 let mut d = target_deg.to_radians() * dir_sign;
335 let two_pi = 2.0 * PI;
336 if d.abs() > two_pi {
337 d = d.signum() * (two_pi - 1e-6);
338 }
339
340 let base_r = (wpp * 24.0).max(0.3);
341 let off = saved_offset(dim_offsets, id).unwrap_or([0.0, 0.0]);
342 let r = base_r + off[0].hypot(off[1]);
343 let (cx, cy) = (inter[0], inter[1]);
344
345 let mut a_start = a0;
347 if off[0] != 0.0 || off[1] != 0.0 {
348 let label_ang = off[1].atan2(off[0]);
349 let norm = |a: f64| {
350 let t = two_pi;
351 let m = a % t;
352 if m < 0.0 {
353 m + t
354 } else {
355 m
356 }
357 };
358 let ang_diff = |a: f64, b: f64| {
359 let mut x = norm(a - b);
360 if x > PI {
361 x = two_pi - x;
362 }
363 x.abs()
364 };
365 let bisector = a_start + d * 0.5;
366 let bisector_opp = bisector + PI;
367 if ang_diff(label_ang, bisector_opp) + 1e-6 < ang_diff(label_ang, bisector) {
368 a_start += PI;
369 }
370 }
371
372 let segs = 48usize;
374 let mut segments = Vec::with_capacity(segs + 4);
375 let mut prev = [cx + a_start.cos() * r, cy + a_start.sin() * r];
376 for i in 1..=segs {
377 let t = a_start + d * (i as f64 / segs as f64);
378 let cur = [cx + t.cos() * r, cy + t.sin() * r];
379 segments.push((prev, cur));
380 prev = cur;
381 }
382 let dir_start = if d >= 0.0 { 1.0 } else { -1.0 };
384 let add_arc_arrow = |segments: &mut Vec<([f64; 2], [f64; 2])>, t: f64, dir: f64| {
385 let tip = [cx + t.cos() * r, cy + t.sin() * r];
386 let (tx, ty) = (-t.sin() * dir, t.cos() * dir);
387 push_arrow(segments, tip, tx, ty, ah, arrow_s);
388 };
389 add_arc_arrow(&mut segments, a_start, dir_start);
390 add_arc_arrow(&mut segments, a_start + d, -dir_start);
391
392 let mid_ang = a_start + d * 0.5;
394 let label_uv = [cx + mid_ang.cos() * r, cy + mid_ang.sin() * r];
395
396 let text = format!("{:.1}°", value.unwrap_or(default_deg));
399 return Some(DimGeometry {
400 segments,
401 label_uv,
402 text,
403 value,
404 value_expr,
405 mode: "angle",
406 });
407 }
408
409 None
410}
411
412pub(super) fn line_intersection(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> [f64; 2] {
415 let r = [b[0] - a[0], b[1] - a[1]];
416 let s = [d[0] - c[0], d[1] - c[1]];
417 let rxs = r[0] * s[1] - r[1] * s[0];
418 if rxs.abs() < 1e-12 {
419 return a;
420 }
421 let t = ((c[0] - a[0]) * s[1] - (c[1] - a[1]) * s[0]) / rxs;
422 [a[0] + t * r[0], a[1] + t * r[1]]
423}
424
425pub fn constraint_dim_segments(
431 c: &SketchConstraint,
432 doc: &SketchDoc,
433 dim_offsets: &serde_json::Map<String, Value>,
434 world_per_pixel: f64,
435) -> Option<Vec<([f64; 2], [f64; 2])>> {
436 let by_id = point_index(doc);
437 dim_geometry(c, &by_id, dim_offsets, world_per_pixel).map(|g| g.segments)
438}
439
440pub fn dimension_leaders_buffers(
444 doc: &SketchDoc,
445 diag: &SketchDiagnostics,
446 plane: &PlaneFrame,
447 dim_offsets: &serde_json::Map<String, Value>,
448 world_per_pixel: f64,
449 colors: &SketchColors,
450) -> (Vec<f32>, Vec<f32>) {
451 dimension_leaders_buffers_with_state(
452 doc,
453 diag,
454 plane,
455 dim_offsets,
456 world_per_pixel,
457 colors,
458 None,
459 &[],
460 )
461}
462
463pub fn dimension_leaders_buffers_with_state(
468 doc: &SketchDoc,
469 diag: &SketchDiagnostics,
470 plane: &PlaneFrame,
471 dim_offsets: &serde_json::Map<String, Value>,
472 world_per_pixel: f64,
473 colors: &SketchColors,
474 hovered: Option<&Value>,
475 selection: &[Value],
476) -> (Vec<f32>, Vec<f32>) {
477 let by_id = point_index(doc);
478 let mut positions: Vec<f32> = Vec::new();
479 let mut color_buf: Vec<f32> = Vec::new();
480 for c in &doc.constraints {
481 let Some(geom) = dim_geometry(c, &by_id, dim_offsets, world_per_pixel) else {
482 continue;
483 };
484 let color = match c.raw.get("id") {
487 Some(id) => super::tessellate::interaction_color(
488 colors,
489 super::constraint_base_color(colors, diag, id),
490 hovered,
491 selection,
492 "constraint",
493 id,
494 ),
495 None => colors.constraint,
496 };
497 let rgb = crate::color::hex_to_srgb_f32(color);
498 for (a, b) in geom.segments {
499 let wa = plane.to_world(a[0], a[1]);
500 let wb = plane.to_world(b[0], b[1]);
501 positions.extend_from_slice(&[
502 wa[0] as f32,
503 wa[1] as f32,
504 wa[2] as f32,
505 wb[0] as f32,
506 wb[1] as f32,
507 wb[2] as f32,
508 ]);
509 color_buf.extend_from_slice(&[rgb[0], rgb[1], rgb[2], rgb[0], rgb[1], rgb[2]]);
510 }
511 }
512 (positions, color_buf)
513}
514
515pub fn dimension_leaders_overlay_json(
518 doc: &SketchDoc,
519 diag: &SketchDiagnostics,
520 plane: &PlaneFrame,
521 dim_offsets: &serde_json::Map<String, Value>,
522 world_per_pixel: f64,
523 colors: &SketchColors,
524) -> String {
525 dimension_leaders_overlay_json_with_state(
526 doc,
527 diag,
528 plane,
529 dim_offsets,
530 world_per_pixel,
531 colors,
532 None,
533 &[],
534 )
535}
536
537pub fn dimension_leaders_overlay_json_with_state(
540 doc: &SketchDoc,
541 diag: &SketchDiagnostics,
542 plane: &PlaneFrame,
543 dim_offsets: &serde_json::Map<String, Value>,
544 world_per_pixel: f64,
545 colors: &SketchColors,
546 hovered: Option<&Value>,
547 selection: &[Value],
548) -> String {
549 let (positions, color_buf) = dimension_leaders_buffers_with_state(
550 doc,
551 diag,
552 plane,
553 dim_offsets,
554 world_per_pixel,
555 colors,
556 hovered,
557 selection,
558 );
559 serde_json::json!({
560 "groups": [
561 {
562 "name": OVERLAY_DIM_LEADERS,
563 "renderOrder": 10003,
564 "lines": { "positions": positions, "colors": color_buf },
565 }
566 ]
567 })
568 .to_string()
569}
570
571pub fn dimension_labels(
574 doc: &SketchDoc,
575 diag: &SketchDiagnostics,
576 plane: &PlaneFrame,
577 dim_offsets: &serde_json::Map<String, Value>,
578 world_per_pixel: f64,
579) -> Vec<DimLabel> {
580 let by_id = point_index(doc);
581 let mut out = Vec::new();
582 for c in &doc.constraints {
583 let Some(geom) = dim_geometry(c, &by_id, dim_offsets, world_per_pixel) else {
584 continue;
585 };
586 let Some(id) = c.raw.get("id").cloned() else {
587 continue;
588 };
589 out.push(DimLabel {
590 conflicting: diag.constraint_conflicting(&id),
591 id,
592 text: geom.text,
593 world: plane.to_world(geom.label_uv[0], geom.label_uv[1]),
594 value: geom.value,
595 value_expr: geom.value_expr,
596 mode: geom.mode,
597 });
598 }
599 out
600}
601
602pub fn dimension_anchor_uv(
607 c: &SketchConstraint,
608 doc: &SketchDoc,
609) -> Option<[f64; 2]> {
610 let by_id = point_index(doc);
611 let get = |id: &Value| by_id.get(&id_key(id)).copied();
612 if is_radial_dimension(c) {
613 let pc = get(&c.points()[0])?;
614 return Some([pc.x, pc.y]);
615 }
616 match c.ctype() {
617 Some("⟺") | Some("↥") => {
618 let (a, b, _) = linear_endpoints(c, &by_id)?;
619 Some([(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0])
620 }
621 Some("∠") if c.points().len() >= 4 => {
622 let pts = c.points();
623 let g = |i: usize| get(&pts[i]);
624 let (p0, p1, p2, p3) = (g(0)?, g(1)?, g(2)?, g(3)?);
625 Some(line_intersection(
626 [p0.x, p0.y],
627 [p1.x, p1.y],
628 [p2.x, p2.y],
629 [p3.x, p3.y],
630 ))
631 }
632 _ => None,
633 }
634}
635
636