mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Merge pull request #2635 from dotCooCoo/fix/issue-2460
Part of #2460: a huge selection of type, images, symbols or meshes shows its bounds
This commit is contained in:
+137
-10
@@ -1770,20 +1770,20 @@ fn node_outline(n: &Node, symbols: &SymbolBoxes) -> BezPath {
|
||||
/// The width of the key object's outline (Align to Key Object), thicker than the selection's.
|
||||
const KEY_OUTLINE: f32 = 2.5;
|
||||
|
||||
/// The most anchor points the selection's outlines and anchors, and the hover highlight, are drawn
|
||||
/// for: an Image Trace of a photo selects hundreds of thousands of paths at once, whose outlines
|
||||
/// would make a mesh larger than the GPU takes in one buffer (#525).
|
||||
/// The most points ([`overlay_points`]) the selection's outlines and anchors, and the hover
|
||||
/// highlight, are drawn for: an Image Trace of a photo selects hundreds of thousands of paths at
|
||||
/// once, and selecting all of an opened PDF can select as many type objects, images and gradient
|
||||
/// mesh pieces. Their outlines would make a mesh larger than the GPU takes in one buffer (#525,
|
||||
/// #2460).
|
||||
const OVERLAY_MAX_ANCHORS: usize = 100_000;
|
||||
|
||||
/// Whether `nodes` have more than [`OVERLAY_MAX_ANCHORS`] anchor points to outline (counting
|
||||
/// stops there).
|
||||
/// Whether `nodes` have more than [`OVERLAY_MAX_ANCHORS`] points to outline ([`overlay_points`];
|
||||
/// counting stops there).
|
||||
fn too_many_anchors<'a>(nodes: impl IntoIterator<Item = &'a Node>) -> bool {
|
||||
fn over(n: &Node, left: &mut usize) -> bool {
|
||||
if let NodeKind::Path { path, .. } = &n.kind {
|
||||
match left.checked_sub(path.anchor_count()) {
|
||||
Some(l) => *left = l,
|
||||
None => return true,
|
||||
}
|
||||
match left.checked_sub(overlay_points(n)) {
|
||||
Some(l) => *left = l,
|
||||
None => return true,
|
||||
}
|
||||
// A clip group counts only its clipping path, the one child `walk_drawn` visits.
|
||||
if n.clips() {
|
||||
@@ -1796,6 +1796,30 @@ fn too_many_anchors<'a>(nodes: impl IntoIterator<Item = &'a Node>) -> bool {
|
||||
nodes.into_iter().any(|n| over(n, &mut left))
|
||||
}
|
||||
|
||||
/// The points counted for `n` itself, without its children, toward [`OVERLAY_MAX_ANCHORS`]: a
|
||||
/// path's anchors; the anchors of area type's frame or of type on a path's path; the four corners
|
||||
/// of the box that area type or type on a path in perspective, images, symbol instances and
|
||||
/// placed documents show; six for point type, its box's corners and the ends of the baseline
|
||||
/// marker a selected one shows; and the points and the ends of the mesh lines of a gradient mesh
|
||||
/// or an envelope.
|
||||
fn overlay_points(n: &Node) -> usize {
|
||||
match &n.kind {
|
||||
NodeKind::Path { path, .. } => path.anchor_count(),
|
||||
NodeKind::Text(t) => match &t.kind {
|
||||
vectorcraft_doc::TextKind::Point => 6,
|
||||
_ if n.extras().perspective.is_some() => 4,
|
||||
vectorcraft_doc::TextKind::Area { frame } => frame.anchor_count(),
|
||||
vectorcraft_doc::TextKind::OnPath { path, .. } => path.anchor_count(),
|
||||
},
|
||||
NodeKind::Image(_) | NodeKind::SymbolInstance { .. } | NodeKind::PlacedDocument(_) => 4,
|
||||
NodeKind::Mesh(m) => m.lines().anchor_count() + m.points.len(),
|
||||
NodeKind::Envelope { .. } => {
|
||||
vectorcraft_doc::live::envelope_overlay(n).map_or(0, |(lines, grid)| lines.anchor_count() + grid.map_or(0, |g| g.points.len()))
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Node::walk`] over what a selection highlight shows: an envelope's content is left out (the
|
||||
/// envelope shows its mesh instead), and a clip group or clipping layer shows only its clipping
|
||||
/// path, its first child, not the art the path clips (#2422).
|
||||
@@ -3947,6 +3971,98 @@ mod tests {
|
||||
assert!(points > 200 && squares > 200, "{points} outline points, {squares} squares");
|
||||
}
|
||||
|
||||
/// #2460: type, images, symbol instances, gradient meshes and envelopes count toward the
|
||||
/// points the overlay outlines, as paths' anchors do. 32,768 of one kind (an opened PDF can
|
||||
/// hold as many type lines, images or mesh pieces) add only the selection's bounds to the
|
||||
/// frame's mesh, under 100 vertices; one alone adds its outline.
|
||||
#[test]
|
||||
fn a_huge_selection_of_type_images_symbols_or_meshes_shows_its_bounds() {
|
||||
for kind in ["point type", "area type", "type on a path", "image", "symbol", "mesh", "envelope"] {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.ui.task_bar = false;
|
||||
// The Rectangle tool draws nothing of its own (no bounding box, no type on a path
|
||||
// brackets): only the selection's overlay changes the frame.
|
||||
app.select_tool("rectangle");
|
||||
match kind {
|
||||
"point type" => {
|
||||
app.session.execute("text.create", &json!({"x": 20, "y": 40, "text": "Type"})).unwrap();
|
||||
}
|
||||
"area type" => {
|
||||
let area = json!({"width": 150, "height": 20});
|
||||
app.session.execute("text.create", &json!({"x": 20, "y": 40, "text": "Type", "area": area})).unwrap();
|
||||
}
|
||||
"type on a path" => {
|
||||
let path = app.session.execute("path.create", &json!({"d": "M20 100 L140 100 L260 100 L380 100"})).unwrap()["id"].clone();
|
||||
app.session.execute("text.createInPath", &json!({"path": path, "mode": "onPath", "text": "Type"})).unwrap();
|
||||
}
|
||||
"image" => {
|
||||
let data = vectorcraft_format::base64_encode(&crate::tests_sysclip::blue_png());
|
||||
app.run("file.place", json!({"name": "blue.png", "dataBase64": data, "link": false, "at": [200, 150]})).unwrap();
|
||||
}
|
||||
"symbol" => {
|
||||
app.session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 40, "height": 20})).unwrap();
|
||||
app.session.execute("symbol.new", &json!({})).unwrap();
|
||||
}
|
||||
"mesh" => {
|
||||
app.session.execute("shape.rectangle", &json!({"x": 150, "y": 50, "width": 30, "height": 20})).unwrap();
|
||||
app.session.execute("object.mesh.create", &json!({"rows": 1, "cols": 1})).unwrap();
|
||||
}
|
||||
_ => {
|
||||
app.session.execute("shape.rectangle", &json!({"x": 150, "y": 50, "width": 30, "height": 20})).unwrap();
|
||||
app.session.execute("object.envelope.makeWithMesh", &json!({"rows": 1, "cols": 1})).unwrap();
|
||||
}
|
||||
}
|
||||
let ctx = egui::Context::default();
|
||||
// The vertices that selecting everything adds to the frame's mesh.
|
||||
let overlay = |app: &mut VectorcraftApp| {
|
||||
app.session.execute("select.none", &json!({})).unwrap();
|
||||
let none = frame_vertices(app, &ctx);
|
||||
app.session.execute("select.all", &json!({})).unwrap();
|
||||
frame_vertices(app, &ctx).saturating_sub(none)
|
||||
};
|
||||
let one = overlay(&mut app);
|
||||
assert!(one > 0, "{kind}: one shows its outline");
|
||||
for _ in 0..15 {
|
||||
app.session.execute("select.all", &json!({})).unwrap();
|
||||
app.run("edit.duplicate", json!({})).unwrap();
|
||||
}
|
||||
let many = overlay(&mut app);
|
||||
assert_eq!(app.session.active().unwrap().selection.objects.len(), 32_768, "{kind}");
|
||||
assert!(many < 100, "{kind}: 32,768 show the selection's bounds, not {many} vertices");
|
||||
}
|
||||
}
|
||||
|
||||
/// #2460: a group of 32,768 images is too much to outline: under the pointer it is not
|
||||
/// highlighted, and as the key object its outline is its bounds.
|
||||
#[test]
|
||||
fn a_group_of_too_many_images_has_no_hover_outline_and_a_key_outline_of_its_bounds() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
let data = vectorcraft_format::base64_encode(&crate::tests_sysclip::blue_png());
|
||||
app.run("file.place", json!({"name": "blue.png", "dataBase64": data, "link": false, "at": [200, 150]})).unwrap();
|
||||
for _ in 0..15 {
|
||||
app.session.execute("select.all", &json!({})).unwrap();
|
||||
app.run("edit.duplicate", json!({})).unwrap();
|
||||
}
|
||||
app.session.execute("select.all", &json!({})).unwrap();
|
||||
let group = app.session.execute("object.group", &json!({})).unwrap()["id"].clone();
|
||||
app.session.execute("shape.rectangle", &json!({"x": 20, "y": 20, "width": 30, "height": 30})).unwrap();
|
||||
app.select_tool("selection");
|
||||
app.session.execute("select.none", &json!({})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let over = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()).to_screen(Point::new(200.0, 150.0));
|
||||
let lines = |app: &mut VectorcraftApp, width: f32| {
|
||||
frame(app, &ctx, vec![egui::Event::PointerMoved(over)]);
|
||||
shapes(app, &ctx).iter().filter(|s| matches!(s, Shape::Path(ps) if ps.stroke.width == width)).count()
|
||||
};
|
||||
assert_eq!(lines(&mut app, 1.5), 0, "no hover outline");
|
||||
app.session.execute("select.all", &json!({})).unwrap();
|
||||
app.session.execute("select.key", &json!({"id": group})).unwrap();
|
||||
assert_eq!(lines(&mut app, KEY_OUTLINE), 1, "the key object's bounds");
|
||||
}
|
||||
|
||||
/// #1111: a selected gradient mesh shows its mesh lines and points to the selection tools, not
|
||||
/// only to the Mesh tool.
|
||||
#[test]
|
||||
@@ -3999,6 +4115,17 @@ mod tests {
|
||||
shapes
|
||||
}
|
||||
|
||||
/// Two headless canvas frames → the vertices of the second one's tessellated mesh, which the
|
||||
/// GPU takes in one buffer.
|
||||
fn frame_vertices(app: &mut VectorcraftApp, ctx: &egui::Context) -> usize {
|
||||
frame(app, ctx, vec![]);
|
||||
let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
let mesh = |p: &egui::ClippedPrimitive| if let egui::epaint::Primitive::Mesh(m) = &p.primitive { m.vertices.len() } else { 0 };
|
||||
ctx.tessellate(out.shapes, out.pixels_per_point).iter().map(mesh).sum()
|
||||
}
|
||||
|
||||
/// One headless canvas frame → the shapes drawn (`Shape::Vec`s flattened) and the textures
|
||||
/// uploaded by it (the art's raster among them, when it was re-rendered).
|
||||
fn frame_output(app: &mut VectorcraftApp, ctx: &egui::Context) -> (Vec<Shape>, egui::TexturesDelta) {
|
||||
|
||||
@@ -703,6 +703,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
The Pathfinder panel, Offset Path, Clean Up, Divide Objects Below, Make and Release Blend, Envelope Distort's Make and Release, Repeat (Radial, Grid, Mirror and Release), Vector Halftone, New and Redefine Symbol, Delete Symbol, erasing with the Symbol Sprayer, Rasterize, Convert to Artboards, Flatten Transparency, Clear Guides, Shape Builder, Live Paint, Release Image Trace, Make and Release Opacity Mask, Delete with direct-selected anchor points, Remove Anchor Points, Cut Path, and the Shaper, Knife, Eraser, Path Eraser, Line Cut, Rectangle Cut, Mirror & Cut and Blob Brush tools add and remove objects in one pass over each layer or group they change, and Outline Stroke and Expand write each new object in the place of the object it replaces (#1429).
|
||||
With more than 10,000 type on a path objects selected, the Selection and Direct Selection tools neither draw nor hit-test the objects' brackets.
|
||||
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).
|
||||
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.
|
||||
An Alt-drag with the Selection, Direct Selection, Group Selection, Rotate, Scale, Reflect or Shear tool copies the selection on its first move, and the moves after it transform those copies; the previews of the Move, Rotate, Scale, Reflect and Shear dialogs with Copy and Preview on copy the selection once in the same way (#1429).
|
||||
|
||||
Reference in New Issue
Block a user