map_gui/render/
intersection.rs

1use std::cell::RefCell;
2
3use geom::{
4    Angle, ArrowCap, Bounds, Distance, Line, PolyLine, Polygon, Pt2D, Ring, Tessellation, Time,
5    EPSILON_DIST,
6};
7use map_model::{
8    ControlTrafficSignal, Direction, DrivingSide, Intersection, IntersectionControl,
9    IntersectionID, LaneType, Map, Road, RoadWithStopSign, Turn, TurnType, SIDEWALK_THICKNESS,
10};
11use widgetry::{Color, Drawable, GeomBatch, GfxCtx, Prerender, RewriteColor, Text};
12
13use crate::colors::ColorScheme;
14use crate::render::{traffic_signal, DrawOptions, Renderable, OUTLINE_THICKNESS};
15use crate::{AppLike, ID};
16
17pub struct DrawIntersection {
18    pub id: IntersectionID,
19    zorder: isize,
20
21    draw_default: RefCell<Option<Drawable>>,
22    pub draw_traffic_signal: RefCell<Option<(Time, Drawable)>>,
23}
24
25impl DrawIntersection {
26    pub fn new(i: &Intersection, map: &Map) -> DrawIntersection {
27        DrawIntersection {
28            id: i.id,
29            zorder: i.get_zorder(map),
30            draw_default: RefCell::new(None),
31            draw_traffic_signal: RefCell::new(None),
32        }
33    }
34
35    pub fn render<P: AsRef<Prerender>>(&self, prerender: &P, app: &dyn AppLike) -> GeomBatch {
36        let map = app.map();
37        let i = map.get_i(self.id);
38
39        // Order matters... main polygon first, then sidewalk corners.
40        let mut default_geom = GeomBatch::new();
41        let rank = i.get_rank(map);
42        default_geom.push(
43            if i.is_footway(map) {
44                // TODO Or maybe shared-use, once there's some visual distinction there
45                app.cs().zoomed_road_surface(LaneType::Footway, rank)
46            } else if i.is_cycleway(map) {
47                app.cs().zoomed_road_surface(LaneType::Biking, rank)
48            } else {
49                app.cs().zoomed_intersection_surface(rank)
50            },
51            i.polygon.clone(),
52        );
53        default_geom.extend(
54            app.cs().zoomed_road_surface(LaneType::Sidewalk, rank),
55            calculate_corners(i, map),
56        );
57        if app.cs().road_outlines {
58            default_geom.extend(app.cs().curb(rank), calculate_corner_curbs(i, map));
59        }
60        if i.is_footway(map) {
61            for pl in Self::get_unzoomed_outline(i, map) {
62                default_geom.extend(
63                    Color::BLACK,
64                    pl.exact_dashed_polygons(
65                        Distance::meters(0.25),
66                        Distance::meters(1.0),
67                        Distance::meters(1.5),
68                    ),
69                );
70            }
71        }
72
73        for turn in &i.turns {
74            if !app.opts().show_crosswalks {
75                break;
76            }
77            if turn.turn_type.pedestrian_crossing() {
78                make_crosswalk(&mut default_geom, turn, map, app.cs());
79            }
80        }
81
82        if i.is_private(map) {
83            if let Some(color) = app.cs().private_road {
84                default_geom.push(color.alpha(0.5), i.polygon.clone());
85            }
86        }
87
88        if i.is_border() {
89            let r = map.get_r(*i.roads.iter().next().unwrap());
90            default_geom.extend(
91                app.cs().road_center_line(map),
92                calculate_border_arrows(i, r, map),
93            );
94        } else {
95            match i.control {
96                IntersectionControl::Signed | IntersectionControl::Uncontrolled => {
97                    for ss in map.get_stop_sign(i.id).roads.values() {
98                        if !app.opts().show_stop_signs {
99                            break;
100                        }
101                        if ss.must_stop {
102                            if let Some((octagon, pole, angle)) =
103                                DrawIntersection::stop_sign_geom(ss, map)
104                            {
105                                let center = octagon.center();
106                                default_geom.push(app.cs().stop_sign, octagon);
107                                default_geom.push(app.cs().stop_sign_pole, pole);
108
109                                // Trial and error to make the scale and angle work. We could also make
110                                // a fixed SVG asset and just rotate it, but we'd still need to
111                                // calculate the octagon hitbox for the stop sign editor.
112                                default_geom.append(
113                                    Text::from(
114                                        widgetry::Line("STOP").small_heading().fg(Color::WHITE),
115                                    )
116                                    .render_autocropped(prerender.as_ref())
117                                    .scale(0.02)
118                                    .centered_on(center)
119                                    .rotate(angle.opposite().rotate_degs(-90.0)),
120                                );
121                            }
122                        }
123                    }
124                }
125                IntersectionControl::Construction => {
126                    // TODO Centering seems weird
127                    default_geom.append(
128                        GeomBatch::load_svg(prerender, "system/assets/map/under_construction.svg")
129                            .scale(0.08)
130                            .centered_on(i.polygon.center()),
131                    );
132                }
133                IntersectionControl::Signalled => {}
134            }
135        }
136
137        let zorder = i.get_zorder(map);
138        if zorder < 0 {
139            default_geom = default_geom.color(RewriteColor::ChangeAlpha(0.5));
140        }
141
142        default_geom
143    }
144
145    // Returns the (octagon, pole, angle of the angle) if there's room to draw it.
146    pub fn stop_sign_geom(ss: &RoadWithStopSign, map: &Map) -> Option<(Polygon, Polygon, Angle)> {
147        let trim_back = Distance::meters(0.1);
148        let edge_lane = map.get_l(ss.lane_closest_to_edge);
149        // TODO The dream of trimming f64's was to isolate epsilon checks like this...
150        if edge_lane.length() - trim_back <= EPSILON_DIST {
151            // TODO warn
152            return None;
153        }
154        let last_line = edge_lane
155            .lane_center_pts
156            .exact_slice(Distance::ZERO, edge_lane.length() - trim_back)
157            .last_line();
158        let last_line = if map.get_config().driving_side == DrivingSide::Right {
159            last_line.shift_right(edge_lane.width)
160        } else {
161            last_line.shift_left(edge_lane.width)
162        };
163
164        let octagon = make_octagon(last_line.pt2(), Distance::meters(1.0), last_line.angle());
165        let pole = Line::must_new(
166            last_line
167                .pt2()
168                .project_away(Distance::meters(1.5), last_line.angle().opposite()),
169            // TODO Slightly < 0.9
170            last_line
171                .pt2()
172                .project_away(Distance::meters(0.9), last_line.angle().opposite()),
173        )
174        .make_polygons(Distance::meters(0.3));
175        Some((octagon, pole, last_line.angle()))
176    }
177
178    pub fn clear_rendering(&mut self) {
179        *self.draw_default.borrow_mut() = None;
180    }
181
182    /// Find sections along the intersection polygon that aren't connected to a road. These should
183    /// contribute an outline.
184    pub fn get_unzoomed_outline(i: &Intersection, map: &Map) -> Vec<PolyLine> {
185        // Turn each road into the left and right point that should be on the ring, so we can
186        // "subtract" them out.
187        let road_pairs = i
188            .roads
189            .iter()
190            .map(|r| {
191                let road = map.get_r(*r);
192                let half_width = road.get_half_width();
193                let left = road.center_pts.must_shift_left(half_width);
194                let right = road.center_pts.must_shift_right(half_width);
195                if road.src_i == i.id {
196                    (left.first_pt(), right.first_pt())
197                } else {
198                    (left.last_pt(), right.last_pt())
199                }
200            })
201            .collect::<Vec<_>>();
202
203        // Walk along each line segment on the ring. If it's not one of our road pairs, add it as a
204        // potential segment.
205        i.polygon
206            .get_outer_ring()
207            .points()
208            .windows(2)
209            .filter(|window| {
210                !road_pairs
211                    .iter()
212                    .any(|road_pair| approx_eq(window, &road_pair))
213            })
214            .filter_map(|pair| PolyLine::new(vec![pair[0], pair[1]]).ok())
215            .collect::<Vec<_>>()
216
217        // TODO We could merge adjacent segments, to get nicer corners
218    }
219
220    fn redraw_default(&self, g: &mut GfxCtx, app: &dyn AppLike) {
221        // Lazily calculate, because these are expensive to all do up-front, and most players won't
222        // exhaustively see every intersection during a single session
223        let mut draw = self.draw_default.borrow_mut();
224        if draw.is_none() {
225            *draw = Some(g.upload(self.render(g, app)));
226        }
227        g.redraw(draw.as_ref().unwrap());
228    }
229
230    fn draw_traffic_signal(
231        &self,
232        g: &mut GfxCtx,
233        app: &dyn AppLike,
234        opts: &DrawOptions,
235        signal: &ControlTrafficSignal,
236    ) {
237        if opts.suppress_traffic_signal_details.contains(&self.id) {
238            return;
239        }
240
241        let mut maybe_redraw = self.draw_traffic_signal.borrow_mut();
242        if app.opts().show_traffic_signal_icon {
243            // The icon doesn't change over time
244            let recalc = maybe_redraw.is_none();
245            if recalc {
246                let batch = GeomBatch::load_svg(g, "system/assets/map/traffic_signal.svg")
247                    .scale(0.3)
248                    .centered_on(app.map().get_i(self.id).polygon.polylabel());
249                *maybe_redraw = Some((Time::START_OF_DAY, g.prerender.upload(batch)));
250            }
251        } else {
252            let recalc = maybe_redraw
253                .as_ref()
254                .map(|(t, _)| *t != app.sim_time())
255                .unwrap_or(true);
256            if recalc {
257                let (idx, remaining) = app.current_stage_and_remaining_time(self.id);
258                let mut batch = GeomBatch::new();
259                traffic_signal::draw_signal_stage(
260                    g.prerender,
261                    &signal.stages[idx],
262                    idx,
263                    self.id,
264                    Some(remaining),
265                    &mut batch,
266                    app,
267                    app.opts().traffic_signal_style.clone(),
268                );
269                *maybe_redraw = Some((app.sim_time(), g.prerender.upload(batch)));
270            }
271        }
272
273        let (_, batch) = maybe_redraw.as_ref().unwrap();
274        g.redraw(batch);
275    }
276}
277
278fn approx_eq(pair1: &[Pt2D], pair2: &(Pt2D, Pt2D)) -> bool {
279    let epsilon = Distance::meters(0.1);
280    (pair1[0].approx_eq(pair2.0, epsilon) && pair1[1].approx_eq(pair2.1, epsilon))
281        || (pair1[0].approx_eq(pair2.1, epsilon) && pair1[1].approx_eq(pair2.0, epsilon))
282}
283
284impl Renderable for DrawIntersection {
285    fn get_id(&self) -> ID {
286        ID::Intersection(self.id)
287    }
288
289    fn draw(&self, g: &mut GfxCtx, app: &dyn AppLike, opts: &DrawOptions) {
290        self.redraw_default(g, app);
291        if let Some(signal) = app.map().maybe_get_traffic_signal(self.id) {
292            self.draw_traffic_signal(g, app, opts, signal);
293        }
294    }
295
296    fn get_outline(&self, map: &Map) -> Tessellation {
297        map.get_i(self.id).polygon.to_outline(OUTLINE_THICKNESS)
298    }
299
300    fn get_bounds(&self, map: &Map) -> Bounds {
301        map.get_i(self.id).polygon.get_bounds()
302    }
303
304    fn contains_pt(&self, pt: Pt2D, map: &Map) -> bool {
305        map.get_i(self.id).polygon.contains_pt(pt)
306    }
307
308    fn get_zorder(&self) -> isize {
309        self.zorder
310    }
311}
312
313// Public for debugging
314pub fn calculate_corners(i: &Intersection, map: &Map) -> Vec<Polygon> {
315    // Don't attempt corners for footways or where normal roads and footways meet
316    if i.roads.iter().any(|r| map.get_r(*r).is_footway()) {
317        return Vec::new();
318    }
319
320    let mut corners = Vec::new();
321
322    for turn in &i.turns {
323        if turn.turn_type == TurnType::SharedSidewalkCorner {
324            let l1 = map.get_l(turn.id.src);
325            let l2 = map.get_l(turn.id.dst);
326
327            // Special case for dead-ends: just thicken the geometry.
328            if i.roads.len() == 1 {
329                corners.push(turn.geom.make_polygons(l1.width.min(l2.width)));
330                continue;
331            }
332
333            // Is point2 counter-clockwise of point1?
334            let dir = if i
335                .polygon
336                .center()
337                .angle_to(turn.geom.first_pt())
338                .simple_shortest_rotation_towards(i.polygon.center().angle_to(turn.geom.last_pt()))
339                > 0.0
340            {
341                1.0
342            } else {
343                -1.0
344            };
345
346            if l1.width == l2.width {
347                // When two sidewalks or two shoulders meet, use the turn geometry to create some
348                // nice rounding.
349                let shift = dir * l1.width / 2.0;
350                if let Some(poly) = (|| {
351                    let mut pts = turn.geom.shift_either_direction(-shift).ok()?.into_points();
352                    pts.push(l2.end_line(i.id).shift_either_direction(shift).pt2());
353                    pts.push(l2.end_line(i.id).shift_either_direction(-shift).pt2());
354                    pts.extend(
355                        turn.geom
356                            .shift_either_direction(shift)
357                            .ok()?
358                            .reversed()
359                            .into_points(),
360                    );
361                    pts.push(l1.end_line(i.id).shift_either_direction(shift).pt2());
362                    pts.push(l1.end_line(i.id).shift_either_direction(-shift).pt2());
363                    pts.push(pts[0]);
364                    Some(Ring::deduping_new(pts).ok()?.into_polygon())
365                })() {
366                    corners.push(poly);
367                }
368            } else {
369                // When a sidewalk and a shoulder meet, use a simpler shape to connect them.
370                let mut pts = vec![
371                    l2.end_line(i.id)
372                        .shift_either_direction(dir * l2.width / 2.0)
373                        .pt2(),
374                    l2.end_line(i.id)
375                        .shift_either_direction(-dir * l2.width / 2.0)
376                        .pt2(),
377                    l1.end_line(i.id)
378                        .shift_either_direction(-dir * l1.width / 2.0)
379                        .pt2(),
380                    l1.end_line(i.id)
381                        .shift_either_direction(dir * l1.width / 2.0)
382                        .pt2(),
383                ];
384                pts.push(pts[0]);
385                if let Ok(ring) = Ring::new(pts) {
386                    corners.push(ring.into_polygon());
387                }
388            }
389        }
390    }
391
392    corners
393}
394
395fn calculate_corner_curbs(i: &Intersection, map: &Map) -> Vec<Polygon> {
396    // Don't attempt corners for footways or where normal roads and footways meet
397    if i.roads.iter().any(|r| map.get_r(*r).is_footway()) {
398        return Vec::new();
399    }
400
401    let mut curbs = Vec::new();
402
403    let thickness = Distance::meters(0.2);
404    let shift = |width| (width - thickness) / 2.0;
405
406    for turn in &i.turns {
407        if turn.turn_type == TurnType::SharedSidewalkCorner {
408            let dir = if turn
409                .geom
410                .first_pt()
411                .angle_to(i.polygon.center())
412                .simple_shortest_rotation_towards(
413                    turn.geom.first_pt().angle_to(turn.geom.last_pt()),
414                )
415                > 0.0
416            {
417                1.0
418            } else {
419                -1.0
420                // At a dead end we're going the long way around
421            } * if i.is_deadend_for_everyone() {
422                -1.0
423            } else {
424                1.0
425            };
426            let l1 = map.get_l(turn.id.src);
427            let l2 = map.get_l(turn.id.dst);
428
429            if l1.width == l2.width {
430                // When two sidewalks or two shoulders meet, use the turn geometry to create some
431                // nice rounding.
432                let width = dir * shift(l1.width);
433
434                if let Some(pl) = (|| {
435                    let mut pts = turn.geom.shift_either_direction(width).ok()?.into_points();
436                    // TODO Connecting the SharedSidewalkCorner geometry to the curb usually
437                    // requires adding a few points from the sidewalk on each end. But sometimes
438                    // this causes "zig-zaggy" artifacts. The approx_eq check helps some (but not
439                    // all) of those cases, but sometimes introduces visual "gaps". This still
440                    // needs more work.
441                    let first_line = l2.end_line(i.id).shift_either_direction(-width);
442                    if !pts.last().unwrap().approx_eq(first_line.pt2(), thickness) {
443                        pts.push(first_line.pt2());
444                        pts.push(first_line.unbounded_dist_along(first_line.length() - thickness));
445                    }
446                    let last_line = l1.end_line(i.id).shift_either_direction(width);
447                    if !pts[0].approx_eq(last_line.pt2(), thickness) {
448                        pts.insert(0, last_line.pt2());
449                        pts.insert(
450                            0,
451                            last_line.unbounded_dist_along(last_line.length() - thickness),
452                        );
453                    }
454                    PolyLine::deduping_new(pts).ok()
455                })() {
456                    curbs.push(pl.make_polygons(thickness));
457                }
458            } else {
459                // When a sidewalk and a shoulder meet, use a simpler shape to connect them.
460                let l1_line = l1
461                    .end_line(i.id)
462                    .shift_either_direction(dir * shift(l1.width));
463                let l2_line = l2
464                    .end_line(i.id)
465                    .shift_either_direction(-dir * shift(l2.width));
466                if let Ok(pl) = PolyLine::deduping_new(vec![
467                    l1_line.unbounded_dist_along(l1_line.length() - thickness),
468                    l1_line.pt2(),
469                    l2_line.pt2(),
470                    l2_line.unbounded_dist_along(l2_line.length() - thickness),
471                ]) {
472                    curbs.push(pl.make_polygons(thickness));
473                }
474            }
475        }
476    }
477
478    curbs
479}
480
481// TODO This assumes the lanes change direction only at one point. A two-way cycletrack right at
482// the border will look a bit off.
483fn calculate_border_arrows(i: &Intersection, r: &Road, map: &Map) -> Vec<Polygon> {
484    let mut result = Vec::new();
485
486    let mut width_fwd = Distance::ZERO;
487    let mut width_back = Distance::ZERO;
488    for l in &r.lanes {
489        if l.dir == Direction::Fwd {
490            width_fwd += l.width;
491        } else {
492            width_back += l.width;
493        }
494    }
495    let center = r.get_dir_change_pl(map);
496
497    // These arrows should point from the void to the road
498    if !i.outgoing_lanes.is_empty() {
499        let (line, width) = if r.dst_i == i.id {
500            (
501                center.last_line().shift_left(width_back / 2.0).reversed(),
502                width_back,
503            )
504        } else {
505            (center.first_line().shift_right(width_fwd / 2.0), width_fwd)
506        };
507        result.push(
508            // DEGENERATE_INTERSECTION_HALF_LENGTH is 2.5m...
509            PolyLine::must_new(vec![
510                line.unbounded_dist_along(Distance::meters(-9.5)),
511                line.unbounded_dist_along(Distance::meters(-0.5)),
512            ])
513            .make_arrow(width / 3.0, ArrowCap::Triangle),
514        );
515    }
516
517    // These arrows should point from the road to the void
518    if !i.incoming_lanes.is_empty() {
519        let (line, width) = if r.dst_i == i.id {
520            (
521                center.last_line().shift_right(width_fwd / 2.0).reversed(),
522                width_fwd,
523            )
524        } else {
525            (center.first_line().shift_left(width_back / 2.0), width_back)
526        };
527        result.push(
528            PolyLine::must_new(vec![
529                line.unbounded_dist_along(Distance::meters(-0.5)),
530                line.unbounded_dist_along(Distance::meters(-9.5)),
531            ])
532            .make_arrow(width / 3.0, ArrowCap::Triangle),
533        );
534    }
535
536    result
537}
538
539// TODO A squished octagon would look better
540fn make_octagon(center: Pt2D, radius: Distance, facing: Angle) -> Polygon {
541    Ring::must_new(
542        (0..=8)
543            .map(|i| center.project_away(radius, facing.rotate_degs(22.5 + f64::from(i * 360 / 8))))
544            .collect(),
545    )
546    .into_polygon()
547}
548
549/// Draws both zebra crosswalks and unmarked crossings
550pub fn make_crosswalk(batch: &mut GeomBatch, turn: &Turn, map: &Map, cs: &ColorScheme) {
551    if turn.turn_type == TurnType::UnmarkedCrossing {
552        make_unmarked_crossing(batch, turn, map, cs);
553        return;
554    }
555
556    if make_rainbow_crosswalk(batch, turn, map) {
557        return;
558    }
559
560    // This size also looks better for shoulders
561    let width = SIDEWALK_THICKNESS;
562    // Start at least width out to not hit sidewalk corners. Also account for the thickness of the
563    // crosswalk line itself. Center the lines inside these two boundaries.
564    let boundary = width;
565    let tile_every = width * 0.6;
566    let line = if let Some(l) = turn.crosswalk_line() {
567        l
568    } else {
569        return;
570    };
571
572    const CROSSWALK_LINE_THICKNESS: Distance = Distance::const_meters(0.15);
573
574    let available_length = line.length() - (boundary * 2.0);
575    if available_length > Distance::ZERO {
576        let num_markings = (available_length / tile_every).floor() as usize;
577        let mut dist_along =
578            boundary + (available_length - tile_every * (num_markings as f64)) / 2.0;
579        // TODO Seems to be an off-by-one sometimes. Not enough of these.
580        let err = format!("make_crosswalk for {} broke", turn.id);
581        for _ in 0..=num_markings {
582            let pt1 = line.dist_along(dist_along).expect(&err);
583            // Reuse perp_line. Project away an arbitrary amount
584            let pt2 = pt1.project_away(Distance::meters(1.0), line.angle());
585            batch.push(
586                cs.general_road_marking,
587                perp_line(Line::must_new(pt1, pt2), width).make_polygons(CROSSWALK_LINE_THICKNESS),
588            );
589
590            // Actually every line is a double
591            let pt3 = line
592                .dist_along(dist_along + 2.0 * CROSSWALK_LINE_THICKNESS)
593                .expect(&err);
594            let pt4 = pt3.project_away(Distance::meters(1.0), line.angle());
595            batch.push(
596                cs.general_road_marking,
597                perp_line(Line::must_new(pt3, pt4), width).make_polygons(CROSSWALK_LINE_THICKNESS),
598            );
599
600            dist_along += tile_every;
601        }
602    }
603}
604
605fn make_rainbow_crosswalk(batch: &mut GeomBatch, turn: &Turn, map: &Map) -> bool {
606    // TODO The crosswalks aren't tagged in OSM yet. Manually hardcoding some now.
607    let node = map.get_i(turn.id.parent).orig_id.0;
608    let way = map.get_parent(turn.id.src).orig_id.osm_way_id.0;
609    match (node, way) {
610        // Broadway and Pine
611        (53073255, 428246441) |
612        (53073255, 332601014) |
613        // Broadway and Pike
614        (53073254, 6447455) |
615        (53073254, 607690679) |
616        // 10th and Pine
617        (53168934, 6456052) |
618        // 10th and Pike
619        (53200834, 6456052) |
620        // 11th and Pine
621        (53068795, 607691081) |
622        (53068795, 65588105) |
623        // 11th and Pike
624        (53068794, 65588105) => {}
625        _ => { return false; }
626    }
627
628    let total_width = map.get_l(turn.id.src).width;
629    let colors = vec![
630        Color::WHITE,
631        Color::RED,
632        Color::ORANGE,
633        Color::YELLOW,
634        Color::GREEN,
635        Color::BLUE,
636        Color::hex("#8B00FF"),
637        Color::WHITE,
638    ];
639    let band_width = total_width / (colors.len() as f64);
640    let total_width = map.get_l(turn.id.src).width;
641    let slice = turn
642        .geom
643        .exact_slice(total_width, turn.geom.length() - total_width)
644        .must_shift_left(total_width / 2.0 - band_width / 2.0);
645    for (idx, color) in colors.into_iter().enumerate() {
646        batch.push(
647            color,
648            slice
649                .must_shift_right(band_width * (idx as f64))
650                .make_polygons(band_width),
651        );
652    }
653    true
654}
655
656fn make_unmarked_crossing(batch: &mut GeomBatch, turn: &Turn, map: &Map, cs: &ColorScheme) {
657    let color = cs.general_road_marking.alpha(0.5);
658    let band_width = Distance::meters(0.1);
659    let total_width = map.get_l(turn.id.src).width;
660    if let Some(line) = turn.crosswalk_line() {
661        if let Ok(slice) = line.slice(total_width, line.length() - total_width) {
662            batch.push(
663                color,
664                slice
665                    .shift_left(total_width / 2.0 - band_width / 2.0)
666                    .make_polygons(band_width),
667            );
668            batch.push(
669                color,
670                slice
671                    .shift_right(total_width / 2.0 - band_width / 2.0)
672                    .make_polygons(band_width),
673            );
674        }
675    }
676}
677
678// TODO copied from DrawLane
679fn perp_line(l: Line, length: Distance) -> Line {
680    let pt1 = l.shift_right(length / 2.0).pt1();
681    let pt2 = l.shift_left(length / 2.0).pt1();
682    Line::must_new(pt1, pt2)
683}