1use geop_core_math::{
13 geop_error::{GeopError, GeopResult},
14 scalars::Scalar,
15 vector::Vector3,
16};
17use geop_core_program::{placed::Located, reader::Reader};
18use geop_core_sketch::{
19 dimension::Measure,
20 plain::{add, cross, distance, dot, perp, scale, sub, unit},
21};
22use geop_core_topology::EdgeId;
23use geop_lib_sheet::{Anchor, Label, Layer, P, Shape, Sheet};
24use geop_ops_model::RefId;
25use serde::{Deserialize, Serialize};
26
27use crate::{
28 drawing::{
29 ARROW_LENGTH, ARROW_WIDTH, CENTER_OVERSHOOT, Layout, Placed, Placement, TEXT, arc_of,
30 length_label, placed_edge, placed_vertex,
31 },
32 layer,
33 scene::Scene,
34 view::{DrawnView, ViewFrame},
35};
36
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum Along {
41 #[default]
43 Aligned,
44 Horizontal,
45 Vertical,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(tag = "type", rename_all = "snake_case")]
51pub enum Target {
52 Vertex {
53 name: String,
54 },
55 Center {
57 edge: String,
58 },
59 Edge {
61 name: String,
62 },
63}
64
65impl std::fmt::Display for Target {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 Target::Vertex { name } | Target::Edge { name } => f.write_str(name),
69 Target::Center { edge } => write!(f, "the centre of {edge}"),
70 }
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
76pub struct Leader {
77 pub view: DrawnView,
78 pub target: Target,
79}
80
81#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
87#[serde(tag = "type", rename_all = "snake_case")]
88pub enum Annotation {
89 Distance {
92 view: DrawnView,
93 from: Target,
94 to: Target,
95 #[serde(default)]
96 along: Along,
97 label: [f64; 2],
98 },
99 Radius {
102 view: DrawnView,
103 edge: String,
104 #[serde(default)]
105 diameter: bool,
106 label: [f64; 2],
107 },
108 Angle {
111 view: DrawnView,
112 a: String,
113 b: String,
114 label: [f64; 2],
115 },
116 Note {
118 text: String,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 leader: Option<Leader>,
121 label: [f64; 2],
122 },
123 CenterMark { view: DrawnView, edge: String },
125}
126
127#[derive(Clone, Debug, PartialEq)]
130pub struct Drawn {
131 pub lines: Vec<(Layer, Vec<P>)>,
132 pub arrows: Vec<[P; 3]>,
134 pub text: Option<Text>,
135 pub label: P,
137 pub anchor: P,
139}
140
141#[derive(Clone, Debug, PartialEq)]
144pub struct Text {
145 pub text: String,
146 pub at: P,
147 pub angle: f64,
148}
149
150impl Drawn {
151 pub(crate) fn measure(measure: Measure, label: P, text: String) -> Drawn {
156 let at = match measure {
157 Measure::Linear { along, .. } => {
158 let mut u = unit(along).unwrap_or([1.0, 0.0]);
159 if u[0] < 0.0 || (u[0] == 0.0 && u[1] < 0.0) {
160 u = scale(u, -1.0);
161 }
162 Text {
163 text,
164 at: add(label, perp(u)),
165 angle: u[1].atan2(u[0]).to_degrees(),
166 }
167 }
168 _ => Text {
169 text,
170 at: add(label, [0.0, 1.0]),
171 angle: 0.0,
172 },
173 };
174 Drawn {
175 lines: measure
176 .lines(label)
177 .into_iter()
178 .map(|line| (layer::DIMENSION, line))
179 .collect(),
180 arrows: measure
181 .arrows(label)
182 .into_iter()
183 .map(|(tip, d)| arrowhead(tip, d))
184 .collect(),
185 text: Some(at),
186 label,
187 anchor: label,
188 }
189 }
190
191 pub fn draw(&self, sheet: &mut Sheet) {
193 for (layer, line) in &self.lines {
194 let shape = match line[..] {
195 [a, b] => Shape::Line(a, b),
196 _ => Shape::Polyline(line.clone()),
197 };
198 sheet.stroke(*layer, shape);
199 }
200 for arrow in &self.arrows {
201 sheet.stroke(layer::DIMENSION, Shape::Filled(arrow.to_vec()));
202 }
203 if let Some(text) = &self.text {
204 sheet.labels.push(Label {
205 layer: layer::DIMENSION,
206 at: text.at,
207 height: TEXT,
208 angle: text.angle,
209 anchor: Anchor::Middle,
210 text: text.text.clone(),
211 });
212 }
213 }
214}
215
216fn arrowhead(tip: P, d: P) -> [P; 3] {
218 let base = sub(tip, scale(d, ARROW_LENGTH));
219 let side = scale(perp(d), ARROW_WIDTH / 2.0);
220 [tip, add(base, side), sub(base, side)]
221}
222
223impl Annotation {
224 pub fn view(&self) -> Option<DrawnView> {
226 match self {
227 Annotation::Distance { view, .. }
228 | Annotation::Radius { view, .. }
229 | Annotation::Angle { view, .. }
230 | Annotation::CenterMark { view, .. } => Some(*view),
231 Annotation::Note { leader, .. } => leader.as_ref().map(|l| l.view),
232 }
233 }
234
235 pub fn label_mut(&mut self) -> Option<&mut [f64; 2]> {
238 match self {
239 Annotation::Distance { label, .. }
240 | Annotation::Radius { label, .. }
241 | Annotation::Angle { label, .. }
242 | Annotation::Note { label, .. } => Some(label),
243 Annotation::CenterMark { .. } => None,
244 }
245 }
246
247 pub fn kind(&self) -> &'static str {
249 match self {
250 Annotation::Distance { along, .. } => match along {
251 Along::Aligned => "Distance",
252 Along::Horizontal => "Horizontal distance",
253 Along::Vertical => "Vertical distance",
254 },
255 Annotation::Radius {
256 diameter: false, ..
257 } => "Radius",
258 Annotation::Radius { diameter: true, .. } => "Diameter",
259 Annotation::Angle { .. } => "Angle",
260 Annotation::Note { .. } => "Note",
261 Annotation::CenterMark { .. } => "Centre mark",
262 }
263 }
264
265 pub fn describe(&self) -> String {
267 let kind = self.kind();
268 match self {
269 Annotation::Distance { view, from, to, .. } => {
270 format!("{kind} from {from} to {to} in the {view} view")
271 }
272 Annotation::Radius { view, edge, .. } | Annotation::CenterMark { view, edge } => {
273 format!("{kind} of {edge} in the {view} view")
274 }
275 Annotation::Angle { view, a, b, .. } => {
276 format!("{kind} between {a} and {b} in the {view} view")
277 }
278 Annotation::Note { text, leader, .. } => match leader {
279 Some(Leader { view, target }) => {
280 format!("{kind} {text:?} pointing at {target} in the {view} view")
281 }
282 None => format!("{kind} {text:?}"),
283 },
284 }
285 }
286
287 pub fn drawn(&self, part: &Reader, layout: &Layout) -> GeopResult<Drawn> {
292 let paper_scale = layout.scale;
293 match self {
294 Annotation::Distance {
295 view,
296 from,
297 to,
298 along,
299 label,
300 } => {
301 let (p, frame) = measuring(layout, *view)?;
302 let (a, b) = (paper(part, &frame, from)?, paper(part, &frame, to)?);
303 let (direction, value) = match along {
304 Along::Aligned => (sub(b, a), distance(a, b)),
305 Along::Horizontal => ([1.0, 0.0], (b[0] - a[0]).abs()),
306 Along::Vertical => ([0.0, 1.0], (b[1] - a[1]).abs()),
307 };
308 if value == 0.0 {
309 return Err(GeopError::new(format!(
310 "{from} and {to} are no distance apart in the {view} view"
311 )));
312 }
313 let (a, b) = (p.place(paper_scale, a), p.place(paper_scale, b));
314 let anchor = middle(a, b);
315 let mut drawn = Drawn::measure(
316 Measure::Linear {
317 a,
318 b,
319 along: direction,
320 },
321 add(anchor, *label),
322 length_label(value),
323 );
324 drawn.anchor = anchor;
325 Ok(drawn)
326 }
327 Annotation::Radius {
328 view,
329 edge,
330 diameter,
331 label,
332 } => {
333 let (p, frame) = measuring(layout, *view)?;
334 let (center, radius) = round(part, p, &frame, edge, paper_scale)?;
335 let text = match diameter {
336 true => format!("⌀{}", length_label(2.0 * radius)),
337 false => format!("R{}", length_label(radius)),
338 };
339 let measure = Measure::Radial {
340 center,
341 radius: radius * paper_scale,
342 diameter: *diameter,
343 };
344 let mut drawn = Drawn::measure(measure, add(center, *label), text);
345 drawn.anchor = center;
346 Ok(drawn)
347 }
348 Annotation::Angle { view, a, b, label } => {
349 let (p, frame) = measuring(layout, *view)?;
350 let side = |edge: &str| -> GeopResult<(P, P)> {
351 let curve = placed_edge(part, edge)?;
352 if curve.as_line()?.is_none() {
353 return Err(GeopError::new(format!(
354 "the edge {edge} is not straight: only straight edges make an angle"
355 )));
356 }
357 let (t0, t1) = curve.domain();
358 let on = |t| -> GeopResult<P> {
359 let q = frame.project_point(&curve.evaluate(t)?);
360 Ok(p.place(paper_scale, [q[0].to_f64(), q[1].to_f64()]))
361 };
362 Ok((on(t0)?, on(t1)?))
363 };
364 let ((a0, a1), (b0, b1)) = (side(a)?, side(b)?);
365 let (da, db) = (sub(a1, a0), sub(b1, b0));
366 let denom = cross(da, db);
367 if denom == 0.0 {
368 return Err(GeopError::new(format!(
369 "{a} and {b} are parallel in the {view} view: they make no angle"
370 )));
371 }
372 let vertex = add(a0, scale(da, cross(sub(b0, a0), db) / denom));
373 let label = add(vertex, *label);
374 let (from, to, sweep) = sector(da, db, sub(label, vertex));
375 let mut drawn = Drawn::measure(
376 Measure::Angular {
377 vertex,
378 from,
379 to,
380 sweep,
381 },
382 label,
383 format!("{}°", length_label(sweep.to_degrees())),
384 );
385 drawn.anchor = vertex;
386 Ok(drawn)
387 }
388 Annotation::Note {
389 text,
390 leader,
391 label,
392 } => {
393 let anchor = match leader {
394 Some(Leader { view, target }) => {
395 let p = layout.placement(*view)?;
396 p.place(paper_scale, paper(part, &frame_of(*view)?, target)?)
397 }
398 None => [0.0, 0.0],
399 };
400 let at = add(anchor, *label);
401 let mut drawn = Drawn {
402 lines: Vec::new(),
403 arrows: Vec::new(),
404 text: Some(Text {
405 text: text.clone(),
406 at: add(at, [0.0, 1.0]),
407 angle: 0.0,
408 }),
409 label: at,
410 anchor,
411 };
412 if leader.is_some()
413 && let Some(d) = unit(sub(anchor, at))
414 {
415 drawn.lines.push((layer::DIMENSION, vec![at, anchor]));
416 drawn.arrows.push(arrowhead(anchor, d));
417 }
418 Ok(drawn)
419 }
420 Annotation::CenterMark { view, edge } => {
421 let (p, frame) = measuring(layout, *view)?;
422 let (c, radius) = round(part, p, &frame, edge, paper_scale)?;
423 let reach = radius * paper_scale + CENTER_OVERSHOOT;
424 Ok(Drawn {
425 lines: vec![
426 (
427 layer::CENTER,
428 vec![[c[0] - reach, c[1]], [c[0] + reach, c[1]]],
429 ),
430 (
431 layer::CENTER,
432 vec![[c[0], c[1] - reach], [c[0], c[1] + reach]],
433 ),
434 ],
435 arrows: Vec::new(),
436 text: None,
437 label: c,
438 anchor: c,
439 })
440 }
441 }
442 }
443}
444
445fn middle(a: P, b: P) -> P {
447 scale(add(a, b), 0.5)
448}
449
450fn frame_of(view: DrawnView) -> GeopResult<ViewFrame> {
453 match view {
454 DrawnView::View(kind) => kind.frame(),
455 DrawnView::Section => Err(GeopError::new(
456 "the section view cannot be annotated: annotate the views of the whole part",
457 )),
458 }
459}
460
461fn measuring(layout: &Layout, view: DrawnView) -> GeopResult<(&Placement, ViewFrame)> {
465 if !view.orthographic() {
466 return Err(GeopError::new(
467 "the isometric view foreshortens every length and angle: dimension in another view",
468 ));
469 }
470 Ok((layout.placement(view)?, frame_of(view)?))
471}
472
473fn paper(part: &Reader, frame: &ViewFrame, target: &Target) -> GeopResult<P> {
476 let point = match target {
477 Target::Vertex { name } => placed_vertex(part, name)?,
478 Target::Center { edge } => {
479 arc_of(&placed_edge(part, edge)?)?
480 .ok_or_else(|| GeopError::new(format!("the edge {edge} is not circular")))?
481 .circle
482 .center
483 }
484 Target::Edge { name } => {
485 let curve = placed_edge(part, name)?;
486 let (t0, t1) = curve.domain();
487 curve.evaluate(t0.add(t1).mul(Scalar::from_f64(0.5)).sharpen())?
489 }
490 };
491 let q = frame.project_point(&point);
492 Ok([q[0].to_f64(), q[1].to_f64()])
493}
494
495fn round(
499 part: &Reader,
500 p: &Placement,
501 frame: &ViewFrame,
502 edge: &str,
503 scale: f64,
504) -> GeopResult<(P, f64)> {
505 let arc = arc_of(&placed_edge(part, edge)?)?.ok_or_else(|| {
506 GeopError::new(format!(
507 "the edge {edge} is not circular: only a circle has a radius"
508 ))
509 })?;
510 if !seen_round(frame, &arc.circle.normal) {
511 return Err(GeopError::new(format!(
512 "the {} view does not see the circle {edge} round: dimension it in a view along its axis",
513 p.view
514 )));
515 }
516 let c = frame.project_point(&arc.circle.center);
517 Ok((
518 p.place(scale, [c[0].to_f64(), c[1].to_f64()]),
519 arc.circle.radius.to_f64(),
520 ))
521}
522
523fn seen_round(frame: &ViewFrame, normal: &Vector3) -> bool {
525 frame
526 .direction
527 .vector()
528 .prod_cross(normal)
529 .could_be_equal(&Vector3::zero())
530}
531
532fn sector(da: P, db: P, towards: P) -> (P, P, f64) {
536 let ccw = |from: P, to: P| {
537 let a = cross(from, to).atan2(dot(from, to));
538 if a < 0.0 {
539 a + std::f64::consts::TAU
540 } else {
541 a
542 }
543 };
544 let mut first = None;
545 for (sa, sb) in [(1.0, 1.0), (1.0, -1.0), (-1.0, 1.0), (-1.0, -1.0)] {
546 let (mut from, mut to) = (scale(da, sa), scale(db, sb));
547 let mut sweep = ccw(from, to);
548 if sweep > std::f64::consts::PI {
549 (from, to) = (to, from);
550 sweep = std::f64::consts::TAU - sweep;
551 }
552 first.get_or_insert((from, to, sweep));
553 if ccw(from, towards) <= sweep {
554 return (from, to, sweep);
555 }
556 }
557 first.expect("four sides tried")
558}
559
560#[derive(Clone, Debug, PartialEq)]
562pub struct Candidate {
563 pub view: DrawnView,
564 pub what: Pickable,
565}
566
567#[derive(Clone, Debug, PartialEq)]
569pub enum Pickable {
570 Point { target: Target, at: P },
572 Edge {
574 name: String,
575 points: Vec<P>,
576 shape: EdgeShape,
577 },
578}
579
580#[derive(Clone, Debug, PartialEq)]
582pub enum EdgeShape {
583 Line {
585 start: String,
586 end: String,
587 },
588 Circle {
591 full: bool,
592 },
593 Other,
594}
595
596const POINTS_PER_PIECE: usize = 12;
598
599pub(crate) fn candidates(
605 scene: &Scene<'_>,
606 placed: &[Placed],
607 scale: f64,
608) -> GeopResult<Vec<Candidate>> {
609 let mut out: Vec<Candidate> = Vec::new();
610 for p in placed
611 .iter()
612 .filter(|p| matches!(p.kind, DrawnView::View(_)))
613 {
614 let placement = Placement::of(p);
615 let place = |q: P| placement.place(scale, q);
616 let frame = &p.view.frame;
617 let on_sheet = |q: &Vector3| {
618 let v = frame.project_point(q);
619 place([v[0].to_f64(), v[1].to_f64()])
620 };
621 type Pieces = Vec<Vec<P>>;
623 let mut edges: Vec<((usize, EdgeId), Pieces)> = Vec::new();
624 for line in &p.view.lines {
625 let Some(edge) = line.edge else { continue };
626 let (t0, t1) = line.curve.domain();
627 let mut points = Vec::with_capacity(POINTS_PER_PIECE + 1);
628 for k in 0..=POINTS_PER_PIECE {
629 let t = match k {
631 0 => t0,
632 POINTS_PER_PIECE => t1,
633 k => {
634 let f = Scalar::from_f64(k as f64 / POINTS_PER_PIECE as f64);
635 Scalar::fraction(t0, t1, f)
636 }
637 };
638 let q = line.curve.evaluate(t)?;
639 points.push(place([q[0].to_f64(), q[1].to_f64()]));
640 }
641 let key = (line.body, edge);
642 match edges.iter_mut().find(|(k, _)| *k == key) {
643 Some((_, pieces)) => pieces.push(points),
644 None => edges.push((key, vec![points])),
645 }
646 }
647 let add_point = |out: &mut Vec<Candidate>, target: Target, at: P| {
648 let known = out.iter().any(|c| {
649 c.view == p.kind
650 && matches!(&c.what, Pickable::Point { target: t, .. } if *t == target)
651 });
652 if !known {
653 out.push(Candidate {
654 view: p.kind,
655 what: Pickable::Point { target, at },
656 });
657 }
658 };
659 for ((body, edge), pieces) in edges {
660 let b = &scene.bodies[body];
661 let model = b.model();
662 let named = |id: RefId| {
663 (b.named.name_of(id)).map(|n| Located::in_instance(b.path.as_str(), n).key())
664 };
665 let Some(name) = named(edge.into()) else {
666 continue;
667 };
668 let e = model.get_edge(edge)?;
669 let world = |q: Vector3| match &b.pose {
670 Some(pose) => pose.apply(&q),
671 None => q,
672 };
673 let ends = [e.start_vertex, e.end_vertex].map(|v| named(v.into()));
674 for (v, end) in [e.start_vertex, e.end_vertex].into_iter().zip(&ends) {
675 if let Some(end) = end {
676 let at = on_sheet(&world(model.get_vertex(v)?.point));
677 add_point(&mut out, Target::Vertex { name: end.clone() }, at);
678 }
679 }
680 let curve = match &b.pose {
681 Some(pose) => e.curve.transform(&pose.motion()),
682 None => e.curve.clone(),
683 };
684 let shape = if curve.as_line()?.is_some() {
685 match ends {
686 [Some(start), Some(end)] => EdgeShape::Line { start, end },
687 _ => EdgeShape::Other,
688 }
689 } else if let Some(arc) = arc_of(&curve)?
690 && seen_round(frame, &arc.circle.normal)
691 {
692 let target = Target::Center { edge: name.clone() };
693 add_point(&mut out, target, on_sheet(&arc.circle.center));
694 EdgeShape::Circle {
695 full: e.start_vertex == e.end_vertex,
696 }
697 } else {
698 EdgeShape::Other
699 };
700 for points in pieces {
701 out.push(Candidate {
702 view: p.kind,
703 what: Pickable::Edge {
704 name: name.clone(),
705 points,
706 shape: shape.clone(),
707 },
708 });
709 }
710 }
711 }
712 Ok(out)
713}