diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index faffd897b..0b508abfa 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -80,6 +80,7 @@ impl VectorcraftApp { print_tiling: None, live_paint_gaps: None, hidden_chars: Default::default(), + selection_marks: Default::default(), selection_box: None, selection_bounds: None, content_bounds: None, diff --git a/crates/ui-egui/src/caches.rs b/crates/ui-egui/src/caches.rs index 736100377..ddb649708 100644 --- a/crates/ui-egui/src/caches.rs +++ b/crates/ui-egui/src/caches.rs @@ -41,6 +41,9 @@ pub struct CanvasCache { /// The marks Type → Show Hidden Characters drew on each text object in the last frame. A frame /// that does not draw them drops them ([`canvas::show`]). pub hidden_chars: canvas::HiddenChars, + /// The selected objects' outlines, anchors and handles as drawn in the last frame. A frame that + /// does not draw them drops them ([`canvas::show`]). + pub selection_marks: canvas::SelectionMarks, /// [`VectorcraftApp::selection_box`] for (document uid, revision, Use Preview Bounds). pub selection_box: Option<((u64, u64, bool), Option)>, /// [`VectorcraftApp::selection_bounds`] for (document uid, revision). diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 8e3315e35..6210c2826 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -28,6 +28,7 @@ mod outline; mod overlays; mod panes; mod raster; +mod selection_marks; mod task_bar; mod tool_overlays; mod view; @@ -65,6 +66,7 @@ pub use panes::Panes; pub(crate) use panes::{consolidate, pane, tile, tiled_area}; use raster::{changed_pixels, pixel_region, upload}; pub(crate) use raster::{color_image, color_image_owned}; +pub use selection_marks::SelectionMarks; use task_bar::task_bar; pub(crate) use task_bar::{has_colors, task_bar_rect}; pub use tool_overlays::HiddenChars; @@ -193,6 +195,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // that does not draw the overlay drops them, for example with the option off, with the edges // hidden, on the Home screen or with no document open. let hidden_chars = std::mem::take(&mut app.canvas.hidden_chars); + // The selection's kept marks. Likewise, a frame that does not draw them drops them. + let selection_marks = std::mem::take(&mut app.canvas.selection_marks); // A release ends the Image Trace panel's eye-button hold, also when the panel is no longer // drawn, so a later press elsewhere does not show the source image. if app.ui.trace_hold_source.is_some() && !ui.input(|i| i.pointer.any_down()) { @@ -336,7 +340,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } original_path_overlay(app, &painter, &xf); - let overlays = selection_visuals(app, &painter, &xf, (hidden_chars, hover_still), revolve_edit, view_info); + let overlays = selection_visuals(app, &painter, &xf, (hidden_chars, selection_marks, hover_still), revolve_edit, view_info); draw_overlays(&painter, &xf, &overlays, &t, HandleLook::of(&app.session.prefs)); if !revolve_edit { shaper_widget_overlay(app, &painter, &xf); @@ -894,7 +898,7 @@ fn selection_visuals( app: &mut VectorcraftApp, painter: &egui::Painter, xf: &Xf, - (hidden_chars, hover_still): (HiddenChars, bool), + (hidden_chars, mut marks, hover_still): (HiddenChars, SelectionMarks, bool), revolve_edit: bool, view_info: vectorcraft_engine::ViewInfo, ) -> Vec { @@ -910,7 +914,8 @@ fn selection_visuals( if hover_still { hover_highlight(app, painter, xf); } - selection_overlay(app, painter, xf, overlays.iter().any(|o| matches!(o, Overlay::CurvePoint { .. }))); + selection_overlay(app, painter, xf, overlays.iter().any(|o| matches!(o, Overlay::CurvePoint { .. })), &mut marks); + app.canvas.selection_marks = marks; draw_inside_overlay(app, painter, xf); mirror_axis_overlay(app, painter, xf); grid_repeat_overlay(app, painter, xf); diff --git a/crates/ui-egui/src/canvas/overlays.rs b/crates/ui-egui/src/canvas/overlays.rs index 206b095ca..8779380ec 100644 --- a/crates/ui-egui/src/canvas/overlays.rs +++ b/crates/ui-egui/src/canvas/overlays.rs @@ -1,7 +1,8 @@ use super::artboards::names_an_artboard; use super::input::marquee_through; -use super::outline::{KEY_OUTLINE, c32, node_outline, selection_hit_at, stroke_path, to_screen_path, too_many_anchors, walk_drawn}; -use super::{Color32, CornerRadius, NodeKind, Overlay, Point, Pos2, Shape, Stroke, StrokeKind, Tokens, VectorcraftApp, Xf, vec2}; +use super::outline::{KEY_OUTLINE, c32, node_outline, selection_hit_at, stroke_path, to_screen_path, too_many_anchors}; +use super::selection_marks::{MarkOptions, SelectionMarks}; +use super::{Color32, CornerRadius, NodeKind, Overlay, Pos2, Shape, Stroke, StrokeKind, Tokens, VectorcraftApp, Xf, vec2}; pub(super) fn hover_highlight(app: &VectorcraftApp, p: &egui::Painter, xf: &Xf) { let Some(h) = app.hover_doc else { return }; @@ -54,9 +55,9 @@ pub(super) fn selected_anchor(c: Color32) -> Color32 { #[derive(Clone, Copy)] pub(super) struct HandleLook<'a> { /// Points bigger (or smaller) than the default size 3, per step of Size (1–7). - grow: f32, + pub(super) grow: f32, /// `solid` (the default), `hollow` or `large`. - style: &'a str, + pub(super) style: &'a str, } impl<'a> HandleLook<'a> { @@ -65,17 +66,21 @@ impl<'a> HandleLook<'a> { } /// A direction handle's end at `c`: a solid dot, a hollow one or a larger solid one. - fn draw(self, p: &egui::Painter, c: Pos2, color: Color32) { + pub(super) fn shapes(self, c: Pos2, color: Color32) -> [Option; 2] { let r = 2.75 + self.grow / 2.0; let hollow = self.style == "hollow"; - p.circle_filled(c, if self.style == "large" { r + 1.5 } else { r }, if hollow { Color32::WHITE } else { color }); - if hollow { - p.circle_stroke(c, r, Stroke::new(1.0, color)); + let dot = Shape::circle_filled(c, if self.style == "large" { r + 1.5 } else { r }, if hollow { Color32::WHITE } else { color }); + [Some(dot), hollow.then(|| Shape::circle_stroke(c, r, Stroke::new(1.0, color)))] + } + + pub(super) fn draw(self, p: &egui::Painter, c: Pos2, color: Color32) { + for shape in self.shapes(c, color).into_iter().flatten() { + p.add(shape); } } } -fn anchor_square(p: &egui::Painter, c: Pos2, color: Color32, filled: bool, size: f32) { +pub(super) fn anchor_square(p: &egui::Painter, c: Pos2, color: Color32, filled: bool, size: f32) { let r = egui::Rect::from_center_size(c, vec2(size, size)); if filled { p.rect_filled(r, 0.0, color); @@ -85,11 +90,23 @@ fn anchor_square(p: &egui::Painter, c: Pos2, color: Color32, filled: bool, size: } } -fn curve_point(p: &egui::Painter, c: Pos2, color: Color32, filled: bool, size: f32) { +pub(super) fn curve_point(p: &egui::Painter, c: Pos2, color: Color32, filled: bool, size: f32) { p.circle(c, size / 2.0, if filled { color } else { Color32::WHITE }, Stroke::new(1.0, color)); } -pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf, curve_points_from_tool: bool) { +/// How the current tool and preferences draw the selection's marks. +pub(super) fn mark_options(app: &VectorcraftApp, curve_points_from_tool: bool) -> MarkOptions<'_> { + let tool = app.session.tool_id(); + MarkOptions { + direct: vectorcraft_tools::catalog::edits_anchors(tool), + curvature: tool == "curvature", + curve_points_from_tool, + look: HandleLook::of(&app.session.prefs), + all_handles: app.session.prefs.show_handles_multiple_anchors, + } +} + +pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: &Xf, curve_points_from_tool: bool, marks: &mut SelectionMarks) { // The Selection tool's bounding box (rotated with the objects after a rotation), also shown by // the shape tools that keep a shape just drawn ready to adjust while they draw none (#5477). It // hides while a drag moves or resizes the selection, so only the art is seen going with the @@ -121,130 +138,29 @@ pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: let symbols = st.doc.symbol_boxes(); let tool = app.session.tool_id(); let direct = vectorcraft_tools::catalog::edits_anchors(tool); - let curvature = tool == "curvature"; // Selection & Anchor Display › Size (1–7, 3 the default): anchors, handles and the bounding // box's handles a point bigger or smaller per step. - let look = HandleLook::of(&app.session.prefs); - let grow = look.grow; + let options = mark_options(app, curve_points_from_tool); + let grow = options.look.grow; let anchor = |direct: bool| grow + if direct { 5.0 } else { 4.0 }; - // The selected anchors' handles (drawn once they are counted: Show handles when multiple - // anchors are selected off shows them for a single one only). - let (mut handles, mut with_handles) = (vec![], 0); // Too many paths to outline: the selection's bounds stand for them. if let Some((b, id)) = big_bounds.zip(st.selection.objects.first()) { stroke_path(p, &vectorcraft_geom::shapes::rectangle(b).to_bezpath(), xf, Stroke::new(1.0, c32(st.doc.layer_color(*id)))); } // The key object (Align to Key Object): its outline drawn thicker, or its bounds when it has // too many paths to outline. - if let Some((k, n)) = st.selection.key.and_then(|k| Some((k, st.doc.node(k)?))) { - let outline = if too_many_anchors([n]) { - n.geometric_bounds_with(&symbols).map(|b| vectorcraft_geom::shapes::rectangle(b).to_bezpath()) - } else { - Some(node_outline(n, &symbols)) - }; - if let Some(bp) = outline { - stroke_path(p, &bp, xf, Stroke::new(KEY_OUTLINE, c32(st.doc.layer_color(k)))); - } - } - for id in st.selection.objects.iter().filter(|_| !big) { - let Some(n) = st.doc.node(*id) else { continue }; - let color = c32(st.doc.layer_color(*id)); - let partial = st.selection.partial(*id); - // Path outlines (and area type / placed document / image / mesh / symbol outlines; - // point type baselines and origin are drawn below). - if !matches!(n.kind, NodeKind::Text(ref tx) if matches!(tx.kind, vectorcraft_doc::TextKind::Point)) { - stroke_path(p, &node_outline(n, &symbols), xf, Stroke::new(1.0, color)); - } - // Anchors (and handles for selected anchors in direct mode); a gradient mesh's or a mesh - // envelope's points. - walk_drawn(n, &mut |c| { - let envelope = vectorcraft_doc::live::envelope_overlay(c).and_then(|(_, grid)| grid); - let grid = match &c.kind { - NodeKind::Mesh(m) => Some(m), - _ => envelope.as_ref(), - }; - if let Some(grid) = grid { - for q in &grid.points { - anchor_square(p, xf.to_screen(q.p), color, false, anchor(direct)); - } - return; - } - let NodeKind::Path { path, .. } = &c.kind else { return }; - // The Curvature tool owns its points while drawing; existing paths use this overlay. - if curvature && curve_points_from_tool { - return; - } - let selected_segments = st.selection.segments.get(&c.id); - if let Some(segments) = selected_segments { - for &(si, seg) in segments { - let Some(sp) = path.subpaths.get(si).filter(|sp| seg < sp.segment_count()) else { continue }; - let curve = sp.segment(seg); - let mut outline = vectorcraft_geom::BezPath::new(); - outline.move_to(curve.p0); - if sp.segment_is_line(seg) { - outline.line_to(curve.p3); - } else { - outline.curve_to(curve.p1, curve.p2, curve.p3); - } - stroke_path(p, &outline, xf, Stroke::new(2.0, color)); - } - } - for (si, ai, a) in path.anchors() { - // An object selected whole has all its anchors selected, and so do the paths inside a - // group, compound path or clip group selected whole. Direction handles show only on a - // path that is selected itself: those are the handles that `handle_anchors` in - // `vectorcraft-tools` lets the tools drag. - let (sel, handles_shown) = match partial { - Some(set) => { - let sel = set.contains(&(si, ai)); - (sel, sel) - } - None => (true, direct && c.id == *id), - }; - let sp = xf.to_screen(a.p); - if !curvature && handles_shown { - with_handles += 1; - let sides = path.subpaths.get(si).map(|subpath| st.selection.handle_sides(c.id, si, ai, subpath)).unwrap_or((true, true)); - handles.extend( - [(a.h_in, sides.0), (a.h_out, sides.1)] - .into_iter() - .filter(|(h, show)| *show && h.distance(a.p) > 1e-6) - .map(|(h, _)| (sp, xf.to_screen(h), color)), - ); - } - let anchor_selected = sel && selected_segments.is_none(); - let ink = if anchor_selected && partial.is_some() { selected_anchor(color) } else { color }; - if curvature { - curve_point(p, sp, ink, anchor_selected && partial.is_some(), grow + 6.0); - } else { - anchor_square(p, sp, ink, anchor_selected, anchor(partial.is_some() || direct)); - } - } - }); - // Centre point (Attributes panel → Show Center). - if n.shows_center() - && let Some(b) = n.geometric_bounds_with(&symbols) - { - anchor_square(p, xf.to_screen(b.center()), color, true, anchor(false)); - } - // Point type: each line's baseline, where a click selects it (`hit`), whatever its - // alignment and rotation, and the anchor (area type shows its frame instead, which may - // not be a rectangle, and type on a path its path). - if let NodeKind::Text(tx) = &n.kind - && matches!(tx.kind, vectorcraft_doc::TextKind::Point) - { - for (a, b) in tx.baselines() { - p.line_segment([xf.to_screen(tx.xf * a), xf.to_screen(tx.xf * b)], Stroke::new(1.0, color)); - } - p.circle_filled(xf.to_screen(tx.xf * Point::ZERO), 2.5, color); - } - } - if app.session.prefs.show_handles_multiple_anchors || with_handles <= 1 { - for (sp, hp, color) in handles { - p.line_segment([sp, hp], Stroke::new(1.0, color)); - look.draw(p, hp, color); - } + let key = st.selection.key.and_then(|k| Some((k, st.doc.node(k)?))); + let (key_bounds, key_outline) = match key { + Some((k, n)) if too_many_anchors([n]) => (Some((k, n)), None), + Some((k, _)) => (None, Some(k)), + None => (None, None), + }; + if let Some((k, n)) = key_bounds + && let Some(b) = n.geometric_bounds_with(&symbols) + { + stroke_path(p, &vectorcraft_geom::shapes::rectangle(b).to_bezpath(), xf, Stroke::new(KEY_OUTLINE, c32(st.doc.layer_color(k)))); } + marks.paint(st, p, xf, &options, key_outline, if big { &[] } else { &st.selection.objects }); // The spine of each selected blend (or of the blend a selected key object belongs to). let mut spines = vec![]; for id in &st.selection.objects { diff --git a/crates/ui-egui/src/canvas/selection_marks.rs b/crates/ui-egui/src/canvas/selection_marks.rs new file mode 100644 index 000000000..f99899863 --- /dev/null +++ b/crates/ui-egui/src/canvas/selection_marks.rs @@ -0,0 +1,941 @@ +//! The selected objects' outlines, anchors and handles, kept between frames. +//! +//! Outlines, handles and slanted lines are kept as triangles. Anchor squares, and level or upright +//! lines that are not handles, are kept as document points. Each selected object has an +//! [`ObjectMarks`], valid for one node (by [`Arc`] identity), layer color and set of selected +//! anchors, for the document and revision if the object uses symbols, and within [`MAX_PAN`]. 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. 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}; +use egui::epaint::{ClippedShape, Primitive, RectShape, TessellationOptions, Tessellator, Vertex}; +use egui::{Mesh, Vec2}; +use vectorcraft_doc::{AnchorRef, NodeId, SegmentRef}; + +use super::outline::{KEY_OUTLINE, c32, node_outline, to_screen_path, walk_drawn}; +use super::overlays::{HandleLook, anchor_square, selected_anchor}; +use super::{BezPath, Color32, Node, NodeKind, Point, Pos2, Shape, Stroke, StrokeKind, SymbolBoxes, Xf, pos2, vec2}; + +/// How far a pan moves an object's kept marks, on either axis, before the object is built again. +/// Screen coordinates are `f32`, which holds a coordinate under 2^16 to within 1/256 px, and a point +/// farther out than this plus the width of a window stays off screen. +const MAX_PAN: f64 = 16_384.0; + +/// The most points and vertices kept. Past this the marks are dropped after the frame that built +/// them. +const MAX_KEPT: usize = 8_000_000; + +/// The most anchor square meshes kept, one per color, style and size in pixels. +const MAX_TEMPLATES: usize = 256; + +/// The farthest a handle's dot and its stroke reach from the end of its line, at the default size. +const HANDLE_REACH: f32 = 6.0; + +/// Everything that decides how marks look, apart from the object and the view's position. +#[derive(Clone, PartialEq)] +struct MarkLook { + zoom: f64, + rot: f64, + pixels_per_point: f32, + tessellation: TessellationOptions, + /// Disc texture coordinates scale with the size of the font texture. + font_image_size: [usize; 2], + direct: bool, + curvature: bool, + curve_points_from_tool: bool, + grow: f32, + handle_style: String, + segments: BTreeMap>, +} + +/// The options [`SelectionMarks::paint`] draws with. +#[derive(Clone, Copy)] +pub(super) struct MarkOptions<'a> { + /// Whether the tool edits anchors. + pub direct: bool, + /// Whether the tool is the Curvature tool. + pub curvature: bool, + /// Whether the Curvature tool draws its own points. + pub curve_points_from_tool: bool, + /// Selection & Anchor Display › Size and Handles. + pub look: HandleLook<'a>, + /// Selection & Anchor Display › Show handles when multiple anchors are selected. + pub all_handles: bool, +} + +/// A view's position: the document point at the middle of the canvas, and that middle on screen. +#[derive(Clone, Copy, PartialEq)] +struct ViewAt { + origin: Pos2, + center: Point, +} + +/// [`Xf`] with its rotation worked out. +struct ViewMap { + sin: f64, + cos: f64, + zoom: f64, + at: ViewAt, +} + +impl ViewMap { + fn new(xf: &Xf) -> Self { + let (sin, cos) = xf.rot.sin_cos(); + Self { sin, cos, zoom: xf.zoom, at: ViewAt { origin: xf.rect.center(), center: xf.center } } + } + + /// A distance in the document, scaled and rotated by the view. + fn turned(&self, dx: f64, dy: f64) -> [f64; 2] { + let (x, y) = (dx * self.zoom, dy * self.zoom); + [x * self.cos - y * self.sin, x * self.sin + y * self.cos] + } + + /// The same result as [`Xf::to_screen`]. + fn to_screen(&self, p: Point) -> Pos2 { + let [x, y] = self.turned(p.x - self.at.center.x, p.y - self.at.center.y); + pos2(self.at.origin.x + x as f32, self.at.origin.y + y as f32) + } + + /// How far on screen a point moves between the view position `from` and this one. + fn moved_from(&self, from: ViewAt) -> [f64; 2] { + let [x, y] = self.turned(from.center.x - self.at.center.x, from.center.y - self.at.center.y); + [x + f64::from(self.at.origin.x - from.origin.x), y + f64::from(self.at.origin.y - from.origin.y)] + } + + fn moved_vec(&self, from: ViewAt) -> Vec2 { + let [x, y] = self.moved_from(from); + vec2(x as f32, y as f32) + } +} + +/// 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: Point, + to: Point, + /// 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 view position its triangles were drawn for. + at: ViewAt, + /// 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. + /// `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 these document points. + Squares { color: Color32, filled: bool, size: f32, at: Vec }, + /// A line that is, or is nearly, level or upright: the tessellator rounds those to whole pixels. + Segment { a: Point, b: Point, stroke: Stroke }, +} + +impl Mark { + fn kept(&self) -> usize { + match self { + Self::Rigid { mesh, points, .. } => mesh.vertices.len().max(*points), + Self::Handles { lines, mesh, .. } => mesh.vertices.len() + lines.len(), + Self::Squares { at, .. } => at.len(), + Self::Segment { .. } => 1, + } + } +} + +/// Builds a list of [`Mark`]s, joining neighbors that can be drawn as one run. +struct MarkList<'a> { + map: &'a ViewMap, + marks: Vec, +} + +impl<'a> MarkList<'a> { + fn new(map: &'a ViewMap) -> Self { + Self { map, marks: vec![] } + } + + fn rigid(&mut self, shape: Shape) { + let count = match &shape { + Shape::Path(path) => path.points.len(), + _ => 1, + }; + 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; + pieces.push(piece); + } else { + 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 rounded = is_rounded_line(self.map.to_screen(from), self.map.to_screen(to)); + let line = HandleLine { from, to, rounded, at: self.map.at, span: None }; + if let Some(Mark::Handles { color: c, lines, .. }) = self.marks.last_mut() + && *c == color + { + lines.push(line); + } else { + self.marks.push(Mark::Handles { color, lines: vec![line], mesh: Mesh::default() }); + } + } + + fn outline(&mut self, path: &BezPath, xf: &Xf, stroke: Stroke) { + for line in to_screen_path(path, xf) { + self.rigid(Shape::line(line, stroke)); + } + } + + fn square(&mut self, p: Point, color: Color32, filled: bool, size: f32) { + if let Some(Mark::Squares { color: c, filled: f, size: s, at }) = self.marks.last_mut() + && (*c, *f, *s) == (color, filled, size) + { + at.push(p); + } else { + self.marks.push(Mark::Squares { color, filled, size, at: vec![p] }); + } + } + + fn line(&mut self, from: Point, to: Point, stroke: Stroke) { + let (sa, sb) = (self.map.to_screen(from), self.map.to_screen(to)); + if is_rounded_line(sa, sb) { + self.marks.push(Mark::Segment { a: from, b: to, stroke }); + } else { + self.rigid(Shape::line_segment([sa, sb], stroke)); + } + } +} + +/// Whether a line from `a` to `b` is, or after a move could be, exactly level or upright. +fn is_rounded_line(a: Pos2, b: Pos2) -> bool { + (a.x - b.x).abs() < 0.002 || (a.y - b.y).abs() < 0.002 +} + +/// The marks of one object (or the key object's outline), and what they were built from. +struct ObjectMarks { + /// Holds the allocation, so that no later node can have this address. While it exists, + /// `Arc::make_mut` moves an edited node to a new address. + node: Weak, + color: Color32, + partial: Option>, + /// The document and revision, for an object that measures symbols' art. + symbols: Option<(u64, u64)>, + /// The view position when the marks were built. + base: ViewAt, + marks: Vec, + handles: Vec, + /// The anchors whose handles `handles` shows. + with_handles: usize, + seen: u64, +} + +impl ObjectMarks { + fn current(&self, node: &Arc, color: Color32, partial: Option<&BTreeSet>, source: (u64, u64), map: &ViewMap) -> bool { + let [x, y] = map.moved_from(self.base); + Weak::as_ptr(&self.node) == Arc::as_ptr(node) + && self.color == color + && self.partial.as_ref() == partial + && self.symbols.is_none_or(|s| s == source) + && x.abs() <= MAX_PAN + && y.abs() <= MAX_PAN + } +} + +/// The node `id` as held in the document, for its identity. +fn node_arc(doc: &vectorcraft_doc::Document, id: NodeId) -> Option<&Arc> { + let path = doc.index_path(id)?; + let (first, rest) = path.split_first()?; + let mut node = doc.layers.get(*first)?; + for i in rest { + node = node.children()?.get(*i)?; + } + Some(node) +} + +/// A square's triangles, relative to the corner it was rounded to. +struct Template { + color: Color32, + filled: bool, + size: f32, + pixels: (i32, i32), + mesh: Mesh, +} + +/// A handle's dot in one color, around the origin. +struct Dot { + color: Color32, + 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 { + [Some(RectShape::filled(rect, 0.0, color)), None] + } else { + [Some(RectShape::filled(rect, 0.0, Color32::WHITE)), Some(RectShape::stroke(rect, 0.0, Stroke::new(1.0, color), StrokeKind::Inside))] + } +} + +/// `shapes` as the frame's tessellator draws them, with nothing culled. +fn tessellated(ctx: &egui::Context, shapes: Vec, pixels_per_point: f32) -> Mesh { + let clipped = shapes.into_iter().map(|shape| ClippedShape { + clip_rect: egui::Rect::EVERYTHING, + shape, + transform_after_tessellation: TSTransform::IDENTITY, + }); + let primitives = ctx.tessellate(clipped.collect(), pixels_per_point); + primitives + .into_iter() + .find_map(|c| match c.primitive { + Primitive::Mesh(mesh) => Some(mesh), + Primitive::Callback(_) => None, + }) + .unwrap_or_default() +} + +/// Joins marks into the frame's mesh. +struct Composer<'a> { + ctx: &'a egui::Context, + map: &'a ViewMap, + clip: egui::Rect, + pixels_per_point: f32, + /// Squares this size or smaller are drawn as lines, which round differently: see + /// [`Tessellator::tessellate_rect`]. + thin: f32, + /// 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: 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. + crisp: Option, + /// Whether the tessellator rounds rectangles to whole pixels, as [`Self::squares`] assumes. + rounds_rects: bool, + look: HandleLook<'a>, + templates: &'a mut Vec