Add layer transforms, interaction in layer (#3906)
⚠️ Removes `Context::translate_layer`, replacing it with a sticky `set_transform_layer` Adds the capability to scale layers. Allows interaction with scaled and transformed widgets inside transformed layers. I've also added a demo of how to have zooming and panning in a window (see the video below). This probably closes #1811. Having a panning and zooming container would just be creating a new `Area` with a new id, and applying zooming and panning with `ctx.transform_layer`. I've run the github workflow scripts in my repository, so hopefully the formatting and `cargo cranky` is satisfied. I'm not sure if all call sites where transforms would be relevant have been handled. This might also be missing are transforming clipping rects, but I'm not sure where / how to accomplish that. In the demo, the clipping rect is transformed to match, which seems to work. https://github.com/emilk/egui/assets/70821802/77e7e743-cdfe-402f-86e3-7744b3ee7b0f --------- Co-authored-by: tweoss <fchua@puffer5.stanford.edu> Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
f7fc3b0154
commit
069d7a634d
|
|
@ -331,7 +331,7 @@ impl Area {
|
|||
);
|
||||
|
||||
if movable && move_response.dragged() {
|
||||
state.pivot_pos += ctx.input(|i| i.pointer.delta());
|
||||
state.pivot_pos += move_response.drag_delta();
|
||||
}
|
||||
|
||||
if (move_response.dragged() || move_response.clicked())
|
||||
|
|
|
|||
|
|
@ -247,6 +247,14 @@ impl SidePanel {
|
|||
.ctx()
|
||||
.layer_id_at(pointer)
|
||||
.map_or(true, |top_layer_id| top_layer_id == ui.layer_id());
|
||||
let pointer = if let Some(transform) = ui
|
||||
.ctx()
|
||||
.memory(|m| m.layer_transforms.get(&ui.layer_id()).cloned())
|
||||
{
|
||||
transform.inverse() * pointer
|
||||
} else {
|
||||
pointer
|
||||
};
|
||||
|
||||
let resize_x = side.opposite().side_x(panel_rect);
|
||||
let mouse_over_resize_line = we_are_on_top
|
||||
|
|
@ -708,6 +716,14 @@ impl TopBottomPanel {
|
|||
.ctx()
|
||||
.layer_id_at(pointer)
|
||||
.map_or(true, |top_layer_id| top_layer_id == ui.layer_id());
|
||||
let pointer = if let Some(transform) = ui
|
||||
.ctx()
|
||||
.memory(|m| m.layer_transforms.get(&ui.layer_id()).cloned())
|
||||
{
|
||||
transform.inverse() * pointer
|
||||
} else {
|
||||
pointer
|
||||
};
|
||||
|
||||
let resize_y = side.opposite().side_y(panel_rect);
|
||||
let mouse_over_resize_line = we_are_on_top
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};
|
||||
|
||||
use ahash::HashMap;
|
||||
use epaint::{mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *};
|
||||
use epaint::{
|
||||
emath::TSTransform, mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
animation_manager::AnimationManager,
|
||||
|
|
@ -1245,6 +1247,12 @@ impl Context {
|
|||
let is_interacted_with = res.is_pointer_button_down_on || clicked || res.drag_released;
|
||||
if is_interacted_with {
|
||||
res.interact_pointer_pos = input.pointer.interact_pos();
|
||||
if let (Some(transform), Some(pos)) = (
|
||||
memory.layer_transforms.get(&res.layer_id),
|
||||
&mut res.interact_pointer_pos,
|
||||
) {
|
||||
*pos = transform.inverse() * *pos;
|
||||
}
|
||||
}
|
||||
|
||||
if input.pointer.any_down() && !res.is_pointer_button_down_on {
|
||||
|
|
@ -1958,7 +1966,9 @@ impl ContextImpl {
|
|||
}
|
||||
}
|
||||
|
||||
let shapes = viewport.graphics.drain(self.memory.areas().order());
|
||||
let shapes = viewport
|
||||
.graphics
|
||||
.drain(self.memory.areas().order(), &self.memory.layer_transforms);
|
||||
|
||||
let mut repaint_needed = false;
|
||||
|
||||
|
|
@ -2266,13 +2276,19 @@ impl Context {
|
|||
}
|
||||
|
||||
impl Context {
|
||||
/// Move all the graphics at the given layer.
|
||||
/// Transform the graphics of the given layer.
|
||||
///
|
||||
/// Can be used to implement drag-and-drop (see relevant demo).
|
||||
pub fn translate_layer(&self, layer_id: LayerId, delta: Vec2) {
|
||||
if delta != Vec2::ZERO {
|
||||
self.graphics_mut(|g| g.entry(layer_id).translate(delta));
|
||||
}
|
||||
/// This is a sticky setting, remembered from one frame to the next.
|
||||
///
|
||||
/// Can be used to implement pan and zoom (see relevant demo).
|
||||
pub fn set_transform_layer(&self, layer_id: LayerId, transform: TSTransform) {
|
||||
self.memory_mut(|m| {
|
||||
if transform == TSTransform::IDENTITY {
|
||||
m.layer_transforms.remove(&layer_id)
|
||||
} else {
|
||||
m.layer_transforms.insert(layer_id, transform)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Top-most layer at the given position.
|
||||
|
|
@ -2302,6 +2318,12 @@ impl Context {
|
|||
///
|
||||
/// See also [`Response::contains_pointer`].
|
||||
pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool {
|
||||
let rect =
|
||||
if let Some(transform) = self.memory(|m| m.layer_transforms.get(&layer_id).cloned()) {
|
||||
transform * rect
|
||||
} else {
|
||||
rect
|
||||
};
|
||||
if !rect.is_positive() {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -2348,6 +2370,12 @@ impl Context {
|
|||
let mut blocking_widget = None;
|
||||
|
||||
self.write(|ctx| {
|
||||
let transform = ctx
|
||||
.memory
|
||||
.layer_transforms
|
||||
.get(&layer_id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let viewport = ctx.viewport();
|
||||
|
||||
// We add all widgets here, even non-interactive ones,
|
||||
|
|
@ -2367,6 +2395,8 @@ impl Context {
|
|||
if contains_pointer {
|
||||
let pointer_pos = viewport.input.pointer.interact_pos();
|
||||
if let Some(pointer_pos) = pointer_pos {
|
||||
// Apply the inverse transformation of this layer to the pointer pos.
|
||||
let pointer_pos = transform.inverse() * pointer_pos;
|
||||
if let Some(rects) = viewport.layer_rects_prev_frame.by_layer.get(&layer_id) {
|
||||
// Iterate backwards, i.e. topmost widgets first.
|
||||
for blocking in rects.iter().rev() {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//! are sometimes painted behind or in front of other things.
|
||||
|
||||
use crate::{Id, *};
|
||||
use epaint::{ClippedShape, Shape};
|
||||
use epaint::{emath::TSTransform, ClippedShape, Shape};
|
||||
|
||||
/// Different layer categories
|
||||
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
|
||||
|
|
@ -158,11 +158,11 @@ impl PaintList {
|
|||
self.0[idx.0].shape = Shape::Noop;
|
||||
}
|
||||
|
||||
/// Translate each [`Shape`] and clip rectangle by this much, in-place
|
||||
pub fn translate(&mut self, delta: Vec2) {
|
||||
/// Transform each [`Shape`] and clip rectangle by this much, in-place
|
||||
pub fn transform(&mut self, transform: TSTransform) {
|
||||
for ClippedShape { clip_rect, shape } in &mut self.0 {
|
||||
*clip_rect = clip_rect.translate(delta);
|
||||
shape.translate(delta);
|
||||
*clip_rect = transform.mul_rect(*clip_rect);
|
||||
shape.transform(transform);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,7 +194,11 @@ impl GraphicLayers {
|
|||
self.0[layer_id.order as usize].get_mut(&layer_id.id)
|
||||
}
|
||||
|
||||
pub fn drain(&mut self, area_order: &[LayerId]) -> Vec<ClippedShape> {
|
||||
pub fn drain(
|
||||
&mut self,
|
||||
area_order: &[LayerId],
|
||||
transforms: &ahash::HashMap<LayerId, TSTransform>,
|
||||
) -> Vec<ClippedShape> {
|
||||
crate::profile_function!();
|
||||
|
||||
let mut all_shapes: Vec<_> = Default::default();
|
||||
|
|
@ -211,6 +215,12 @@ impl GraphicLayers {
|
|||
for layer_id in area_order {
|
||||
if layer_id.order == order {
|
||||
if let Some(list) = order_map.get_mut(&layer_id.id) {
|
||||
if let Some(transform) = transforms.get(layer_id) {
|
||||
for clipped_shape in &mut list.0 {
|
||||
clipped_shape.clip_rect = *transform * clipped_shape.clip_rect;
|
||||
clipped_shape.shape.transform(*transform);
|
||||
}
|
||||
}
|
||||
all_shapes.append(&mut list.0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
#![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs
|
||||
|
||||
use ahash::HashMap;
|
||||
use epaint::emath::TSTransform;
|
||||
|
||||
use crate::{
|
||||
area, vec2,
|
||||
window::{self, WindowInteraction},
|
||||
|
|
@ -85,6 +88,9 @@ pub struct Memory {
|
|||
#[cfg_attr(feature = "persistence", serde(skip))]
|
||||
everything_is_visible: bool,
|
||||
|
||||
/// Transforms per layer
|
||||
pub layer_transforms: HashMap<LayerId, TSTransform>,
|
||||
|
||||
// -------------------------------------------------
|
||||
// Per-viewport:
|
||||
areas: ViewportIdMap<Areas>,
|
||||
|
|
@ -107,6 +113,7 @@ impl Default for Memory {
|
|||
viewport_id: Default::default(),
|
||||
window_interactions: Default::default(),
|
||||
areas: Default::default(),
|
||||
layer_transforms: Default::default(),
|
||||
popup: Default::default(),
|
||||
everything_is_visible: Default::default(),
|
||||
};
|
||||
|
|
@ -672,7 +679,8 @@ impl Memory {
|
|||
|
||||
/// Top-most layer at the given position.
|
||||
pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<LayerId> {
|
||||
self.areas().layer_id_at(pos, resize_interact_radius_side)
|
||||
self.areas()
|
||||
.layer_id_at(pos, resize_interact_radius_side, &self.layer_transforms)
|
||||
}
|
||||
|
||||
/// An iterator over all layers. Back-to-front. Top is last.
|
||||
|
|
@ -948,7 +956,12 @@ impl Areas {
|
|||
}
|
||||
|
||||
/// Top-most layer at the given position.
|
||||
pub fn layer_id_at(&self, pos: Pos2, resize_interact_radius_side: f32) -> Option<LayerId> {
|
||||
pub fn layer_id_at(
|
||||
&self,
|
||||
pos: Pos2,
|
||||
resize_interact_radius_side: f32,
|
||||
layer_transforms: &HashMap<LayerId, TSTransform>,
|
||||
) -> Option<LayerId> {
|
||||
for layer in self.order.iter().rev() {
|
||||
if self.is_visible(layer) {
|
||||
if let Some(state) = self.areas.get(&layer.id) {
|
||||
|
|
@ -959,6 +972,10 @@ impl Areas {
|
|||
rect = rect.expand(resize_interact_radius_side);
|
||||
}
|
||||
|
||||
if let Some(transform) = layer_transforms.get(layer) {
|
||||
rect = *transform * rect;
|
||||
}
|
||||
|
||||
if rect.contains(pos) {
|
||||
return Some(*layer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -330,7 +330,14 @@ impl Response {
|
|||
#[inline]
|
||||
pub fn drag_delta(&self) -> Vec2 {
|
||||
if self.dragged() {
|
||||
self.ctx.input(|i| i.pointer.delta())
|
||||
let mut delta = self.ctx.input(|i| i.pointer.delta());
|
||||
if let Some(scaling) = self
|
||||
.ctx
|
||||
.memory(|m| m.layer_transforms.get(&self.layer_id).map(|t| t.scaling))
|
||||
{
|
||||
delta /= scaling;
|
||||
}
|
||||
delta
|
||||
} else {
|
||||
Vec2::ZERO
|
||||
}
|
||||
|
|
@ -395,7 +402,14 @@ impl Response {
|
|||
#[inline]
|
||||
pub fn hover_pos(&self) -> Option<Pos2> {
|
||||
if self.hovered() {
|
||||
self.ctx.input(|i| i.pointer.hover_pos())
|
||||
let mut pos = self.ctx.input(|i| i.pointer.hover_pos())?;
|
||||
if let Some(transform) = self
|
||||
.ctx
|
||||
.memory(|m| m.layer_transforms.get(&self.layer_id).cloned())
|
||||
{
|
||||
pos = transform * pos;
|
||||
}
|
||||
Some(pos)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2176,7 +2176,8 @@ impl Ui {
|
|||
|
||||
if let Some(pointer_pos) = self.ctx().pointer_interact_pos() {
|
||||
let delta = pointer_pos - response.rect.center();
|
||||
self.ctx().translate_layer(layer_id, delta);
|
||||
self.ctx()
|
||||
.set_transform_layer(layer_id, emath::TSTransform::from_translation(delta));
|
||||
}
|
||||
|
||||
InnerResponse::new(inner, response)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ impl Default for Demos {
|
|||
Box::<super::MiscDemoWindow>::default(),
|
||||
Box::<super::multi_touch::MultiTouch>::default(),
|
||||
Box::<super::painting::Painting>::default(),
|
||||
Box::<super::pan_zoom::PanZoom>::default(),
|
||||
Box::<super::panels::Panels>::default(),
|
||||
Box::<super::plot_demo::PlotDemo>::default(),
|
||||
Box::<super::scrolling::Scrolling>::default(),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub mod misc_demo_window;
|
|||
pub mod multi_touch;
|
||||
pub mod paint_bezier;
|
||||
pub mod painting;
|
||||
pub mod pan_zoom;
|
||||
pub mod panels;
|
||||
pub mod password;
|
||||
pub mod plot_demo;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
use egui::emath::TSTransform;
|
||||
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct PanZoom {
|
||||
transform: TSTransform,
|
||||
drag_value: f32,
|
||||
}
|
||||
|
||||
impl Eq for PanZoom {}
|
||||
|
||||
impl super::Demo for PanZoom {
|
||||
fn name(&self) -> &'static str {
|
||||
"🗖 Pan Zoom"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
let window = egui::Window::new("Pan Zoom")
|
||||
.default_width(300.0)
|
||||
.default_height(300.0)
|
||||
.vscroll(false)
|
||||
.open(open);
|
||||
window.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for PanZoom {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.label(
|
||||
"Pan, zoom in, and zoom out with scrolling (see the plot demo for more instructions). \
|
||||
Double click on the background to reset.",
|
||||
);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add(crate::egui_github_link_file!());
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
let (id, rect) = ui.allocate_space(ui.available_size());
|
||||
let response = ui.interact(rect, id, egui::Sense::click_and_drag());
|
||||
// Allow dragging the background as well.
|
||||
if response.dragged() {
|
||||
self.transform.translation += response.drag_delta();
|
||||
}
|
||||
|
||||
// Plot-like reset
|
||||
if response.double_clicked() {
|
||||
self.transform = TSTransform::default();
|
||||
}
|
||||
|
||||
let transform =
|
||||
TSTransform::from_translation(ui.min_rect().left_top().to_vec2()) * self.transform;
|
||||
|
||||
if let Some(pointer) = ui.ctx().input(|i| i.pointer.hover_pos()) {
|
||||
// Note: doesn't catch zooming / panning if a button in this PanZoom container is hovered.
|
||||
if response.hovered() {
|
||||
let pointer_in_layer = transform.inverse() * pointer;
|
||||
let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
|
||||
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
|
||||
|
||||
// Zoom in on pointer:
|
||||
self.transform = self.transform
|
||||
* TSTransform::from_translation(pointer_in_layer.to_vec2())
|
||||
* TSTransform::from_scaling(zoom_delta)
|
||||
* TSTransform::from_translation(-pointer_in_layer.to_vec2());
|
||||
|
||||
// Pan:
|
||||
self.transform = TSTransform::from_translation(pan_delta) * self.transform;
|
||||
}
|
||||
}
|
||||
|
||||
for (id, pos, callback) in [
|
||||
(
|
||||
"a",
|
||||
egui::Pos2::new(0.0, 0.0),
|
||||
Box::new(|ui: &mut egui::Ui, _: &mut Self| ui.button("top left!"))
|
||||
as Box<dyn Fn(&mut egui::Ui, &mut Self) -> egui::Response>,
|
||||
),
|
||||
(
|
||||
"b",
|
||||
egui::Pos2::new(0.0, 120.0),
|
||||
Box::new(|ui: &mut egui::Ui, _| ui.button("bottom left?")),
|
||||
),
|
||||
(
|
||||
"c",
|
||||
egui::Pos2::new(120.0, 120.0),
|
||||
Box::new(|ui: &mut egui::Ui, _| ui.button("right bottom :D")),
|
||||
),
|
||||
(
|
||||
"d",
|
||||
egui::Pos2::new(120.0, 0.0),
|
||||
Box::new(|ui: &mut egui::Ui, _| ui.button("right top ):")),
|
||||
),
|
||||
(
|
||||
"e",
|
||||
egui::Pos2::new(60.0, 60.0),
|
||||
Box::new(|ui, state| {
|
||||
use egui::epaint::*;
|
||||
// Smiley face.
|
||||
let painter = ui.painter();
|
||||
painter.add(CircleShape::filled(pos2(0.0, -10.0), 1.0, Color32::YELLOW));
|
||||
painter.add(CircleShape::filled(pos2(10.0, -10.0), 1.0, Color32::YELLOW));
|
||||
painter.add(QuadraticBezierShape::from_points_stroke(
|
||||
[pos2(0.0, 0.0), pos2(5.0, 3.0), pos2(10.0, 0.0)],
|
||||
false,
|
||||
Color32::TRANSPARENT,
|
||||
Stroke::new(1.0, Color32::YELLOW),
|
||||
));
|
||||
|
||||
ui.add(egui::Slider::new(&mut state.drag_value, 0.0..=100.0).text("My value"))
|
||||
}),
|
||||
),
|
||||
] {
|
||||
let id = egui::Area::new(id)
|
||||
.default_pos(pos)
|
||||
// Need to cover up the pan_zoom demo window,
|
||||
// but may also cover over other windows.
|
||||
.order(egui::Order::Foreground)
|
||||
.show(ui.ctx(), |ui| {
|
||||
ui.set_clip_rect(transform.inverse() * rect);
|
||||
egui::Frame::default()
|
||||
.rounding(egui::Rounding::same(4.0))
|
||||
.inner_margin(egui::Margin::same(8.0))
|
||||
.stroke(ui.ctx().style().visuals.window_stroke)
|
||||
.fill(ui.style().visuals.panel_fill)
|
||||
.show(ui, |ui| {
|
||||
ui.style_mut().wrap = Some(false);
|
||||
callback(ui, self)
|
||||
});
|
||||
})
|
||||
.response
|
||||
.layer_id;
|
||||
ui.ctx().set_transform_layer(id, transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ mod rect;
|
|||
mod rect_transform;
|
||||
mod rot2;
|
||||
pub mod smart_aim;
|
||||
mod ts_transform;
|
||||
mod vec2;
|
||||
mod vec2b;
|
||||
|
||||
|
|
@ -48,6 +49,7 @@ pub use {
|
|||
rect::*,
|
||||
rect_transform::*,
|
||||
rot2::*,
|
||||
ts_transform::*,
|
||||
vec2::*,
|
||||
vec2b::*,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,146 @@
|
|||
use crate::{Pos2, Rect, Vec2};
|
||||
|
||||
/// Linearly transforms positions via a translation, then a scaling.
|
||||
///
|
||||
/// [`TSTransform`] first scales points with the scaling origin at `0, 0`
|
||||
/// (the top left corner), then translates them.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||
pub struct TSTransform {
|
||||
/// Scaling applied first, scaled around (0, 0).
|
||||
pub scaling: f32,
|
||||
|
||||
/// Translation amount, applied after scaling.
|
||||
pub translation: Vec2,
|
||||
}
|
||||
|
||||
impl Eq for TSTransform {}
|
||||
|
||||
impl Default for TSTransform {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self::IDENTITY
|
||||
}
|
||||
}
|
||||
|
||||
impl TSTransform {
|
||||
pub const IDENTITY: Self = Self {
|
||||
translation: Vec2::ZERO,
|
||||
scaling: 1.0,
|
||||
};
|
||||
|
||||
#[inline]
|
||||
/// Creates a new translation that first scales points around
|
||||
/// `(0, 0)`, then translates them.
|
||||
pub fn new(translation: Vec2, scaling: f32) -> Self {
|
||||
Self {
|
||||
translation,
|
||||
scaling,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from_translation(translation: Vec2) -> Self {
|
||||
Self::new(translation, 1.0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from_scaling(scaling: f32) -> Self {
|
||||
Self::new(Vec2::ZERO, scaling)
|
||||
}
|
||||
|
||||
/// Inverts the transform.
|
||||
///
|
||||
/// ```
|
||||
/// # use emath::{pos2, vec2, TSTransform};
|
||||
/// let p1 = pos2(2.0, 3.0);
|
||||
/// let p2 = pos2(12.0, 5.0);
|
||||
/// let ts = TSTransform::new(vec2(2.0, 3.0), 2.0);
|
||||
/// let inv = ts.inverse();
|
||||
/// assert_eq!(inv.mul_pos(p1), pos2(0.0, 0.0));
|
||||
/// assert_eq!(inv.mul_pos(p2), pos2(5.0, 1.0));
|
||||
///
|
||||
/// assert_eq!(ts.inverse().inverse(), ts);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn inverse(&self) -> Self {
|
||||
Self::new(-self.translation / self.scaling, 1.0 / self.scaling)
|
||||
}
|
||||
|
||||
/// Transforms the given coordinate.
|
||||
///
|
||||
/// ```
|
||||
/// # use emath::{pos2, vec2, TSTransform};
|
||||
/// let p1 = pos2(0.0, 0.0);
|
||||
/// let p2 = pos2(5.0, 1.0);
|
||||
/// let ts = TSTransform::new(vec2(2.0, 3.0), 2.0);
|
||||
/// assert_eq!(ts.mul_pos(p1), pos2(2.0, 3.0));
|
||||
/// assert_eq!(ts.mul_pos(p2), pos2(12.0, 5.0));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn mul_pos(&self, pos: Pos2) -> Pos2 {
|
||||
self.scaling * pos + self.translation
|
||||
}
|
||||
|
||||
/// Transforms the given rectangle.
|
||||
///
|
||||
/// ```
|
||||
/// # use emath::{pos2, vec2, Rect, TSTransform};
|
||||
/// let rect = Rect::from_min_max(pos2(5.0, 5.0), pos2(15.0, 10.0));
|
||||
/// let ts = TSTransform::new(vec2(1.0, 0.0), 3.0);
|
||||
/// let transformed = ts.mul_rect(rect);
|
||||
/// assert_eq!(transformed.min, pos2(16.0, 15.0));
|
||||
/// assert_eq!(transformed.max, pos2(46.0, 30.0));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn mul_rect(&self, rect: Rect) -> Rect {
|
||||
Rect {
|
||||
min: self.mul_pos(rect.min),
|
||||
max: self.mul_pos(rect.max),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms the position.
|
||||
impl std::ops::Mul<Pos2> for TSTransform {
|
||||
type Output = Pos2;
|
||||
|
||||
#[inline]
|
||||
fn mul(self, pos: Pos2) -> Pos2 {
|
||||
self.mul_pos(pos)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms the rectangle.
|
||||
impl std::ops::Mul<Rect> for TSTransform {
|
||||
type Output = Rect;
|
||||
|
||||
#[inline]
|
||||
fn mul(self, rect: Rect) -> Rect {
|
||||
self.mul_rect(rect)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Self> for TSTransform {
|
||||
type Output = Self;
|
||||
|
||||
#[inline]
|
||||
/// Applies the right hand side transform, then the left hand side.
|
||||
///
|
||||
/// ```
|
||||
/// # use emath::{TSTransform, vec2};
|
||||
/// let ts1 = TSTransform::new(vec2(1.0, 0.0), 2.0);
|
||||
/// let ts2 = TSTransform::new(vec2(-1.0, -1.0), 3.0);
|
||||
/// let ts_combined = TSTransform::new(vec2(2.0, -1.0), 6.0);
|
||||
/// assert_eq!(ts_combined, ts2 * ts1);
|
||||
/// ```
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
// Apply rhs first.
|
||||
Self {
|
||||
scaling: self.scaling * rhs.scaling,
|
||||
translation: self.translation + self.scaling * rhs.translation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -278,6 +278,13 @@ impl Mesh {
|
|||
}
|
||||
}
|
||||
|
||||
/// Transform the mesh in-place with the given transform.
|
||||
pub fn transform(&mut self, transform: TSTransform) {
|
||||
for v in &mut self.vertices {
|
||||
v.pos = transform * v.pos;
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate by some angle about an origin, in-place.
|
||||
///
|
||||
/// Origin is a position in screen space.
|
||||
|
|
|
|||
|
|
@ -356,48 +356,70 @@ impl Shape {
|
|||
}
|
||||
|
||||
/// Move the shape by this many points, in-place.
|
||||
pub fn translate(&mut self, delta: Vec2) {
|
||||
///
|
||||
/// If using a [`PaintCallback`], note that only the rect is scaled as opposed
|
||||
/// to other shapes where the stroke is also scaled.
|
||||
pub fn transform(&mut self, transform: TSTransform) {
|
||||
match self {
|
||||
Self::Noop => {}
|
||||
Self::Vec(shapes) => {
|
||||
for shape in shapes {
|
||||
shape.translate(delta);
|
||||
shape.transform(transform);
|
||||
}
|
||||
}
|
||||
Self::Circle(circle_shape) => {
|
||||
circle_shape.center += delta;
|
||||
circle_shape.center = transform * circle_shape.center;
|
||||
circle_shape.radius *= transform.scaling;
|
||||
circle_shape.stroke.width *= transform.scaling;
|
||||
}
|
||||
Self::LineSegment { points, .. } => {
|
||||
Self::LineSegment { points, stroke } => {
|
||||
for p in points {
|
||||
*p += delta;
|
||||
*p = transform * *p;
|
||||
}
|
||||
stroke.width *= transform.scaling;
|
||||
}
|
||||
Self::Path(path_shape) => {
|
||||
for p in &mut path_shape.points {
|
||||
*p += delta;
|
||||
*p = transform * *p;
|
||||
}
|
||||
path_shape.stroke.width *= transform.scaling;
|
||||
}
|
||||
Self::Rect(rect_shape) => {
|
||||
rect_shape.rect = rect_shape.rect.translate(delta);
|
||||
rect_shape.rect = transform * rect_shape.rect;
|
||||
rect_shape.stroke.width *= transform.scaling;
|
||||
}
|
||||
Self::Text(text_shape) => {
|
||||
text_shape.pos += delta;
|
||||
text_shape.pos = transform * text_shape.pos;
|
||||
|
||||
// Scale text:
|
||||
let galley = Arc::make_mut(&mut text_shape.galley);
|
||||
for row in &mut galley.rows {
|
||||
row.visuals.mesh_bounds = transform.scaling * row.visuals.mesh_bounds;
|
||||
for v in &mut row.visuals.mesh.vertices {
|
||||
v.pos = Pos2::new(transform.scaling * v.pos.x, transform.scaling * v.pos.y);
|
||||
}
|
||||
}
|
||||
|
||||
galley.mesh_bounds = transform.scaling * galley.mesh_bounds;
|
||||
galley.rect = transform.scaling * galley.rect;
|
||||
}
|
||||
Self::Mesh(mesh) => {
|
||||
mesh.translate(delta);
|
||||
mesh.transform(transform);
|
||||
}
|
||||
Self::QuadraticBezier(bezier_shape) => {
|
||||
bezier_shape.points[0] += delta;
|
||||
bezier_shape.points[1] += delta;
|
||||
bezier_shape.points[2] += delta;
|
||||
bezier_shape.points[0] = transform * bezier_shape.points[0];
|
||||
bezier_shape.points[1] = transform * bezier_shape.points[1];
|
||||
bezier_shape.points[2] = transform * bezier_shape.points[2];
|
||||
bezier_shape.stroke.width *= transform.scaling;
|
||||
}
|
||||
Self::CubicBezier(cubic_curve) => {
|
||||
for p in &mut cubic_curve.points {
|
||||
*p += delta;
|
||||
*p = transform * *p;
|
||||
}
|
||||
cubic_curve.stroke.width *= transform.scaling;
|
||||
}
|
||||
Self::Callback(shape) => {
|
||||
shape.rect = shape.rect.translate(delta);
|
||||
shape.rect = transform * shape.rect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -428,7 +428,10 @@ fn drag_source<R>(
|
|||
|
||||
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
|
||||
let delta = pointer_pos - res.response.rect.center();
|
||||
ui.ctx().translate_layer(layer_id, delta);
|
||||
ui.ctx().set_transform_layer(
|
||||
layer_id,
|
||||
eframe::emath::TSTransform::from_translation(delta),
|
||||
);
|
||||
}
|
||||
|
||||
res
|
||||
|
|
|
|||
Loading…
Reference in New Issue