diff --git a/crates/ui-egui/src/canvas/selection_marks.rs b/crates/ui-egui/src/canvas/selection_marks.rs index d0d01b312..0a7e0b60d 100644 --- a/crates/ui-egui/src/canvas/selection_marks.rs +++ b/crates/ui-egui/src/canvas/selection_marks.rs @@ -4,10 +4,13 @@ //! color and set of selected anchors. The marks of all objects are joined into one mesh, in the //! order they are drawn. A frame with nothing changed draws that mesh again. A pan moves the //! triangles of outlines and handles, and places the anchor squares again, because a square's edges -//! are rounded to whole pixels where it lands. A frame with a new zoom, rotation or display scale -//! draws plain shapes and keeps nothing, and the next frame with the same view builds the marks. +//! are rounded to whole pixels where it lands. Each subpath, handle and square is tested against the +//! view on its own: one out of view adds nothing to the mesh, and a subpath or handle gets its +//! triangles the first time it is in view. A frame with a new zoom, rotation or display scale draws +//! plain shapes and keeps nothing, and the next frame with the same view builds the marks. use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::ops::Range; use std::sync::{Arc, Weak}; use egui::emath::{GuiRounding as _, TSTransform}; @@ -101,17 +104,42 @@ impl ViewMap { } } +/// The triangles of one item in its run's mesh. +#[derive(Clone)] +struct Span { + vertices: Range, + indices: Range, +} + +/// A polyline, disc or slanted line in a [`Mark::Rigid`]. +struct RigidPiece { + /// The shape, in screen coordinates at the object's `base`, until it is first on screen. + shape: Option, + /// Where the shape draws at the object's `base`. + bounds: egui::Rect, + span: Span, +} + +/// A direction handle in a [`Mark::Handles`]: the line from its anchor and the dot at its end. +struct HandleLine { + from: [f64; 2], + to: [f64; 2], + /// Whether the line is, or is nearly, level or upright. Its triangles then move only by whole + /// pixels, and any other move draws it again. + rounded: bool, + /// The offset its triangles were drawn for. + at: [f64; 2], + /// Its triangles, from the first time it is on screen. + span: Option, +} + /// One run of an object's marks. enum Mark { /// Marks whose triangles keep their form when moved: polylines, discs and slanted lines. - /// `shapes` are in screen coordinates at the object's `base` and become `mesh` the first time - /// the run is on screen. Discs need the frame's own tessellator (`discs`). - Rigid { shapes: Vec, mesh: Mesh, bounds: egui::Rect, points: usize, discs: bool }, - /// Direction handles of one color, from [`ViewMap::view`]: the line from each anchor and the dot - /// at its end. They become `mesh` for the offset `at` the first time the run is on screen. With a - /// line that is, or is nearly, level or upright (`rounded`), `mesh` moves only by whole pixels, - /// and any other move draws the run again. - Handles { color: Color32, lines: Vec<[[f64; 2]; 2]>, rounded: bool, mesh: Mesh, at: [f64; 2], bounds: ([f64; 2], [f64; 2]) }, + /// `mesh` holds the triangles of the pieces that have been on screen. + Rigid { pieces: Vec, mesh: Mesh, points: usize }, + /// Direction handles of one color. `mesh` holds the triangles of those that have been on screen. + Handles { color: Color32, lines: Vec, mesh: Mesh }, /// Anchor squares in one style, at positions from [`ViewMap::view`]. Squares { color: Color32, filled: bool, size: f32, at: Vec<[f64; 2]> }, /// A line that is, or is nearly, level or upright: the tessellator rounds those to whole pixels. @@ -141,43 +169,30 @@ impl<'a> MarkList<'a> { } fn rigid(&mut self, shape: Shape) { - let rect = shape.visual_bounding_rect(); let count = match &shape { Shape::Path(path) => path.points.len(), _ => 1, }; - let disc = matches!(shape, Shape::Circle(_)); - if let Some(Mark::Rigid { shapes, bounds, points, discs, .. }) = self.marks.last_mut() { - *bounds = bounds.union(rect); + let piece = RigidPiece { bounds: shape.visual_bounding_rect(), shape: Some(shape), span: Span { vertices: 0..0, indices: 0..0 } }; + if let Some(Mark::Rigid { pieces, points, .. }) = self.marks.last_mut() { *points += count; - *discs |= disc; - shapes.push(shape); + pieces.push(piece); } else { - self.marks.push(Mark::Rigid { shapes: vec![shape], mesh: Mesh::default(), bounds: rect, points: count, discs: disc }); + self.marks.push(Mark::Rigid { pieces: vec![piece], mesh: Mesh::default(), points: count }); } } /// A direction handle: its line from `from` and the dot at `to`. fn handle(&mut self, from: Point, to: Point, color: Color32) { let (a, b) = (self.map.view(from), self.map.view(to)); - let is_rounded = is_rounded_line(self.map.screen(a), self.map.screen(b)); - let least = [a[0].min(b[0]), a[1].min(b[1])]; - let most = [a[0].max(b[0]), a[1].max(b[1])]; - if let Some(Mark::Handles { color: c, lines, rounded, bounds, .. }) = self.marks.last_mut() + let rounded = is_rounded_line(self.map.screen(a), self.map.screen(b)); + let line = HandleLine { from: a, to: b, rounded, at: [0.0; 2], span: None }; + if let Some(Mark::Handles { color: c, lines, .. }) = self.marks.last_mut() && *c == color { - *bounds = ([bounds.0[0].min(least[0]), bounds.0[1].min(least[1])], [bounds.1[0].max(most[0]), bounds.1[1].max(most[1])]); - *rounded |= is_rounded; - lines.push([a, b]); + lines.push(line); } else { - self.marks.push(Mark::Handles { - color, - lines: vec![[a, b]], - rounded: is_rounded, - mesh: Mesh::default(), - at: [0.0; 2], - bounds: (least, most), - }); + self.marks.push(Mark::Handles { color, lines: vec![line], mesh: Mesh::default() }); } } @@ -269,6 +284,14 @@ struct Dot { mesh: Mesh, } +/// A disc in one style, around the origin. +struct Disc { + radius: f32, + fill: Color32, + stroke: Stroke, + mesh: Mesh, +} + /// What one anchor square draws: the fill, then the stroke of a hollow one. fn square_shapes(rect: egui::Rect, color: Color32, filled: bool) -> [Option; 2] { if filled { @@ -307,7 +330,7 @@ struct Composer<'a> { /// Draws lines and squares that rounding keeps from being reused, culled by `clip`. tessellator: Tessellator, /// Draws what is kept for later frames, with nothing culled. Like `tessellator`, it has no discs, - /// so neither draws circles. + /// so neither draws circles: those come from `discs`, stamped where they land. keeper: Tessellator, /// Draws filled squares without the soft edge, when that edge falls between pixel centers: on a /// square rounded to whole pixels, each pixel center is then fully in or fully out. @@ -317,6 +340,7 @@ struct Composer<'a> { look: HandleLook<'a>, templates: &'a mut Vec