From 7e22d3816a8ae15b153db9192f1ba7fba593fcfc Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 6 Nov 2026 11:23:54 -0800 Subject: [PATCH] Tests: the pan test also pans with the Selection tool a_pan_moves_the_marks_without_building_them covered Direct Selection only. It now runs with both tools. The module comment and MAX_PAN's comment now say what is kept as triangles and as document points, and when a pan builds the marks again. A test label now says points rather than px. The timings in docs/target-app-parity.md are measured again with 10 fill colors on the 2,000 circles. The pixel sentence there also names the rounding errors of kept vertices. --- crates/ui-egui/src/canvas/selection_marks.rs | 29 ++++++++++-------- .../src/canvas/selection_marks/tests.rs | 30 ++++++++++--------- docs/target-app-parity.md | 2 +- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/crates/ui-egui/src/canvas/selection_marks.rs b/crates/ui-egui/src/canvas/selection_marks.rs index 2a09080fe..f99899863 100644 --- a/crates/ui-egui/src/canvas/selection_marks.rs +++ b/crates/ui-egui/src/canvas/selection_marks.rs @@ -1,13 +1,16 @@ -//! The selected objects' outlines, anchors and handles, kept between frames as triangles. +//! The selected objects' outlines, anchors and handles, kept between frames. //! -//! 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. 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. +//! 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; @@ -22,9 +25,9 @@ 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. +/// 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 @@ -616,7 +619,9 @@ impl SelectionMarks { } } } + let kept = self.objects.len(); self.objects.retain(|_, entry| entry.seen == pass); + changed |= self.objects.len() != kept; let shown = Shown { at: map.at, clip: p.clip_rect(), all_handles: options.all_handles }; if changed || self.shown.as_ref() != Some(&shown) || self.order != objects { self.order.clear(); diff --git a/crates/ui-egui/src/canvas/selection_marks/tests.rs b/crates/ui-egui/src/canvas/selection_marks/tests.rs index 760414ca6..98ef0fdfb 100644 --- a/crates/ui-egui/src/canvas/selection_marks/tests.rs +++ b/crates/ui-egui/src/canvas/selection_marks/tests.rs @@ -268,10 +268,10 @@ fn paint(marks: &mut SelectionMarks, app: &VectorcraftApp, p: &egui::Painter, xf } /// Kept marks, marks built from nothing and plain shapes all draw the same pixels. Kept marks may -/// differ at up to 1 pixel in 1,000: at a corner of exactly 90 degrees on a rotated view, the -/// tessellator picks a miter or a cut corner by a rounding error of the corner's position, and -/// moved triangles keep the corner they were built with. An anchor square left stale covers more -/// than this limit. +/// differ at up to 1 pixel in 1,000: at a corner of exactly 90 degrees on a rotated view, plain +/// shapes take a miter or a cut corner by a rounding error of the corner's position, which a pan +/// changes, and moved triangles keep the corner they were built with. An anchor square left stale +/// covers more than this limit. #[track_caller] fn assert_matches_plain(ctx: &egui::Context, marks: &mut SelectionMarks, app: &VectorcraftApp, xf: &Xf, pixels_per_point: f32, what: &str) { let want = picture(ctx, pixels_per_point, |p| plain(app, p, xf, false)); @@ -309,16 +309,18 @@ fn marks_draw_the_same_pixels_as_plain_shapes_at_each_view() { fn a_pan_moves_the_marks_without_building_them() { let (mut app, _) = scene(); let ctx = egui::Context::default(); - app.select_tool("directSelection"); - for (pixels_per_point, rot) in [(1.0, 0.0), (1.25, 0.0), (2.0, 30.0)] { - let mut marks = SelectionMarks::default(); - assert_matches_plain(&ctx, &mut marks, &app, &xf(1.5, VIEW, rot), pixels_per_point, "before the pan"); - let builds = marks.builds; - for (dx, dy) in [(10.0, 0.0), (3.0, 7.0), (0.37, 0.61), (-31.3, 12.9), (160.0, 90.0)] { - let view = xf(1.5, (VIEW.0 + dx, VIEW.1 + dy), rot); - assert_matches_plain(&ctx, &mut marks, &app, &view, pixels_per_point, &format!("{pixels_per_point}x pan {dx} {dy}")); + for tool in ["selection", "directSelection"] { + app.select_tool(tool); + for (pixels_per_point, rot) in [(1.0, 0.0), (1.25, 0.0), (2.0, 30.0)] { + let mut marks = SelectionMarks::default(); + assert_matches_plain(&ctx, &mut marks, &app, &xf(1.5, VIEW, rot), pixels_per_point, &format!("{tool}, before the pan")); + let builds = marks.builds; + for (dx, dy) in [(10.0, 0.0), (3.0, 7.0), (0.37, 0.61), (-31.3, 12.9), (160.0, 90.0)] { + let view = xf(1.5, (VIEW.0 + dx, VIEW.1 + dy), rot); + assert_matches_plain(&ctx, &mut marks, &app, &view, pixels_per_point, &format!("{tool}, {pixels_per_point}x pan {dx} {dy}")); + } + assert_eq!(marks.builds, builds, "{tool}: a pan builds nothing"); } - assert_eq!(marks.builds, builds, "a pan builds nothing"); } } @@ -331,7 +333,7 @@ fn a_long_pan_at_high_zoom_builds_the_marks_again_and_stays_exact() { let zoom = 2_000.0; assert_matches_plain(&ctx, &mut marks, &app, &xf(zoom, (180.0, 41.0), 0.0), 1.0, "the top of the curve's straight end"); let builds = marks.builds; - assert_matches_plain(&ctx, &mut marks, &app, &xf(zoom, (180.0, 89.0), 0.0), 1.0, "96,000 px along the straight end"); + assert_matches_plain(&ctx, &mut marks, &app, &xf(zoom, (180.0, 89.0), 0.0), 1.0, "96,000 points along the straight end"); assert!(marks.builds > builds, "a pan past MAX_PAN builds the marks again"); } diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 6ff89b992..b80d40b44 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1332,7 +1332,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The renderer keeps the path of a path or compound path of 1,024 anchors or more by object, with the bounds of its segments and of its miter spikes in runs of 128 segments. When anchors move and the subpaths and anchor counts stay the same, the next frame changes only the segments that start or end at a moved anchor and measures again only the runs that hold them or the segments with length before them, instead of making the whole path again and measuring it whole for culling. The pixels and the cull bounds are the same. On Windows at 1600 × 1200, the frame after one anchor moves takes 8.2 ms instead of 30.1 ms with an open path of 200,000 anchors, 4.3 ms instead of 10.4 ms with a closed path of 50,000 anchors, and 6.6 ms instead of 11.9 ms with the filled closed path of 50,000 anchors and a dashed stroke (medians of five runs of `anchor_bench`). Type › Show Hidden Characters keeps the marks it places on each text object from one frame to the next. The canvas lays a text object out again when an edit changes it or its threaded story, or when the last frame that showed the marks did not draw it, and it lays out all type in view again when the fonts or the hyphenation exceptions change. With 1,000 two-line labels in view, drawing the marks takes 0.8 ms per frame in a release build instead of 19.7 ms. A selection with more than 100,000 points to outline shows its bounds instead of each object's outline and anchors. Paths count their anchors. Area type and type on a path count the anchors of their frame or path, and point type counts six: its box's corners and the ends of its baseline marker. Images, symbol instances, placed documents, and area type or type on a path in perspective count the four corners of their box, and gradient meshes and envelopes count their points and the ends of their mesh lines. The hover highlight leaves out an object past that count, and such an object shows its bounds as the key object. With 32,768 type objects, images, symbol instances, gradient meshes or envelopes selected, the selection's outlines and anchors add fewer than 100 vertices to the canvas's mesh (#2460). - The canvas keeps the selected objects' outlines, anchors and handles from one frame to the next as one mesh. It builds an object's marks again when the object, its layer color or its selected anchors change. A pan moves the kept outlines and handles and places the anchor squares again. Each subpath and handle gets its triangles the first time it is in view. A frame with a new zoom or rotation draws the marks one shape at a time and keeps nothing, and the next frame at that view builds them. The pixels are the same, except that a kept outline's corner of exactly 90 degrees in a rotated view can keep its miter or cut corner after a pan. On Windows at 1440 × 900, in a release build, with 2,000 selected circles of 50 anchors each, an idle frame takes 12.6 ms instead of 64.6 ms with the Selection tool and 22.0 ms instead of 135.9 ms with Direct Selection (medians of five runs). A frame during a zoom takes about what it did: 64.7 ms instead of 62.6 ms with the Selection tool. + The canvas keeps the selected objects' outlines, anchors and handles from one frame to the next as one mesh. It builds an object's marks again when the object, its layer color or its selected anchors change. A pan moves the kept outlines and handles and places the anchor squares again. Each subpath and handle gets its triangles the first time it is in view. A frame with a new zoom or rotation draws the marks one shape at a time and keeps nothing, and the next frame at that view builds them. The pixels are the same, apart from two small differences: a kept outline's corner of exactly 90 degrees in a rotated view can keep its miter or cut corner after a pan, and kept vertices can move by rounding errors. On Windows at 1440 × 900, in a release build, with 2,000 selected circles of 50 anchors each in 10 fill colors, an idle frame takes 12.8 ms instead of 59.9 ms with the Selection tool and 29.8 ms instead of 192.1 ms with Direct Selection (medians of five runs). A frame during a zoom takes about what it did: 86.7 ms instead of 80.0 ms with the Selection tool. Delete, Cut, Group, Ungroup, Make and Release Compound Path, Make Clipping Mask, Send to Current Layer, Duplicate, Copy in the Move, Rotate, Scale, Reflect and Shear dialogs, Transform Each with Copy and each move of an Alt-drag take time in proportion to the size of the document (#1429). In the web build the status bar's render and UI times are measured with the browser's `performance.now()`, and the Layers panel stops rendering row thumbnails in a frame once they have taken 8 ms. On the desktop, the Layers panel renders a row's thumbnail on its thumbnail thread when the row's paths, and the paths of its opacity masks that are not disabled, hold more than 2,000 anchors in all. It already did this for the first thumbnail of a row of more than 200 objects. Until its first thumbnail is ready, the row shows boxes at the bounds of up to 41 of its paths, each in its path's solid fill or stroke color or in gray, when its own paths, without its opacity masks, hold at most 2,000 anchors in all, and an empty white box otherwise. With one path of 200,000 anchors, the first frame after the panel opens takes 2.4 ms instead of 393 ms (release build, median of three runs).