From 4a5ff880ff844e5142874a96c5dee3f6e0061b71 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 6 Nov 2026 07:42:09 -0800 Subject: [PATCH 1/7] Canvas: selection outlines and anchors are reused between frames The canvas flattened every selected object's outline and tessellated its anchors and handles again in each frame. It now keeps them per object as one mesh, valid for the object's node, layer color and selected anchors. An idle frame draws the mesh again. A pan moves kept outlines and handles and places the anchor squares again. A frame with a new zoom, rotation or display scale draws plain shapes and keeps nothing, and the next frame at that view builds the marks. Entries of objects that leave the selection or the document are dropped. --- crates/ui-egui/src/app.rs | 1 + crates/ui-egui/src/caches.rs | 3 + crates/ui-egui/src/canvas.rs | 11 +- crates/ui-egui/src/canvas/overlays.rs | 166 +--- crates/ui-egui/src/canvas/selection_marks.rs | 864 ++++++++++++++++++ .../src/canvas/selection_marks/tests.rs | 525 +++++++++++ docs/target-app-parity.md | 1 + 7 files changed, 1443 insertions(+), 128 deletions(-) create mode 100644 crates/ui-egui/src/canvas/selection_marks.rs create mode 100644 crates/ui-egui/src/canvas/selection_marks/tests.rs 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..d0d01b312 --- /dev/null +++ b/crates/ui-egui/src/canvas/selection_marks.rs @@ -0,0 +1,864 @@ +//! The selected objects' outlines, anchors and handles, kept between frames as triangles. +//! +//! Each selected object has an [`ObjectMarks`], valid for one node (by [`Arc`] identity), layer +//! 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. + +use std::collections::{BTreeMap, BTreeSet, HashMap}; +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 kept triangles before their 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, +} + +/// [`Xf`] with its rotation worked out, split into a linear part and an offset. +struct ViewMap { + sin: f64, + cos: f64, + zoom: f64, + origin: Pos2, + center: [f64; 2], + offset: [f64; 2], +} + +impl ViewMap { + fn new(xf: &Xf) -> Self { + let (sin, cos) = xf.rot.sin_cos(); + let origin = xf.rect.center(); + let mut map = Self { sin, cos, zoom: xf.zoom, origin, center: [0.0; 2], offset: [0.0; 2] }; + map.center = map.view(xf.center); + map.offset = [f64::from(origin.x) - map.center[0], f64::from(origin.y) - map.center[1]]; + map + } + + /// `p` scaled and rotated by the view, without its position. + fn view(&self, p: Point) -> [f64; 2] { + let (x, y) = (p.x * self.zoom, p.y * self.zoom); + [x * self.cos - y * self.sin, x * self.sin + y * self.cos] + } + + fn screen(&self, v: [f64; 2]) -> Pos2 { + pos2(self.origin.x + (v[0] - self.center[0]) as f32, self.origin.y + (v[1] - self.center[1]) as f32) + } + + fn to_screen(&self, p: Point) -> Pos2 { + self.screen(self.view(p)) + } +} + +/// 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]) }, + /// 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. + Segment { a: [f64; 2], b: [f64; 2], 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 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); + *points += count; + *discs |= disc; + shapes.push(shape); + } else { + self.marks.push(Mark::Rigid { shapes: vec![shape], mesh: Mesh::default(), bounds: rect, points: count, discs: disc }); + } + } + + /// 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() + && *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]); + } else { + self.marks.push(Mark::Handles { + color, + lines: vec![[a, b]], + rounded: is_rounded, + mesh: Mesh::default(), + at: [0.0; 2], + bounds: (least, most), + }); + } + } + + 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) { + let v = self.map.view(p); + if let Some(Mark::Squares { color: c, filled: f, size: s, at }) = self.marks.last_mut() + && (*c, *f, *s) == (color, filled, size) + { + at.push(v); + } else { + self.marks.push(Mark::Squares { color, filled, size, at: vec![v] }); + } + } + + fn line(&mut self, from: Point, to: Point, stroke: Stroke) { + let (a, b) = (self.map.view(from), self.map.view(to)); + let (sa, sb) = (self.map.screen(a), self.map.screen(b)); + if is_rounded_line(sa, sb) { + self.marks.push(Mark::Segment { a, b, 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)>, + /// [`ViewMap::offset`] when the marks were built. + base: [f64; 2], + 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 { + 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) + && (map.offset[0] - self.base[0]).abs() <= MAX_PAN + && (map.offset[1] - self.base[1]).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, +} + +/// 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. + 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