mirror of
https://github.com/emilk/egui.git
synced 2026-06-28 07:23:13 -04:00
## What (written by @emilk) When editing long text (thousands of line), egui would previously re-layout the entire text on each edit. This could be slow. With this PR, we instead split the text into paragraphs (split on `\n`) and then cache each such paragraph. When editing text then, only the changed paragraph needs to be laid out again. Still, there is overhead from splitting the text, hashing each paragraph, and then joining the results, so the runtime complexity is still O(N). In our benchmark, editing a 2000 line string goes from ~8ms to ~300 ms, a speedup of ~25x. In the future, we could also consider laying out each paragraph in parallel, to speed up the initial layout of the text. ## Details This is an ~~almost complete~~ implementation of the approach described by emilk [in this comment](<https://github.com/emilk/egui/issues/3086#issuecomment-1724205777>), excluding CoW semantics for `LayoutJob` (but including them for `Row`). It supersedes the previous unsuccessful attempt here: https://github.com/emilk/egui/pull/4000. Draft because: - [X] ~~Currently individual rows will have `ends_with_newline` always set to false. This breaks selection with Ctrl+A (and probably many other things)~~ - [X] ~~The whole block for doing the splitting and merging should probably become a function (I'll do that later).~~ - [X] ~~I haven't run the check script, the tests, and haven't made sure all of the examples build (although I assume they probably don't rely on Galley internals).~~ - [x] ~~Layout is sometimes incorrect (missing empty lines, wrapping sometimes makes text overlap).~~ - A lot of text-related code had to be changed so this needs to be properly tested to ensure no layout issues were introduced, especially relating to the now row-relative coordinate system of `Row`s. Also this requires that we're fine making these very breaking changes. It does significantly improve the performance of rendering large blocks of text (if they have many newlines), this is the test program I used to test it (adapted from <https://github.com/emilk/egui/issues/3086>): <details> <summary>code</summary> ```rust use eframe::egui::{self, CentralPanel, TextEdit}; use std::fmt::Write; fn main() -> Result<(), eframe::Error> { let options = eframe::NativeOptions { ..Default::default() }; eframe::run_native( "editor big file test", options, Box::new(|_cc| Ok(Box::<MyApp>::new(MyApp::new()))), ) } struct MyApp { text: String, } impl MyApp { fn new() -> Self { let mut string = String::new(); for line_bytes in (0..50000).map(|_| (0u8..50)) { for byte in line_bytes { write!(string, " {byte:02x}").unwrap(); } write!(string, "\n").unwrap(); } println!("total bytes: {}", string.len()); MyApp { text: string } } } impl eframe::App for MyApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { CentralPanel::default().show(ctx, |ui| { let start = std::time::Instant::now(); egui::ScrollArea::vertical().show(ui, |ui| { let code_editor = TextEdit::multiline(&mut self.text) .code_editor() .desired_width(f32::INFINITY) .desired_rows(40); let response = code_editor.show(ui).response; if response.changed() { println!("total bytes now: {}", self.text.len()); } }); let end = std::time::Instant::now(); let time_to_update = end - start; if time_to_update.as_secs_f32() > 0.5 { println!("Long update took {:.3}s", time_to_update.as_secs_f32()) } }); } } ``` </details> I think the way to proceed would be to make a new type, something like `PositionedRow`, that would wrap an `Arc<Row>` but have a separate `pos` ~~and `ends_with_newline`~~ (that would mean `Row` only holds a `size` instead of a `rect`). This type would of course have getters that would allow you to easily get a `Rect` from it and probably a `Deref` to the underlying `Row`. ~~I haven't done this yet because I wanted to get some opinions whether this would be an acceptable API first.~~ This is now implemented, but of course I'm still open to discussion about this approach and whether it's what we want to do. Breaking changes (currently): - The `Galley::rows` field has a different type. - There is now a `PlacedRow` wrapper for `Row`. - `Row` now uses a coordinate system relative to itself instead of the `Galley`. * Closes <https://github.com/emilk/egui/issues/3086> * [X] I have followed the instructions in the PR template --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
889 lines
24 KiB
Rust
889 lines
24 KiB
Rust
use std::fmt;
|
|
|
|
use crate::{lerp, pos2, vec2, Div, Mul, Pos2, Rangef, Rot2, Vec2};
|
|
|
|
/// A rectangular region of space.
|
|
///
|
|
/// Usually a [`Rect`] has a positive (or zero) size,
|
|
/// and then [`Self::min`] `<=` [`Self::max`].
|
|
/// In these cases [`Self::min`] is the left-top corner
|
|
/// and [`Self::max`] is the right-bottom corner.
|
|
///
|
|
/// A rectangle is allowed to have a negative size, which happens when the order
|
|
/// of `min` and `max` are swapped. These are usually a sign of an error.
|
|
///
|
|
/// Normally the unit is points (logical pixels) in screen space coordinates.
|
|
///
|
|
/// `Rect` does NOT implement `Default`, because there is no obvious default value.
|
|
/// [`Rect::ZERO`] may seem reasonable, but when used as a bounding box, [`Rect::NOTHING`]
|
|
/// is a better default - so be explicit instead!
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Eq, PartialEq)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
|
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
|
pub struct Rect {
|
|
/// One of the corners of the rectangle, usually the left top one.
|
|
pub min: Pos2,
|
|
|
|
/// The other corner, opposing [`Self::min`]. Usually the right bottom one.
|
|
pub max: Pos2,
|
|
}
|
|
|
|
impl Rect {
|
|
/// Infinite rectangle that contains every point.
|
|
pub const EVERYTHING: Self = Self {
|
|
min: pos2(-f32::INFINITY, -f32::INFINITY),
|
|
max: pos2(f32::INFINITY, f32::INFINITY),
|
|
};
|
|
|
|
/// The inverse of [`Self::EVERYTHING`]: stretches from positive infinity to negative infinity.
|
|
/// Contains no points.
|
|
///
|
|
/// This is useful as the seed for bounding boxes.
|
|
///
|
|
/// # Example:
|
|
/// ```
|
|
/// # use emath::*;
|
|
/// let mut rect = Rect::NOTHING;
|
|
/// assert!(rect.size() == Vec2::splat(-f32::INFINITY));
|
|
/// assert!(rect.contains(pos2(0.0, 0.0)) == false);
|
|
/// rect.extend_with(pos2(2.0, 1.0));
|
|
/// rect.extend_with(pos2(0.0, 3.0));
|
|
/// assert_eq!(rect, Rect::from_min_max(pos2(0.0, 1.0), pos2(2.0, 3.0)))
|
|
/// ```
|
|
pub const NOTHING: Self = Self {
|
|
min: pos2(f32::INFINITY, f32::INFINITY),
|
|
max: pos2(-f32::INFINITY, -f32::INFINITY),
|
|
};
|
|
|
|
/// An invalid [`Rect`] filled with [`f32::NAN`].
|
|
pub const NAN: Self = Self {
|
|
min: pos2(f32::NAN, f32::NAN),
|
|
max: pos2(f32::NAN, f32::NAN),
|
|
};
|
|
|
|
/// A [`Rect`] filled with zeroes.
|
|
pub const ZERO: Self = Self {
|
|
min: Pos2::ZERO,
|
|
max: Pos2::ZERO,
|
|
};
|
|
|
|
#[inline(always)]
|
|
pub const fn from_min_max(min: Pos2, max: Pos2) -> Self {
|
|
Self { min, max }
|
|
}
|
|
|
|
/// left-top corner plus a size (stretching right-down).
|
|
#[inline(always)]
|
|
pub fn from_min_size(min: Pos2, size: Vec2) -> Self {
|
|
Self {
|
|
min,
|
|
max: min + size,
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn from_center_size(center: Pos2, size: Vec2) -> Self {
|
|
Self {
|
|
min: center - size * 0.5,
|
|
max: center + size * 0.5,
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn from_x_y_ranges(x_range: impl Into<Rangef>, y_range: impl Into<Rangef>) -> Self {
|
|
let x_range = x_range.into();
|
|
let y_range = y_range.into();
|
|
Self {
|
|
min: pos2(x_range.min, y_range.min),
|
|
max: pos2(x_range.max, y_range.max),
|
|
}
|
|
}
|
|
|
|
/// Returns the bounding rectangle of the two points.
|
|
#[inline]
|
|
pub fn from_two_pos(a: Pos2, b: Pos2) -> Self {
|
|
Self {
|
|
min: pos2(a.x.min(b.x), a.y.min(b.y)),
|
|
max: pos2(a.x.max(b.x), a.y.max(b.y)),
|
|
}
|
|
}
|
|
|
|
/// A zero-sized rect at a specific point.
|
|
#[inline]
|
|
pub fn from_pos(point: Pos2) -> Self {
|
|
Self {
|
|
min: point,
|
|
max: point,
|
|
}
|
|
}
|
|
|
|
/// Bounding-box around the points.
|
|
pub fn from_points(points: &[Pos2]) -> Self {
|
|
let mut rect = Self::NOTHING;
|
|
for &p in points {
|
|
rect.extend_with(p);
|
|
}
|
|
rect
|
|
}
|
|
|
|
/// A [`Rect`] that contains every point to the right of the given X coordinate.
|
|
#[inline]
|
|
pub fn everything_right_of(left_x: f32) -> Self {
|
|
let mut rect = Self::EVERYTHING;
|
|
rect.set_left(left_x);
|
|
rect
|
|
}
|
|
|
|
/// A [`Rect`] that contains every point to the left of the given X coordinate.
|
|
#[inline]
|
|
pub fn everything_left_of(right_x: f32) -> Self {
|
|
let mut rect = Self::EVERYTHING;
|
|
rect.set_right(right_x);
|
|
rect
|
|
}
|
|
|
|
/// A [`Rect`] that contains every point below a certain y coordinate
|
|
#[inline]
|
|
pub fn everything_below(top_y: f32) -> Self {
|
|
let mut rect = Self::EVERYTHING;
|
|
rect.set_top(top_y);
|
|
rect
|
|
}
|
|
|
|
/// A [`Rect`] that contains every point above a certain y coordinate
|
|
#[inline]
|
|
pub fn everything_above(bottom_y: f32) -> Self {
|
|
let mut rect = Self::EVERYTHING;
|
|
rect.set_bottom(bottom_y);
|
|
rect
|
|
}
|
|
|
|
#[must_use]
|
|
#[inline]
|
|
pub fn with_min_x(mut self, min_x: f32) -> Self {
|
|
self.min.x = min_x;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
#[inline]
|
|
pub fn with_min_y(mut self, min_y: f32) -> Self {
|
|
self.min.y = min_y;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
#[inline]
|
|
pub fn with_max_x(mut self, max_x: f32) -> Self {
|
|
self.max.x = max_x;
|
|
self
|
|
}
|
|
|
|
#[must_use]
|
|
#[inline]
|
|
pub fn with_max_y(mut self, max_y: f32) -> Self {
|
|
self.max.y = max_y;
|
|
self
|
|
}
|
|
|
|
/// Expand by this much in each direction, keeping the center
|
|
#[must_use]
|
|
pub fn expand(self, amnt: f32) -> Self {
|
|
self.expand2(Vec2::splat(amnt))
|
|
}
|
|
|
|
/// Expand by this much in each direction, keeping the center
|
|
#[must_use]
|
|
pub fn expand2(self, amnt: Vec2) -> Self {
|
|
Self::from_min_max(self.min - amnt, self.max + amnt)
|
|
}
|
|
|
|
/// Scale up by this factor in each direction, keeping the center
|
|
#[must_use]
|
|
pub fn scale_from_center(self, scale_factor: f32) -> Self {
|
|
self.scale_from_center2(Vec2::splat(scale_factor))
|
|
}
|
|
|
|
/// Scale up by this factor in each direction, keeping the center
|
|
#[must_use]
|
|
pub fn scale_from_center2(self, scale_factor: Vec2) -> Self {
|
|
Self::from_center_size(self.center(), self.size() * scale_factor)
|
|
}
|
|
|
|
/// Shrink by this much in each direction, keeping the center
|
|
#[must_use]
|
|
pub fn shrink(self, amnt: f32) -> Self {
|
|
self.shrink2(Vec2::splat(amnt))
|
|
}
|
|
|
|
/// Shrink by this much in each direction, keeping the center
|
|
#[must_use]
|
|
pub fn shrink2(self, amnt: Vec2) -> Self {
|
|
Self::from_min_max(self.min + amnt, self.max - amnt)
|
|
}
|
|
|
|
#[must_use]
|
|
#[inline]
|
|
pub fn translate(self, amnt: Vec2) -> Self {
|
|
Self::from_min_size(self.min + amnt, self.size())
|
|
}
|
|
|
|
/// Rotate the bounds (will expand the [`Rect`])
|
|
#[must_use]
|
|
#[inline]
|
|
pub fn rotate_bb(self, rot: Rot2) -> Self {
|
|
let a = rot * self.left_top().to_vec2();
|
|
let b = rot * self.right_top().to_vec2();
|
|
let c = rot * self.left_bottom().to_vec2();
|
|
let d = rot * self.right_bottom().to_vec2();
|
|
|
|
Self::from_min_max(
|
|
a.min(b).min(c).min(d).to_pos2(),
|
|
a.max(b).max(c).max(d).to_pos2(),
|
|
)
|
|
}
|
|
|
|
#[must_use]
|
|
#[inline]
|
|
pub fn intersects(self, other: Self) -> bool {
|
|
self.min.x <= other.max.x
|
|
&& other.min.x <= self.max.x
|
|
&& self.min.y <= other.max.y
|
|
&& other.min.y <= self.max.y
|
|
}
|
|
|
|
/// keep min
|
|
pub fn set_width(&mut self, w: f32) {
|
|
self.max.x = self.min.x + w;
|
|
}
|
|
|
|
/// keep min
|
|
pub fn set_height(&mut self, h: f32) {
|
|
self.max.y = self.min.y + h;
|
|
}
|
|
|
|
/// Keep size
|
|
pub fn set_center(&mut self, center: Pos2) {
|
|
*self = self.translate(center - self.center());
|
|
}
|
|
|
|
#[must_use]
|
|
#[inline(always)]
|
|
pub fn contains(&self, p: Pos2) -> bool {
|
|
self.min.x <= p.x && p.x <= self.max.x && self.min.y <= p.y && p.y <= self.max.y
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn contains_rect(&self, other: Self) -> bool {
|
|
self.contains(other.min) && self.contains(other.max)
|
|
}
|
|
|
|
/// Return the given points clamped to be inside the rectangle
|
|
/// Panics if [`Self::is_negative`].
|
|
#[must_use]
|
|
pub fn clamp(&self, p: Pos2) -> Pos2 {
|
|
p.clamp(self.min, self.max)
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn extend_with(&mut self, p: Pos2) {
|
|
self.min = self.min.min(p);
|
|
self.max = self.max.max(p);
|
|
}
|
|
|
|
#[inline(always)]
|
|
/// Expand to include the given x coordinate
|
|
pub fn extend_with_x(&mut self, x: f32) {
|
|
self.min.x = self.min.x.min(x);
|
|
self.max.x = self.max.x.max(x);
|
|
}
|
|
|
|
#[inline(always)]
|
|
/// Expand to include the given y coordinate
|
|
pub fn extend_with_y(&mut self, y: f32) {
|
|
self.min.y = self.min.y.min(y);
|
|
self.max.y = self.max.y.max(y);
|
|
}
|
|
|
|
/// The union of two bounding rectangle, i.e. the minimum [`Rect`]
|
|
/// that contains both input rectangles.
|
|
#[inline(always)]
|
|
#[must_use]
|
|
pub fn union(self, other: Self) -> Self {
|
|
Self {
|
|
min: self.min.min(other.min),
|
|
max: self.max.max(other.max),
|
|
}
|
|
}
|
|
|
|
/// The intersection of two [`Rect`], i.e. the area covered by both.
|
|
#[inline]
|
|
#[must_use]
|
|
pub fn intersect(self, other: Self) -> Self {
|
|
Self {
|
|
min: self.min.max(other.min),
|
|
max: self.max.min(other.max),
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn center(&self) -> Pos2 {
|
|
Pos2 {
|
|
x: (self.min.x + self.max.x) / 2.0,
|
|
y: (self.min.y + self.max.y) / 2.0,
|
|
}
|
|
}
|
|
|
|
/// `rect.size() == Vec2 { x: rect.width(), y: rect.height() }`
|
|
#[inline(always)]
|
|
pub fn size(&self) -> Vec2 {
|
|
self.max - self.min
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn width(&self) -> f32 {
|
|
self.max.x - self.min.x
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn height(&self) -> f32 {
|
|
self.max.y - self.min.y
|
|
}
|
|
|
|
/// Width / height
|
|
///
|
|
/// * `aspect_ratio < 1`: portrait / high
|
|
/// * `aspect_ratio = 1`: square
|
|
/// * `aspect_ratio > 1`: landscape / wide
|
|
pub fn aspect_ratio(&self) -> f32 {
|
|
self.width() / self.height()
|
|
}
|
|
|
|
/// `[2, 1]` for wide screen, and `[1, 2]` for portrait, etc.
|
|
/// At least one dimension = 1, the other >= 1
|
|
/// Returns the proportions required to letter-box a square view area.
|
|
pub fn square_proportions(&self) -> Vec2 {
|
|
let w = self.width();
|
|
let h = self.height();
|
|
if w > h {
|
|
vec2(w / h, 1.0)
|
|
} else {
|
|
vec2(1.0, h / w)
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn area(&self) -> f32 {
|
|
self.width() * self.height()
|
|
}
|
|
|
|
/// The distance from the rect to the position.
|
|
///
|
|
/// The distance is zero when the position is in the interior of the rectangle.
|
|
///
|
|
/// [Negative rectangles](Self::is_negative) always return [`f32::INFINITY`].
|
|
#[inline]
|
|
pub fn distance_to_pos(&self, pos: Pos2) -> f32 {
|
|
self.distance_sq_to_pos(pos).sqrt()
|
|
}
|
|
|
|
/// The distance from the rect to the position, squared.
|
|
///
|
|
/// The distance is zero when the position is in the interior of the rectangle.
|
|
///
|
|
/// [Negative rectangles](Self::is_negative) always return [`f32::INFINITY`].
|
|
#[inline]
|
|
pub fn distance_sq_to_pos(&self, pos: Pos2) -> f32 {
|
|
if self.is_negative() {
|
|
return f32::INFINITY;
|
|
}
|
|
|
|
let dx = if self.min.x > pos.x {
|
|
self.min.x - pos.x
|
|
} else if pos.x > self.max.x {
|
|
pos.x - self.max.x
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
let dy = if self.min.y > pos.y {
|
|
self.min.y - pos.y
|
|
} else if pos.y > self.max.y {
|
|
pos.y - self.max.y
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
dx * dx + dy * dy
|
|
}
|
|
|
|
/// Signed distance to the edge of the box.
|
|
///
|
|
/// Negative inside the box.
|
|
///
|
|
/// [Negative rectangles](Self::is_negative) always return [`f32::INFINITY`].
|
|
///
|
|
/// ```
|
|
/// # use emath::{pos2, Rect};
|
|
/// let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
|
|
/// assert_eq!(rect.signed_distance_to_pos(pos2(0.50, 0.50)), -0.50);
|
|
/// assert_eq!(rect.signed_distance_to_pos(pos2(0.75, 0.50)), -0.25);
|
|
/// assert_eq!(rect.signed_distance_to_pos(pos2(1.50, 0.50)), 0.50);
|
|
/// ```
|
|
pub fn signed_distance_to_pos(&self, pos: Pos2) -> f32 {
|
|
if self.is_negative() {
|
|
return f32::INFINITY;
|
|
}
|
|
|
|
let edge_distances = (pos - self.center()).abs() - self.size() * 0.5;
|
|
let inside_dist = edge_distances.max_elem().min(0.0);
|
|
let outside_dist = edge_distances.max(Vec2::ZERO).length();
|
|
inside_dist + outside_dist
|
|
}
|
|
|
|
/// Linearly interpolate so that `[0, 0]` is [`Self::min`] and
|
|
/// `[1, 1]` is [`Self::max`].
|
|
#[inline]
|
|
pub fn lerp_inside(&self, t: Vec2) -> Pos2 {
|
|
Pos2 {
|
|
x: lerp(self.min.x..=self.max.x, t.x),
|
|
y: lerp(self.min.y..=self.max.y, t.y),
|
|
}
|
|
}
|
|
|
|
/// Linearly self towards other rect.
|
|
#[inline]
|
|
pub fn lerp_towards(&self, other: &Self, t: f32) -> Self {
|
|
Self {
|
|
min: self.min.lerp(other.min, t),
|
|
max: self.max.lerp(other.max, t),
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn x_range(&self) -> Rangef {
|
|
Rangef::new(self.min.x, self.max.x)
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn y_range(&self) -> Rangef {
|
|
Rangef::new(self.min.y, self.max.y)
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn bottom_up_range(&self) -> Rangef {
|
|
Rangef::new(self.max.y, self.min.y)
|
|
}
|
|
|
|
/// `width < 0 || height < 0`
|
|
#[inline(always)]
|
|
pub fn is_negative(&self) -> bool {
|
|
self.max.x < self.min.x || self.max.y < self.min.y
|
|
}
|
|
|
|
/// `width > 0 && height > 0`
|
|
#[inline(always)]
|
|
pub fn is_positive(&self) -> bool {
|
|
self.min.x < self.max.x && self.min.y < self.max.y
|
|
}
|
|
|
|
/// True if all members are also finite.
|
|
#[inline(always)]
|
|
pub fn is_finite(&self) -> bool {
|
|
self.min.is_finite() && self.max.is_finite()
|
|
}
|
|
|
|
/// True if any member is NaN.
|
|
#[inline(always)]
|
|
pub fn any_nan(self) -> bool {
|
|
self.min.any_nan() || self.max.any_nan()
|
|
}
|
|
}
|
|
|
|
/// ## Convenience functions (assumes origin is towards left top):
|
|
impl Rect {
|
|
/// `min.x`
|
|
#[inline(always)]
|
|
pub fn left(&self) -> f32 {
|
|
self.min.x
|
|
}
|
|
|
|
/// `min.x`
|
|
#[inline(always)]
|
|
pub fn left_mut(&mut self) -> &mut f32 {
|
|
&mut self.min.x
|
|
}
|
|
|
|
/// `min.x`
|
|
#[inline(always)]
|
|
pub fn set_left(&mut self, x: f32) {
|
|
self.min.x = x;
|
|
}
|
|
|
|
/// `max.x`
|
|
#[inline(always)]
|
|
pub fn right(&self) -> f32 {
|
|
self.max.x
|
|
}
|
|
|
|
/// `max.x`
|
|
#[inline(always)]
|
|
pub fn right_mut(&mut self) -> &mut f32 {
|
|
&mut self.max.x
|
|
}
|
|
|
|
/// `max.x`
|
|
#[inline(always)]
|
|
pub fn set_right(&mut self, x: f32) {
|
|
self.max.x = x;
|
|
}
|
|
|
|
/// `min.y`
|
|
#[inline(always)]
|
|
pub fn top(&self) -> f32 {
|
|
self.min.y
|
|
}
|
|
|
|
/// `min.y`
|
|
#[inline(always)]
|
|
pub fn top_mut(&mut self) -> &mut f32 {
|
|
&mut self.min.y
|
|
}
|
|
|
|
/// `min.y`
|
|
#[inline(always)]
|
|
pub fn set_top(&mut self, y: f32) {
|
|
self.min.y = y;
|
|
}
|
|
|
|
/// `max.y`
|
|
#[inline(always)]
|
|
pub fn bottom(&self) -> f32 {
|
|
self.max.y
|
|
}
|
|
|
|
/// `max.y`
|
|
#[inline(always)]
|
|
pub fn bottom_mut(&mut self) -> &mut f32 {
|
|
&mut self.max.y
|
|
}
|
|
|
|
/// `max.y`
|
|
#[inline(always)]
|
|
pub fn set_bottom(&mut self, y: f32) {
|
|
self.max.y = y;
|
|
}
|
|
|
|
#[inline(always)]
|
|
#[doc(alias = "top_left")]
|
|
pub fn left_top(&self) -> Pos2 {
|
|
pos2(self.left(), self.top())
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn center_top(&self) -> Pos2 {
|
|
pos2(self.center().x, self.top())
|
|
}
|
|
|
|
#[inline(always)]
|
|
#[doc(alias = "top_right")]
|
|
pub fn right_top(&self) -> Pos2 {
|
|
pos2(self.right(), self.top())
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn left_center(&self) -> Pos2 {
|
|
pos2(self.left(), self.center().y)
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn right_center(&self) -> Pos2 {
|
|
pos2(self.right(), self.center().y)
|
|
}
|
|
|
|
#[inline(always)]
|
|
#[doc(alias = "bottom_left")]
|
|
pub fn left_bottom(&self) -> Pos2 {
|
|
pos2(self.left(), self.bottom())
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn center_bottom(&self) -> Pos2 {
|
|
pos2(self.center().x, self.bottom())
|
|
}
|
|
|
|
#[inline(always)]
|
|
#[doc(alias = "bottom_right")]
|
|
pub fn right_bottom(&self) -> Pos2 {
|
|
pos2(self.right(), self.bottom())
|
|
}
|
|
|
|
/// Split rectangle in left and right halves. `t` is expected to be in the (0,1) range.
|
|
pub fn split_left_right_at_fraction(&self, t: f32) -> (Self, Self) {
|
|
self.split_left_right_at_x(lerp(self.min.x..=self.max.x, t))
|
|
}
|
|
|
|
/// Split rectangle in left and right halves at the given `x` coordinate.
|
|
pub fn split_left_right_at_x(&self, split_x: f32) -> (Self, Self) {
|
|
let left = Self::from_min_max(self.min, Pos2::new(split_x, self.max.y));
|
|
let right = Self::from_min_max(Pos2::new(split_x, self.min.y), self.max);
|
|
(left, right)
|
|
}
|
|
|
|
/// Split rectangle in top and bottom halves. `t` is expected to be in the (0,1) range.
|
|
pub fn split_top_bottom_at_fraction(&self, t: f32) -> (Self, Self) {
|
|
self.split_top_bottom_at_y(lerp(self.min.y..=self.max.y, t))
|
|
}
|
|
|
|
/// Split rectangle in top and bottom halves at the given `y` coordinate.
|
|
pub fn split_top_bottom_at_y(&self, split_y: f32) -> (Self, Self) {
|
|
let top = Self::from_min_max(self.min, Pos2::new(self.max.x, split_y));
|
|
let bottom = Self::from_min_max(Pos2::new(self.min.x, split_y), self.max);
|
|
(top, bottom)
|
|
}
|
|
}
|
|
|
|
impl Rect {
|
|
/// Does this Rect intersect the given ray (where `d` is normalized)?
|
|
///
|
|
/// A ray that starts inside the rect will return `true`.
|
|
pub fn intersects_ray(&self, o: Pos2, d: Vec2) -> bool {
|
|
debug_assert!(
|
|
d.is_normalized(),
|
|
"expected normalized direction, but `d` has length {}",
|
|
d.length()
|
|
);
|
|
|
|
let mut tmin = -f32::INFINITY;
|
|
let mut tmax = f32::INFINITY;
|
|
|
|
if d.x != 0.0 {
|
|
let tx1 = (self.min.x - o.x) / d.x;
|
|
let tx2 = (self.max.x - o.x) / d.x;
|
|
|
|
tmin = tmin.max(tx1.min(tx2));
|
|
tmax = tmax.min(tx1.max(tx2));
|
|
}
|
|
|
|
if d.y != 0.0 {
|
|
let ty1 = (self.min.y - o.y) / d.y;
|
|
let ty2 = (self.max.y - o.y) / d.y;
|
|
|
|
tmin = tmin.max(ty1.min(ty2));
|
|
tmax = tmax.min(ty1.max(ty2));
|
|
}
|
|
|
|
0.0 <= tmax && tmin <= tmax
|
|
}
|
|
|
|
/// Where does a ray from the center intersect the rectangle?
|
|
///
|
|
/// `d` is the direction of the ray and assumed to be normalized.
|
|
pub fn intersects_ray_from_center(&self, d: Vec2) -> Pos2 {
|
|
debug_assert!(
|
|
d.is_normalized(),
|
|
"expected normalized direction, but `d` has length {}",
|
|
d.length()
|
|
);
|
|
|
|
let mut tmin = f32::NEG_INFINITY;
|
|
let mut tmax = f32::INFINITY;
|
|
|
|
for i in 0..2 {
|
|
let inv_d = 1.0 / -d[i];
|
|
let mut t0 = (self.min[i] - self.center()[i]) * inv_d;
|
|
let mut t1 = (self.max[i] - self.center()[i]) * inv_d;
|
|
|
|
if inv_d < 0.0 {
|
|
std::mem::swap(&mut t0, &mut t1);
|
|
}
|
|
|
|
tmin = tmin.max(t0);
|
|
tmax = tmax.min(t1);
|
|
}
|
|
|
|
let t = tmax.min(tmin);
|
|
self.center() + t * -d
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for Rect {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
if let Some(precision) = f.precision() {
|
|
write!(f, "[{1:.0$?} - {2:.0$?}]", precision, self.min, self.max)
|
|
} else {
|
|
write!(f, "[{:?} - {:?}]", self.min, self.max)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Rect {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str("[")?;
|
|
self.min.fmt(f)?;
|
|
f.write_str(" - ")?;
|
|
self.max.fmt(f)?;
|
|
f.write_str("]")?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// from (min, max) or (left top, right bottom)
|
|
impl From<[Pos2; 2]> for Rect {
|
|
#[inline]
|
|
fn from([min, max]: [Pos2; 2]) -> Self {
|
|
Self { min, max }
|
|
}
|
|
}
|
|
|
|
impl Mul<f32> for Rect {
|
|
type Output = Self;
|
|
|
|
#[inline]
|
|
fn mul(self, factor: f32) -> Self {
|
|
Self {
|
|
min: self.min * factor,
|
|
max: self.max * factor,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Mul<Rect> for f32 {
|
|
type Output = Rect;
|
|
|
|
#[inline]
|
|
fn mul(self, vec: Rect) -> Rect {
|
|
Rect {
|
|
min: self * vec.min,
|
|
max: self * vec.max,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Div<f32> for Rect {
|
|
type Output = Self;
|
|
|
|
#[inline]
|
|
fn div(self, factor: f32) -> Self {
|
|
Self {
|
|
min: self.min / factor,
|
|
max: self.max / factor,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_rect() {
|
|
let r = Rect::from_min_max(pos2(10.0, 10.0), pos2(20.0, 20.0));
|
|
assert_eq!(r.distance_sq_to_pos(pos2(15.0, 15.0)), 0.0);
|
|
assert_eq!(r.distance_sq_to_pos(pos2(10.0, 15.0)), 0.0);
|
|
assert_eq!(r.distance_sq_to_pos(pos2(10.0, 10.0)), 0.0);
|
|
|
|
assert_eq!(r.distance_sq_to_pos(pos2(5.0, 15.0)), 25.0); // left of
|
|
assert_eq!(r.distance_sq_to_pos(pos2(25.0, 15.0)), 25.0); // right of
|
|
assert_eq!(r.distance_sq_to_pos(pos2(15.0, 5.0)), 25.0); // above
|
|
assert_eq!(r.distance_sq_to_pos(pos2(15.0, 25.0)), 25.0); // below
|
|
assert_eq!(r.distance_sq_to_pos(pos2(25.0, 5.0)), 50.0); // right and above
|
|
}
|
|
|
|
#[test]
|
|
fn scale_rect() {
|
|
let c = pos2(100.0, 50.0);
|
|
let r = Rect::from_center_size(c, vec2(30.0, 60.0));
|
|
|
|
assert_eq!(
|
|
r.scale_from_center(2.0),
|
|
Rect::from_center_size(c, vec2(60.0, 120.0))
|
|
);
|
|
assert_eq!(
|
|
r.scale_from_center(0.5),
|
|
Rect::from_center_size(c, vec2(15.0, 30.0))
|
|
);
|
|
assert_eq!(
|
|
r.scale_from_center2(vec2(2.0, 3.0)),
|
|
Rect::from_center_size(c, vec2(60.0, 180.0))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ray_intersection() {
|
|
let rect = Rect::from_min_max(pos2(1.0, 1.0), pos2(3.0, 3.0));
|
|
|
|
println!("Righward ray from left:");
|
|
assert!(rect.intersects_ray(pos2(0.0, 2.0), Vec2::RIGHT));
|
|
|
|
println!("Righward ray from center:");
|
|
assert!(rect.intersects_ray(pos2(2.0, 2.0), Vec2::RIGHT));
|
|
|
|
println!("Righward ray from right:");
|
|
assert!(!rect.intersects_ray(pos2(4.0, 2.0), Vec2::RIGHT));
|
|
|
|
println!("Leftward ray from left:");
|
|
assert!(!rect.intersects_ray(pos2(0.0, 2.0), Vec2::LEFT));
|
|
|
|
println!("Leftward ray from center:");
|
|
assert!(rect.intersects_ray(pos2(2.0, 2.0), Vec2::LEFT));
|
|
|
|
println!("Leftward ray from right:");
|
|
assert!(rect.intersects_ray(pos2(4.0, 2.0), Vec2::LEFT));
|
|
}
|
|
|
|
#[test]
|
|
fn test_ray_from_center_intersection() {
|
|
let rect = Rect::from_min_max(pos2(1.0, 1.0), pos2(3.0, 3.0));
|
|
|
|
assert_eq!(
|
|
rect.intersects_ray_from_center(Vec2::RIGHT),
|
|
pos2(3.0, 2.0),
|
|
"rightward ray"
|
|
);
|
|
|
|
assert_eq!(
|
|
rect.intersects_ray_from_center(Vec2::UP),
|
|
pos2(2.0, 1.0),
|
|
"upward ray"
|
|
);
|
|
|
|
assert_eq!(
|
|
rect.intersects_ray_from_center(Vec2::LEFT),
|
|
pos2(1.0, 2.0),
|
|
"leftward ray"
|
|
);
|
|
|
|
assert_eq!(
|
|
rect.intersects_ray_from_center(Vec2::DOWN),
|
|
pos2(2.0, 3.0),
|
|
"downward ray"
|
|
);
|
|
|
|
assert_eq!(
|
|
rect.intersects_ray_from_center((Vec2::LEFT + Vec2::DOWN).normalized()),
|
|
pos2(1.0, 3.0),
|
|
"bottom-left corner ray"
|
|
);
|
|
|
|
assert_eq!(
|
|
rect.intersects_ray_from_center((Vec2::LEFT + Vec2::UP).normalized()),
|
|
pos2(1.0, 1.0),
|
|
"top-left corner ray"
|
|
);
|
|
|
|
assert_eq!(
|
|
rect.intersects_ray_from_center((Vec2::RIGHT + Vec2::DOWN).normalized()),
|
|
pos2(3.0, 3.0),
|
|
"bottom-right corner ray"
|
|
);
|
|
|
|
assert_eq!(
|
|
rect.intersects_ray_from_center((Vec2::RIGHT + Vec2::UP).normalized()),
|
|
pos2(3.0, 1.0),
|
|
"top-right corner ray"
|
|
);
|
|
}
|
|
}
|