diff --git a/crates/engine/src/tests/prefs.rs b/crates/engine/src/tests/prefs.rs index feaba57a1..5727e9db1 100644 --- a/crates/engine/src/tests/prefs.rs +++ b/crates/engine/src/tests/prefs.rs @@ -1703,6 +1703,39 @@ fn deferred_preview_overlays_are_cached_across_redraws() { assert_eq!(s.doc().unwrap().revision, rev, "the preview command is not run again"); } +/// With Real-time Drawing off, a moved selection whose outlines have more points than the canvas +/// outlines (100,000) shows its moved bounds instead (#525, #2460). +#[test] +fn real_time_drawing_off_draws_the_bounds_of_a_selection_too_big_to_outline() { + use vectorcraft_tools::Overlay; + use vectorcraft_tools::draw2::FEEDBACK; + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 600, "height": 600})).unwrap(); + let anchors: Vec = (0..120_000).map(|i| json!({"x": 100.0 + f64::from(i) * 0.003, "y": if i % 2 == 0 { 100 } else { 150 }})).collect(); + let id = s.execute("path.create", &json!({"anchors": anchors, "closed": false})).unwrap()["id"].clone(); + s.execute("select.set", &json!({"ids": [id]})).unwrap(); + let at_rest = s.doc().unwrap().doc.bounds_of(&s.doc().unwrap().selection.objects, true).unwrap(); + let v = ViewInfo::default(); + s.select_tool("selection", v).unwrap(); + set_pref(&mut s, "realTimeDrawing", json!(false)); + s.pointer(&PointerEvent::new(PointerKind::Down, 200.0, 100.0), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, 260.0, 160.0), v).unwrap(); + let feedback: Vec<_> = s + .overlays(v) + .into_iter() + .filter_map(|o| match o { + Overlay::Path { path, color, .. } if color == FEEDBACK => Some(path), + _ => None, + }) + .collect(); + let points: usize = feedback.iter().map(|p| p.elements().len()).sum(); + assert!(points <= 100_000, "the drag feedback has {points} points"); + let [bounds] = feedback.as_slice() else { panic!("one bounds rectangle, got {} paths", feedback.len()) }; + let b = kurbo::Shape::bounding_box(bounds); + let moved = (b.x0 - at_rest.x0, b.y0 - at_rest.y0, b.width() - at_rest.width(), b.height() - at_rest.height()); + assert!(moved.0 > 1.0 && moved.1 > 1.0 && moved.2.abs() < 1e-6 && moved.3.abs() < 1e-6, "the bounds moved with the drag: {moved:?}"); +} + /// A deferred commit that fails its preview cancels the interaction instead of leaving it open. #[test] fn deferred_commit_preview_failure_cancels_interaction() { diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index 29329bf0f..efaef7d64 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -887,13 +887,28 @@ impl Session { } } +/// The most points the outlines of [`outlines_for_selection`] are drawn for, the same number as the +/// canvas's limit on the anchor points it outlines (`OVERLAY_MAX_ANCHORS` in +/// `crates/ui-egui/src/canvas/outline.rs`). Past it the selection's bounds are drawn instead. +const FEEDBACK_MAX_POINTS: usize = 100_000; + +/// The outline of every selected object, or the selection's bounds when the outlines have more than +/// [`FEEDBACK_MAX_POINTS`] points. fn outlines_for_selection(doc: &vectorcraft_doc::Document, selection: &Selection) -> Vec { - selection - .objects - .iter() - .filter_map(|&id| { - let n = doc.node(id)?; - vectorcraft_render::drawn_outline(doc, n).map(|(path, _)| Overlay::Path { path, color: FEEDBACK, width: 1.0, dashed: false }) - }) - .collect() + let mut left = FEEDBACK_MAX_POINTS; + let mut outlines = vec![]; + for &id in &selection.objects { + let Some((path, _)) = doc.node(id).and_then(|n| vectorcraft_render::drawn_outline(doc, n)) else { continue }; + let points = path.elements().iter().filter(|el| !matches!(el, kurbo::PathEl::ClosePath)).count(); + let Some(l) = left.checked_sub(points) else { + return doc + .bounds_of(&selection.objects, true) + .map(|b| Overlay::Path { path: vectorcraft_geom::shapes::rectangle(b).to_bezpath(), color: FEEDBACK, width: 1.0, dashed: false }) + .into_iter() + .collect(); + }; + left = l; + outlines.push(Overlay::Path { path, color: FEEDBACK, width: 1.0, dashed: false }); + } + outlines } diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 5e604c822..c6e2172dd 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -633,6 +633,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Preferences audit (2026-10-30, #394): every Preferences dialog option is read by shipped code, by its field or its key, except User Interface › Open Documents As Tabs, Units › East Asian Type, Hyphenation › Default Language, Plug-ins & Scratch Disks › Primary and Secondary, and Clipboard Handling › Legacy vector clipboard and its mode, which are stored only; [gaps.md G12](gaps.md#g12-preferences-and-panels-that-dont-act-yet) says what each waits on. - With Performance › Real-time Drawing and Editing off, the bounding box moves with a Selection-tool move drag and the object moves when the button is released ([help](https://web.archive.org/web/20220324053041id_/https://helpx.adobe.com/illustrator/using/real-time-editing.html); `real_time_drawing_off_moves_the_bounding_box_with_a_move_drag`). - Performance › Real-time Drawing and Editing off defers a drag's edits to its release, but a ruler guide dragged out of a ruler or moved still shows as it is dragged, where only its position label showed (#4646; `a_guide_dragged_out_of_a_ruler_shows_while_dragged`, `real_time_drawing_off_still_shows_a_dragged_guide`). + - With Real-time Drawing and Editing off, a dragged selection whose outlines have more than 100,000 points shows its moved bounds instead, as the selection at rest does (#525, #2460; `real_time_drawing_off_draws_the_bounds_of_a_selection_too_big_to_outline`). - Dialogs move by dragging their heading (the band down from the top edge, as a title bar), stay on screen and keep where they were moved for the session; the form dialogs' choices are dropdowns (Offset Path's Joins, Average's and Shear's and Reflect's axis), and Offset Path lists Offset, Joins, Miter Limit and previews (#423). - Dialogs open with their first field focused and its value selected: type a value and press Enter (Move, Rotate, Scale…). A dialog opened from a menu takes the keyboard focus every time it opens, not only the first (#1367), and one whose first field starts greyed out (Blend Options on Smooth Color, Layers Panel Options) gives the focus to that field once it is enabled (#1395). Number fields in panels, the Control bar and dialogs (preferences too) step with Up/Down, or with the mouse wheel over the focused field (#485: a notch a step; the panel under it then stays put, and an unfocused field leaves the wheel to scrolling) (Shift: ten, Ctrl/Cmd: a tenth of their unit), applied at once. - While Alt (Option on macOS) is held, the Flare Tool Options' Cancel button reads Reset, which puts every option back to the Flare tool's default and leaves the dialog open (`ui.dialogReset`).