Document as much of geom as I can before my battery dies. And a mechanical API changes, Angle::new_degs -> Angle::degrees

This commit is contained in:
Dustin Carlino 2020-10-19 19:55:05 -05:00
parent 262c48721b
commit af70904377
21 changed files with 147 additions and 50 deletions

View File

@ -243,7 +243,7 @@ pub fn draw_occupants(details: &mut Details, app: &App, id: BuildingID, focus: O
// Lies
id: PedestrianID(person.0),
pos,
facing: Angle::new_degs(90.0),
facing: Angle::degrees(90.0),
waiting_for_turn: None,
preparing_bike: false,
// Both hands and feet!

View File

@ -209,7 +209,7 @@ pub fn trips(
.scale(1.5);
if !open_trips.contains_key(t) {
icon = icon.rotate(Angle::new_degs(180.0));
icon = icon.rotate(Angle::degrees(180.0));
}
icon.batch().container().align_right().margin_right(10)

View File

@ -77,10 +77,10 @@ impl DrawBuilding {
}
x => {
let angle = match x {
CameraAngle::IsometricNE => Angle::new_degs(-45.0),
CameraAngle::IsometricNW => Angle::new_degs(-135.0),
CameraAngle::IsometricSE => Angle::new_degs(45.0),
CameraAngle::IsometricSW => Angle::new_degs(135.0),
CameraAngle::IsometricNE => Angle::degrees(-45.0),
CameraAngle::IsometricNW => Angle::degrees(-135.0),
CameraAngle::IsometricSE => Angle::degrees(45.0),
CameraAngle::IsometricSW => Angle::degrees(135.0),
CameraAngle::TopDown | CameraAngle::Abstract => unreachable!(),
};

View File

@ -47,8 +47,8 @@ impl DrawBusStop {
batch.push(
cs.stop_sign_pole,
Line::new(
center.project_away(RADIUS, Angle::new_degs(90.0)),
center.project_away(1.5 * RADIUS, Angle::new_degs(90.0)),
center.project_away(RADIUS, Angle::degrees(90.0)),
center.project_away(1.5 * RADIUS, Angle::degrees(90.0)),
)
.unwrap()
.make_polygons(Distance::meters(0.3)),

View File

@ -126,14 +126,14 @@ impl DrawLane {
GeomBatch::load_svg(g.prerender, "system/assets/map/bus_only.svg")
.scale(0.06)
.centered_on(pt)
.rotate(angle.shortest_rotation_towards(Angle::new_degs(-90.0))),
.rotate(angle.shortest_rotation_towards(Angle::degrees(-90.0))),
);
} else if lane.is_biking() {
draw.append(
GeomBatch::load_svg(g.prerender, "system/assets/meters/bike.svg")
.scale(0.06)
.centered_on(pt)
.rotate(angle.shortest_rotation_towards(Angle::new_degs(-90.0))),
.rotate(angle.shortest_rotation_towards(Angle::degrees(-90.0))),
);
} else if lane.lane_type == LaneType::SharedLeftTurn {
draw.append(
@ -141,7 +141,7 @@ impl DrawLane {
.autocrop()
.scale(0.003)
.centered_on(pt)
.rotate(angle.shortest_rotation_towards(Angle::new_degs(-90.0))),
.rotate(angle.shortest_rotation_towards(Angle::degrees(-90.0))),
);
} else if lane.lane_type == LaneType::Construction {
// TODO Still not quite centered right, but close enough
@ -152,7 +152,7 @@ impl DrawLane {
)
.scale(0.05)
.rotate_around_batch_center(
angle.shortest_rotation_towards(Angle::new_degs(-90.0)),
angle.shortest_rotation_towards(Angle::degrees(-90.0)),
)
.autocrop()
.centered_on(pt),

View File

@ -254,6 +254,6 @@ fn crosswalk_icon(geom: &PolyLine) -> (Pt2D, Angle) {
let l = Line::must_new(geom.points()[1], geom.points()[2]);
(
l.dist_along(Distance::meters(1.0)).unwrap_or(l.pt1()),
l.angle().shortest_rotation_towards(Angle::new_degs(90.0)),
l.angle().shortest_rotation_towards(Angle::degrees(90.0)),
)
}

17
geom/README.md Normal file
View File

@ -0,0 +1,17 @@
# geom
This crate contains primitive types used by A/B Street. It's unclear if other
apps will have any use for this crate. In some cases, `geom` just wraps much
more polished APIs, like `rust-geo`. In others, it has its own geometric
algorithms, but they likely have many bugs and make use-case-driven assumptions.
So, be warned if you use this.
## Contents
Many of the types are geometric: `Pt2D`, `Ring`, `Distance`, `Line`,
`InfiniteLine`, `FindClosest`, `Circle`, `Angle`, `LonLat`, `Bounds`,
`GPSBounds`, `PolyLine`, `Polygon`, `Triangle`.
Some involve time: `Time`, `Duration`, `Speed`.
And there's also a `Percent` wrapper and a `Histogram`.

View File

@ -2,7 +2,7 @@ use std::fmt;
use serde::{Deserialize, Serialize};
/// Stores in radians
/// An angle, stored in radians.
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
pub struct Angle(f64);
@ -15,10 +15,12 @@ impl Angle {
Angle((rads * 10_000_000.0).round() / 10_000_000.0)
}
pub fn new_degs(degs: f64) -> Angle {
/// Create an angle in degrees.
pub fn degrees(degs: f64) -> Angle {
Angle::new_rads(degs.to_radians())
}
/// Invert the direction of this angle.
pub fn opposite(self) -> Angle {
Angle::new_rads(self.0 + std::f64::consts::PI)
}
@ -27,18 +29,22 @@ impl Angle {
Angle::new_rads(2.0 * std::f64::consts::PI - self.0)
}
/// Rotates this angle by some degrees.
pub fn rotate_degs(self, degrees: f64) -> Angle {
Angle::new_rads(self.0 + degrees.to_radians())
}
/// Returns [0, 2pi)
pub fn normalized_radians(self) -> f64 {
if self.0 < 0.0 {
// TODO Be more careful about how we store the angle, to make sure this works
self.0 + (2.0 * std::f64::consts::PI)
} else {
self.0
}
}
/// Returns [0, 360)
pub fn normalized_degrees(self) -> f64 {
self.normalized_radians().to_degrees()
}
@ -47,11 +53,12 @@ impl Angle {
/// normalize to be [0, 360].
pub fn shortest_rotation_towards(self, other: Angle) -> Angle {
// https://math.stackexchange.com/questions/110080/shortest-way-to-achieve-target-angle
Angle::new_degs(
Angle::degrees(
((self.normalized_degrees() - other.normalized_degrees() + 540.0) % 360.0) - 180.0,
)
}
/// True if this angle is within some degrees of another, accounting for rotation
pub fn approx_eq(self, other: Angle, within_degrees: f64) -> bool {
// https://math.stackexchange.com/questions/110080/shortest-way-to-achieve-target-angle
// This yields [-180, 180]

View File

@ -4,6 +4,7 @@ use aabb_quadtree::geom::{Point, Rect};
use crate::{LonLat, Polygon, Pt2D, Ring};
/// Represents a rectangular boundary of `Pt2D` points.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Bounds {
pub min_x: f64,
@ -13,6 +14,7 @@ pub struct Bounds {
}
impl Bounds {
/// A boundary including no points.
pub fn new() -> Bounds {
Bounds {
min_x: f64::MAX,
@ -22,6 +24,7 @@ impl Bounds {
}
}
/// Create a boundary covering some points.
pub fn from(pts: &Vec<Pt2D>) -> Bounds {
let mut b = Bounds::new();
for pt in pts {
@ -30,6 +33,7 @@ impl Bounds {
b
}
/// Update the boundary to include this point.
pub fn update(&mut self, pt: Pt2D) {
self.min_x = self.min_x.min(pt.x());
self.max_x = self.max_x.max(pt.x());
@ -37,15 +41,18 @@ impl Bounds {
self.max_y = self.max_y.max(pt.y());
}
/// Unions two boundaries.
pub fn union(&mut self, other: Bounds) {
self.update(Pt2D::new(other.min_x, other.min_y));
self.update(Pt2D::new(other.max_x, other.max_y));
}
/// True if the point is within the boundary.
pub fn contains(&self, pt: Pt2D) -> bool {
pt.x() >= self.min_x && pt.x() <= self.max_x && pt.y() >= self.min_y && pt.y() <= self.max_y
}
/// Converts the boundary to the format used by `aabb_quadtree`.
pub fn as_bbox(&self) -> Rect {
Rect {
top_left: Point {
@ -59,6 +66,7 @@ impl Bounds {
}
}
/// Creates a rectangle covering this boundary.
pub fn get_rectangle(&self) -> Polygon {
Ring::must_new(vec![
Pt2D::new(self.min_x, self.min_y),
@ -70,13 +78,18 @@ impl Bounds {
.to_polygon()
}
/// The width of this boundary.
// TODO Really should be Distance
pub fn width(&self) -> f64 {
self.max_x - self.min_x
}
/// The height of this boundary.
pub fn height(&self) -> f64 {
self.max_y - self.min_y
}
/// The center point of this boundary.
pub fn center(&self) -> Pt2D {
Pt2D::new(
self.min_x + self.width() / 2.0,
@ -85,6 +98,9 @@ impl Bounds {
}
}
/// Represents a rectangular boundary of `LonLat` points. After building one of these, `LonLat`s
/// can be transformed into `Pt2D`s, treating the top-left of the boundary as (0, 0), and growing
/// to the right and down (screen-drawing order, not Cartesian) in meters.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct GPSBounds {
pub(crate) min_lon: f64,
@ -94,6 +110,7 @@ pub struct GPSBounds {
}
impl GPSBounds {
/// A boundary including no points.
pub fn new() -> GPSBounds {
GPSBounds {
min_lon: f64::MAX,
@ -103,6 +120,7 @@ impl GPSBounds {
}
}
/// Create a boundary covering some points.
pub fn from(pts: Vec<LonLat>) -> GPSBounds {
let mut b = GPSBounds::new();
for pt in pts {
@ -111,6 +129,7 @@ impl GPSBounds {
b
}
/// Update the boundary to include this point.
pub fn update(&mut self, pt: LonLat) {
self.min_lon = self.min_lon.min(pt.x());
self.max_lon = self.max_lon.max(pt.x());
@ -118,6 +137,7 @@ impl GPSBounds {
self.max_lat = self.max_lat.max(pt.y());
}
/// True if the point is within the boundary.
pub fn contains(&self, pt: LonLat) -> bool {
pt.x() >= self.min_lon
&& pt.x() <= self.max_lon
@ -125,15 +145,17 @@ impl GPSBounds {
&& pt.y() <= self.max_lat
}
/// The bottom-right corner of the boundary, in map-space.
// TODO cache this
pub fn get_max_world_pt(&self) -> Pt2D {
let width = LonLat::new(self.min_lon, self.min_lat)
.gps_dist_meters(LonLat::new(self.max_lon, self.min_lat));
.gps_dist(LonLat::new(self.max_lon, self.min_lat));
let height = LonLat::new(self.min_lon, self.min_lat)
.gps_dist_meters(LonLat::new(self.min_lon, self.max_lat));
.gps_dist(LonLat::new(self.min_lon, self.max_lat));
Pt2D::new(width.inner_meters(), height.inner_meters())
}
/// Converts the boundary to map-space.
pub fn to_bounds(&self) -> Bounds {
let mut b = Bounds::new();
b.update(Pt2D::new(0.0, 0.0));
@ -141,7 +163,7 @@ impl GPSBounds {
b
}
/// Fails if points are out-of-bounds.
/// Convert all points to map-space, failing if any points are outside this boundary.
pub fn try_convert(&self, pts: &Vec<LonLat>) -> Option<Vec<Pt2D>> {
let mut result = Vec::new();
for pt in pts {
@ -153,18 +175,14 @@ impl GPSBounds {
Some(result)
}
/// Results can be out-of-bounds.
/// Convert all points to map-space. The points may be outside this boundary.
pub fn convert(&self, pts: &Vec<LonLat>) -> Vec<Pt2D> {
pts.iter().map(|pt| Pt2D::from_gps(*pt, self)).collect()
}
/// Convert map-space points back to `LonLat`s. This is only valid if the `GPSBounds` used
/// is the same as the one used to originally produce the `Pt2D`s.
pub fn convert_back(&self, pts: &Vec<Pt2D>) -> Vec<LonLat> {
pts.iter().map(|pt| pt.to_gps(self)).collect()
}
pub fn approx_eq(&self, other: &GPSBounds) -> bool {
LonLat::new(self.min_lon, self.min_lat).approx_eq(LonLat::new(other.min_lon, other.min_lat))
&& LonLat::new(self.max_lon, self.max_lat)
.approx_eq(LonLat::new(other.max_lon, other.max_lat))
}
}

View File

@ -6,6 +6,7 @@ use crate::{Angle, Bounds, Distance, Polygon, Pt2D, Ring};
const TRIANGLES_PER_CIRCLE: usize = 60;
/// A circle, defined by a center and radius.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Circle {
pub center: Pt2D,
@ -13,16 +14,19 @@ pub struct Circle {
}
impl Circle {
/// Creates a circle.
pub fn new(center: Pt2D, radius: Distance) -> Circle {
Circle { center, radius }
}
/// True if the point is inside the circle.
pub fn contains_pt(&self, pt: Pt2D) -> bool {
// avoid sqrt by squaring radius instead
(pt.x() - self.center.x()).powi(2) + (pt.y() - self.center.y()).powi(2)
< self.radius.inner_meters().powi(2)
}
/// Get the boundary containing this circle.
pub fn get_bounds(&self) -> Bounds {
Bounds {
min_x: self.center.x() - self.radius.inner_meters(),
@ -32,17 +36,20 @@ impl Circle {
}
}
/// Renders the circle as a polygon.
pub fn to_polygon(&self) -> Polygon {
self.to_partial_polygon(1.0)
}
/// Renders some percent, between [0, 1], of the circle as a polygon. The polygon starts from 0
/// degrees.
pub fn to_partial_polygon(&self, percent_full: f64) -> Polygon {
let mut pts = vec![self.center];
let mut indices = Vec::new();
for i in 0..TRIANGLES_PER_CIRCLE {
pts.push(self.center.project_away(
self.radius,
Angle::new_degs((i as f64) / (TRIANGLES_PER_CIRCLE as f64) * percent_full * 360.0),
Angle::degrees((i as f64) / (TRIANGLES_PER_CIRCLE as f64) * percent_full * 360.0),
));
indices.push(0);
indices.push(i + 1);
@ -58,20 +65,21 @@ impl Circle {
Polygon::precomputed(pts, indices)
}
/// Returns the ring around the circle.
fn to_ring(&self) -> Ring {
Ring::must_new(
(0..=TRIANGLES_PER_CIRCLE)
.map(|i| {
self.center.project_away(
self.radius,
Angle::new_degs((i as f64) / (TRIANGLES_PER_CIRCLE as f64) * 360.0),
Angle::degrees((i as f64) / (TRIANGLES_PER_CIRCLE as f64) * 360.0),
)
})
.collect(),
)
}
/// Draws an outline around the circle, strictly contained with the circle's original radius.
/// Creates an outline around the circle, strictly contained with the circle's original radius.
pub fn to_outline(&self, thickness: Distance) -> Result<Polygon, String> {
if self.radius <= thickness {
return Err(format!(

View File

@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use crate::{trim_f64, Duration, Speed, UnitFmt};
/// In meters. Can be negative.
/// A distance, in meters. Can be negative.
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct Distance(f64);
@ -19,6 +19,7 @@ impl Ord for Distance {
impl Distance {
pub const ZERO: Distance = Distance::const_meters(0.0);
/// Creates a distance in meters.
pub fn meters(value: f64) -> Distance {
if !value.is_finite() {
panic!("Bad Distance {}", value);
@ -32,18 +33,22 @@ impl Distance {
Distance(value)
}
/// Creates a distance in inches.
pub fn inches(value: f64) -> Distance {
Distance::meters(0.0254 * value)
}
/// Creates a distance in miles.
pub fn miles(value: f64) -> Distance {
Distance::meters(1609.34 * value)
}
/// Creates a distance in centimeters.
pub fn centimeters(value: usize) -> Distance {
Distance::meters((value as f64) / 100.0)
}
/// Returns the absolute value of this distance.
pub fn abs(self) -> Distance {
if self.0 > 0.0 {
self
@ -52,15 +57,18 @@ impl Distance {
}
}
/// Returns the square root of this distance.
pub fn sqrt(self) -> Distance {
Distance::meters(self.0.sqrt())
}
/// Returns the distance in meters. Prefer to work with type-safe `Distance`s.
// TODO Remove if possible.
pub fn inner_meters(self) -> f64 {
self.0
}
/// Describes the distance according to formatting rules.
pub fn to_string(self, fmt: &UnitFmt) -> String {
if fmt.metric {
// TODO Round values to nearest meter, and km
@ -76,6 +84,7 @@ impl Distance {
}
}
/// Returns the largest of the two inputs.
pub fn max(self, other: Distance) -> Distance {
if self >= other {
self
@ -84,6 +93,7 @@ impl Distance {
}
}
/// Returns the smallest of the two inputs.
pub fn min(self, other: Distance) -> Distance {
if self <= other {
self
@ -95,7 +105,6 @@ impl Distance {
impl fmt::Display for Distance {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO This is harder to localize
write!(f, "{}m", self.0)
}
}

View File

@ -8,7 +8,7 @@ use abstutil::elapsed_seconds;
use crate::{trim_f64, Distance, Speed};
/// In seconds. Can be negative.
/// A duration, in seconds. Can be negative.
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct Duration(f64);
@ -24,6 +24,7 @@ impl Duration {
pub const ZERO: Duration = Duration::const_seconds(0.0);
const EPSILON: Duration = Duration::const_seconds(0.0001);
/// Creates a duration in seconds.
pub fn seconds(value: f64) -> Duration {
if !value.is_finite() {
panic!("Bad Duration {}", value);
@ -32,14 +33,17 @@ impl Duration {
Duration(trim_f64(value))
}
/// Creates a duration in minutes.
pub fn minutes(mins: usize) -> Duration {
Duration::seconds((mins as f64) * 60.0)
}
/// Creates a duration in hours.
pub fn hours(hours: usize) -> Duration {
Duration::seconds((hours as f64) * 3600.0)
}
/// Creates a duration in minutes.
pub fn f64_minutes(mins: f64) -> Duration {
Duration::seconds(mins * 60.0)
}
@ -56,13 +60,14 @@ impl Duration {
(x as f64) * Duration::EPSILON
}
/// Returns the duration in seconds. Prefer working in typesafe `Duration`s.
// TODO Remove if possible.
pub fn inner_seconds(self) -> f64 {
self.0
}
/// Splits the duration into (hours, minutes, seconds, centiseconds).
// TODO Could share some of this with Time -- the representations are the same
// (hours, minutes, seconds, centiseconds)
fn get_parts(self) -> (usize, usize, usize, usize) {
// Force positive
let mut remainder = self.inner_seconds().abs();
@ -82,6 +87,7 @@ impl Duration {
)
}
/// Parses a duration such as "3:00" to `Duration::minutes(3)`.
// TODO This is NOT the inverse of Display!
pub fn parse(string: &str) -> Result<Duration, Box<dyn Error>> {
let parts: Vec<&str> = string.split(':').collect();
@ -128,10 +134,12 @@ impl Duration {
}
}
/// Returns the duration elapsed from this moment in real time.
pub fn realtime_elapsed(since: Instant) -> Duration {
Duration::seconds(elapsed_seconds(since))
}
/// Rounds a duration up to the nearest whole number multiple.
pub fn round_up(self, multiple: Duration) -> Duration {
let remainder = self % multiple;
if remainder == Duration::ZERO {
@ -141,6 +149,7 @@ impl Duration {
}
}
/// Returns the duration as a number of minutes, rounded up.
pub fn num_minutes_rounded_up(self) -> usize {
let (hrs, mins, secs, rem) = self.get_parts();
let mut result = mins + 60 * hrs;

View File

@ -10,6 +10,7 @@ use crate::{Bounds, Distance, Pt2D};
// TODO Maybe use https://crates.io/crates/spatial-join proximity maps
/// A quad-tree to quickly find the closest points to some polylines.
pub struct FindClosest<K> {
// TODO maybe any type of geo:: thing
geometries: BTreeMap<K, geo::LineString<f64>>,
@ -20,6 +21,7 @@ impl<K> FindClosest<K>
where
K: Clone + Ord + std::fmt::Debug,
{
/// Creates the quad-tree, limited to points contained in the boundary.
pub fn new(bounds: &Bounds) -> FindClosest<K> {
FindClosest {
geometries: BTreeMap::new(),
@ -27,12 +29,15 @@ where
}
}
/// Add an object to the quadtree, remembering some key associated with the points.
pub fn add(&mut self, key: K, pts: &Vec<Pt2D>) {
self.geometries.insert(key.clone(), pts_to_line_string(pts));
self.quadtree
.insert_with_box(key, Bounds::from(pts).as_bbox());
}
/// For every object within some distance of a query point, return the (object's key, point on
/// the object's polyline, distance away).
pub fn all_close_pts(
&self,
query_pt: Pt2D,

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use crate::Distance;
/// longitude is x, latitude is y
/// Represents a (longitude, latitude) point.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)]
pub struct LonLat {
longitude: NotNan<f64>,
@ -16,6 +16,7 @@ pub struct LonLat {
}
impl LonLat {
/// Note the order of arguments!
pub fn new(lon: f64, lat: f64) -> LonLat {
LonLat {
longitude: NotNan::new(lon).unwrap(),
@ -23,16 +24,18 @@ impl LonLat {
}
}
/// Returns the longitude of this point.
pub fn x(self) -> f64 {
self.longitude.into_inner()
}
/// Returns the latitude of this point.
pub fn y(self) -> f64 {
self.latitude.into_inner()
}
pub fn gps_dist_meters(self, other: LonLat) -> Distance {
// Haversine distance
/// Returns the Haversine distance to another point.
pub(crate) fn gps_dist(self, other: LonLat) -> Distance {
let earth_radius_m = 6_371_000.0;
let lon1 = self.x().to_radians();
let lon2 = other.x().to_radians();
@ -53,11 +56,8 @@ impl LonLat {
NotNan::new((self.x() - other.x()).powi(2) + (self.y() - other.y()).powi(2)).unwrap()
}
pub(crate) fn approx_eq(self, other: LonLat) -> bool {
let epsilon = 1e-8;
(self.x() - other.x()).abs() < epsilon && (self.y() - other.y()).abs() < epsilon
}
/// Parses a file in the https://wiki.openstreetmap.org/wiki/Osmosis/Polygon_Filter_File_Format
/// and returns all points.
pub fn read_osmosis_polygon(path: String) -> Result<Vec<LonLat>, Box<dyn Error>> {
let f = File::open(&path)?;
let mut pts = Vec::new();

View File

@ -5,50 +5,63 @@ use serde::{Deserialize, Serialize};
use crate::{Angle, Distance, PolyLine, Polygon, Pt2D, EPSILON_DIST};
/// Segment, technically. Should rename.
/// A line segment.
// TODO Rename?
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct Line(Pt2D, Pt2D);
impl Line {
/// Creates a line segment between two points. None if the points are the same.
pub fn new(pt1: Pt2D, pt2: Pt2D) -> Option<Line> {
if pt1.dist_to(pt2) <= EPSILON_DIST {
return None;
}
Some(Line(pt1, pt2))
}
// Just to be more clear at the call-site
/// Equivalent to `Line::new(pt1, pt2).unwrap()`. Use this to effectively document an assertion
/// at the call-site.
pub fn must_new(pt1: Pt2D, pt2: Pt2D) -> Line {
Line::new(pt1, pt2).unwrap()
}
/// Returns an infinite line passing through this line's two points.
pub fn infinite(&self) -> InfiniteLine {
InfiniteLine(self.0, self.1)
}
/// Returns the first point in this line segment.
pub fn pt1(&self) -> Pt2D {
self.0
}
/// Returns the second point in this line segment.
pub fn pt2(&self) -> Pt2D {
self.1
}
/// Returns the two points in this line segment.
pub fn points(&self) -> Vec<Pt2D> {
vec![self.0, self.1]
}
/// Returns a polyline containing these two points.
pub fn to_polyline(&self) -> PolyLine {
PolyLine::must_new(self.points())
}
/// Returns a thick line segment.
pub fn make_polygons(&self, thickness: Distance) -> Polygon {
self.to_polyline().make_polygons(thickness)
}
/// Length of the line segment
pub fn length(&self) -> Distance {
self.pt1().dist_to(self.pt2())
}
/// If two line segments intersect -- including endpoints -- return the point where they hit.
/// Undefined if the two lines have more than one intersection point!
// TODO Also return the distance along self
pub fn intersection(&self, other: &Line) -> Option<Pt2D> {
// From http://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
@ -74,7 +87,7 @@ impl Line {
}
}
/// An intersection that isn't just two endpoints touching
/// Determine if two line segments intersect, but more so than just two endpoints touching.
pub fn crosses(&self, other: &Line) -> bool {
if self.pt1() == other.pt1()
|| self.pt1() == other.pt2()
@ -86,6 +99,9 @@ impl Line {
self.intersection(other).is_some()
}
/// If the line segment intersects with an infinite line -- including endpoints -- return the
/// point where they hit. Undefined if the segment and infinite line intersect at more than one
/// point!
// TODO Also return the distance along self
pub fn intersection_infinite(&self, other: &InfiniteLine) -> Option<Pt2D> {
let hit = self.infinite().intersection(other)?;
@ -96,6 +112,7 @@ impl Line {
}
}
/// Perpendicularly shifts the line over to the right. Width must be non-negative.
pub fn shift_right(&self, width: Distance) -> Line {
assert!(width >= Distance::ZERO);
let angle = self.angle().rotate_degs(90.0);
@ -105,6 +122,7 @@ impl Line {
)
}
/// Perpendicularly shifts the line over to the left. Width must be non-negative.
pub fn shift_left(&self, width: Distance) -> Line {
assert!(width >= Distance::ZERO);
let angle = self.angle().rotate_degs(-90.0);
@ -114,6 +132,7 @@ impl Line {
)
}
/// Perpendicularly shifts the line to the right if positive or left if negative.
pub fn shift_either_direction(&self, width: Distance) -> Line {
if width >= Distance::ZERO {
self.shift_right(width)
@ -122,14 +141,17 @@ impl Line {
}
}
/// Returns a reversed line segment
pub fn reverse(&self) -> Line {
Line::must_new(self.pt2(), self.pt1())
}
/// The angle of the line segment, from the first to the second point
pub fn angle(&self) -> Angle {
self.pt1().angle_to(self.pt2())
}
/// Returns a point along the line segment, unless the distance exceeds the segment's length.
pub fn dist_along(&self, dist: Distance) -> Option<Pt2D> {
let len = self.length();
if dist < Distance::ZERO || dist > len {
@ -137,6 +159,8 @@ impl Line {
}
self.percent_along(dist / len)
}
/// Equivalent to `self.dist_along(dist).unwrap()`. Use this to document an assertion at the
/// call-site.
pub fn must_dist_along(&self, dist: Distance) -> Pt2D {
self.dist_along(dist).unwrap()
}

View File

@ -242,7 +242,7 @@ impl Polygon {
const RESOLUTION: usize = 5;
let mut arc = |center: Pt2D, angle1_degs: f64, angle2_degs: f64| {
for i in 0..=RESOLUTION {
let angle = Angle::new_degs(
let angle = Angle::degrees(
angle1_degs + (angle2_degs - angle1_degs) * ((i as f64) / (RESOLUTION as f64)),
);
pts.push(center.project_away(Distance::meters(r), angle.invert_y()));

View File

@ -103,7 +103,7 @@ impl CompareTimes {
let y_label = {
let label = Text::from(Line(format!("{} (minutes)", y_name.into())))
.render(ctx)
.rotate(Angle::new_degs(90.0))
.rotate(Angle::degrees(90.0))
.autocrop();
// The text is already scaled; don't use Widget::draw_batch and scale it again.
JustDraw::wrap(ctx, label).centered_vert().margin_right(5)

View File

@ -154,7 +154,7 @@ impl FanChart {
// TODO Need ticks now to actually see where this goes
let batch = Text::from(Line(t.to_string()))
.render(ctx)
.rotate(Angle::new_degs(-15.0))
.rotate(Angle::degrees(-15.0))
.autocrop();
// The text is already scaled; don't use Widget::draw_batch and scale it again.
row.push(JustDraw::wrap(ctx, batch));

View File

@ -175,7 +175,7 @@ impl<T: Yvalue<T>> LinePlot<T> {
// TODO Need ticks now to actually see where this goes
let batch = Text::from(Line(t.to_string()))
.render(ctx)
.rotate(Angle::new_degs(-15.0))
.rotate(Angle::degrees(-15.0))
.autocrop();
// The text is already scaled; don't use Widget::draw_batch and scale it again.
row.push(JustDraw::wrap(ctx, batch));

View File

@ -152,7 +152,7 @@ impl ScatterPlot {
// TODO Need ticks now to actually see where this goes
let batch = Text::from(Line(t.to_string()))
.render(ctx)
.rotate(Angle::new_degs(-15.0))
.rotate(Angle::degrees(-15.0))
.autocrop();
// The text is already scaled; don't use Widget::draw_batch and scale it again.
row.push(JustDraw::wrap(ctx, batch));

View File

@ -262,7 +262,7 @@ fn setup_scrollable_canvas(ctx: &mut EventCtx) -> Drawable {
.render_to_batch(&ctx.prerender)
.scale(2.0)
.centered_on(Pt2D::new(600.0, 500.0))
.rotate(Angle::new_degs(-30.0)),
.rotate(Angle::degrees(-30.0)),
);
let mut rng = if cfg!(target_arch = "wasm32") {