Skip to main content

geop_ops_drawing/
annotation.rs

1//! What is added on a drawing's sheet by hand: dimensions between what its
2//! views show, notes and centre marks ([`Annotation`]) — and what in a view
3//! can be picked to add them ([`Candidate`]).
4//!
5//! An annotation refers to what it measures by the entities' names and the
6//! view's, so it follows the part through every rebuild, and its value is
7//! measured anew each time: a drawing changes nothing, it only annotates.
8//! Its value is placed where the designer dragged it, as an offset on the
9//! paper from what it measures, and drawn the way a sketch draws its
10//! dimensions (see [`geop_core_sketch::dimension`]).
11
12use 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/// Which way a distance is measured on the paper.
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum Along {
41    /// Along the line between its points.
42    #[default]
43    Aligned,
44    Horizontal,
45    Vertical,
46}
47
48/// A point of a view, by the entity that defines it.
49#[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    /// The centre of a circular edge.
56    Center {
57        edge: String,
58    },
59    /// The middle of an edge: where a note's leader points at it.
60    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/// Where a note's leader points: a point of a view.
75#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
76pub struct Leader {
77    pub view: DrawnView,
78    pub target: Target,
79}
80
81/// A dimension, a note or a centre mark added on the sheet, in one of its
82/// views. `label` is where its value or text goes: on the paper, in
83/// millimetres, from the point it is placed from — the middle between a
84/// distance's points, a circle's centre, an angle's vertex, a leader's
85/// target — or, for a note without a leader, from the sheet's corner.
86#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
87#[serde(tag = "type", rename_all = "snake_case")]
88pub enum Annotation {
89    /// The distance between two points as `view` shows them — along the
90    /// line between them, or horizontally or vertically on the paper.
91    Distance {
92        view: DrawnView,
93        from: Target,
94        to: Target,
95        #[serde(default)]
96        along: Along,
97        label: [f64; 2],
98    },
99    /// The radius of a circular edge `view` sees round — or, `diameter`,
100    /// its diameter.
101    Radius {
102        view: DrawnView,
103        edge: String,
104        #[serde(default)]
105        diameter: bool,
106        label: [f64; 2],
107    },
108    /// The angle between two straight edges, as `view` shows them: the one
109    /// of the four between their lines that `label` is in.
110    Angle {
111        view: DrawnView,
112        a: String,
113        b: String,
114        label: [f64; 2],
115    },
116    /// Text, with a leader to a point of a view if it has one.
117    Note {
118        text: String,
119        #[serde(default, skip_serializing_if = "Option::is_none")]
120        leader: Option<Leader>,
121        label: [f64; 2],
122    },
123    /// Two centre lines across a circular edge `view` sees round.
124    CenterMark { view: DrawnView, edge: String },
125}
126
127/// An annotation as it is drawn on the sheet: its lines, each on its
128/// layer, its arrowheads, and its text.
129#[derive(Clone, Debug, PartialEq)]
130pub struct Drawn {
131    pub lines: Vec<(Layer, Vec<P>)>,
132    /// Filled triangles.
133    pub arrows: Vec<[P; 3]>,
134    pub text: Option<Text>,
135    /// Where its value is shown, and dragged by.
136    pub label: P,
137    /// The point it is placed from (see [`Annotation`]).
138    pub anchor: P,
139}
140
141/// A line of text on the sheet, the middle of its baseline at `at`, turned
142/// `angle` degrees counter-clockwise.
143#[derive(Clone, Debug, PartialEq)]
144pub struct Text {
145    pub text: String,
146    pub at: P,
147    pub angle: f64,
148}
149
150impl Drawn {
151    /// The dimension `measure`, its value `text` shown at `label`, as a
152    /// sketch draws one, with drafting's arrowheads: a linear one's text
153    /// along its dimension line, reading left to right or bottom to top,
154    /// just off it; any other's level, just above the label.
155    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    /// Puts it on `sheet`.
192    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
216/// An arrowhead with its tip at `tip`, pointing along the unit vector `d`.
217fn 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    /// The view it is in; a note's without a leader is in none.
225    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    /// Where its value or text goes, from the point it is placed from (see
236    /// [`Annotation`]); none for a centre mark, which has none.
237    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    /// What kind of annotation it is, as a list names it.
248    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    /// It in words, naming what it refers to.
266    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    /// How it is drawn on `layout`'s sheet, measured anew in the part
288    /// `part` shows — or
289    /// why it cannot be: its view is gone, an entity it names is, or the
290    /// view does not show what it measures.
291    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
445/// The point halfway between `a` and `b`.
446fn middle(a: P, b: P) -> P {
447    scale(add(a, b), 0.5)
448}
449
450/// The frame the view `view` looks along. Refused for the section view:
451/// its lines are of the part cut, which nothing of the part names.
452fn 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
461/// The view `view` to measure in: where it is placed, and the frame it
462/// looks along. Refused for the isometric view, which foreshortens every
463/// length and angle.
464fn 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
473/// Where `target` is on the paper of the view looking along `frame`, in
474/// model units.
475fn 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            // Any point of it would do: its middle, sharp.
488            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
495/// The centre of the circular edge `edge` on the sheet drawn at `scale`,
496/// and its radius — refused unless the view placed at `p`, looking along
497/// `frame`, sees it round.
498fn 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
523/// Whether a view looking along `frame` sees a circle about `normal` round.
524fn 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
532/// The sides along `da` or `-da` and `db` or `-db` between which `towards`
533/// points, and the angle between them, counter-clockwise from the first:
534/// of the four angles two lines make, the one a label there dimensions.
535fn 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/// What can be picked in a view to annotate it.
561#[derive(Clone, Debug, PartialEq)]
562pub struct Candidate {
563    pub view: DrawnView,
564    pub what: Pickable,
565}
566
567/// A point or an edge of a view, where it is on the sheet.
568#[derive(Clone, Debug, PartialEq)]
569pub enum Pickable {
570    /// A vertex, or the centre of a circle the view sees round.
571    Point { target: Target, at: P },
572    /// A piece of an edge, drawn as `points`.
573    Edge {
574        name: String,
575        points: Vec<P>,
576        shape: EdgeShape,
577    },
578}
579
580/// What an edge is, for what it can be dimensioned by.
581#[derive(Clone, Debug, PartialEq)]
582pub enum EdgeShape {
583    /// Straight, between these two vertices.
584    Line {
585        start: String,
586        end: String,
587    },
588    /// A circle or an arc the view sees round; `full` if it is a whole
589    /// circle, its ends one vertex.
590    Circle {
591        full: bool,
592    },
593    Other,
594}
595
596/// Points along each piece of a line a candidate is drawn with.
597const POINTS_PER_PIECE: usize = 12;
598
599/// What can be picked in the views `placed` of `scene` at `scale`: the
600/// vertices of the edges each draws, the centres of the circles it sees
601/// round, and the edges themselves — each by its name in the part drawn.
602/// The section view's lines are of the part cut, whose entities the part
603/// does not name: nothing can be picked in it.
604pub(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        // The pieces of each edge drawn, by body and edge.
622        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                // Where along it is a free choice, but its ends are its own.
630                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}