mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
Zoom tool: Space held while dragging moves the zoom marquee
Squashed from #4372 onto main.
This commit is contained in:
@@ -370,9 +370,12 @@ fn canvas_cursor(app: &mut VectorcraftApp, ui: &Ui, view_info: vectorcraft_engin
|
||||
let space = ui.input(|i| i.key_down(egui::Key::Space));
|
||||
let drag = ui.data(|d| d.get_temp::<Drag>(drag_id()));
|
||||
let panning = matches!(drag, Some(Drag::Pan { .. }));
|
||||
let zooming = if space { m.command } else { app.session.tool_id() == "zoom" };
|
||||
// A zoom drag keeps its cursor when Space is pressed (with or without Shift) or Cmd is let go.
|
||||
let zoom_drag = matches!(drag, Some(Drag::ZoomBox { .. } | Drag::ZoomScrub { .. }));
|
||||
let zooming = zoom_drag || if space { m.command } else { app.session.tool_id() == "zoom" };
|
||||
// A rotating drag keeps its cursor when Shift is let go.
|
||||
let rotating = matches!(drag, Some(Drag::RotateView { .. })) || if space { m.shift && !m.command } else { app.session.tool_id() == "rotateView" };
|
||||
let rotating = matches!(drag, Some(Drag::RotateView { .. }))
|
||||
|| (!zoom_drag && if space { m.shift && !m.command } else { app.session.tool_id() == "rotateView" });
|
||||
// The hand and the magnifier are drawn like the tool cursors below: Windows has no
|
||||
// system cursor for them (winit shows the four-headed move cursor for Grab).
|
||||
let view_cursor = if panning {
|
||||
|
||||
@@ -262,12 +262,19 @@ fn begin_drag(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, p: Pos2) {
|
||||
// A new press has not dragged yet, whatever an interrupted one left behind.
|
||||
dd.remove::<Point>(tool_drag_doc_id());
|
||||
dd.insert_temp(tool_press_doc_id(), xf.to_doc(p));
|
||||
// Space held at the press (Cmd+Space lends the Zoom tool) sizes the marquee until
|
||||
// it is let go ([`zoom_marquee_start`]).
|
||||
if matches!(d, Drag::ZoomBox { .. }) {
|
||||
dd.insert_temp(zoom_space_free_id(), !space);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A gesture held at `p`: the view follows, or the tool gets drag events.
|
||||
fn drag_held(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, d: Drag, p: Pos2) {
|
||||
let Pointing { resp, rect, pointer, m, space, xf, view, .. } = *f;
|
||||
// Where the pointer was in the frame before: a zoom marquee moved with Space follows it.
|
||||
let before: Option<Pos2> = ui.data(|dd| dd.get_temp(drag_pos_id()));
|
||||
ui.data_mut(|dd| dd.insert_temp(drag_pos_id(), p));
|
||||
match d {
|
||||
Drag::Pan { start, center, .. } => {
|
||||
@@ -288,7 +295,11 @@ fn drag_held(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, d: Drag, p: Pos2)
|
||||
vm.rotation = vectorcraft_geom::normalize_deg(deg);
|
||||
}
|
||||
}
|
||||
Drag::ZoomBox { start } => marquee(ui.painter(), egui::Rect::from_two_pos(start, p)),
|
||||
Drag::ZoomBox { start } => {
|
||||
let start = zoom_marquee_start(ui, start, before, p, space);
|
||||
ui.data_mut(|dd| dd.insert_temp(drag_id(), Drag::ZoomBox { start }));
|
||||
marquee(ui.painter(), egui::Rect::from_two_pos(start, p));
|
||||
}
|
||||
Drag::ZoomScrub { start, held, moved } => {
|
||||
let dt = ui.input(|i| i.unstable_dt).min(0.25);
|
||||
let (held, moved) = (held + dt, moved || (p - start).length() > SCRUB_SLOP);
|
||||
@@ -355,6 +366,8 @@ fn drag_released(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, d: Drag, p: Po
|
||||
let Pointing { resp, rect, m, space, xf, view, .. } = *f;
|
||||
// The last position the tool's drag got: none if the press never dragged.
|
||||
let last_drag: Option<Point> = ui.data(|dd| dd.get_temp(tool_drag_doc_id()));
|
||||
// Where the pointer was in the frame before: a zoom marquee moved with Space follows it.
|
||||
let before: Option<Pos2> = ui.data(|dd| dd.get_temp(drag_pos_id()));
|
||||
ui.data_mut(|dd| {
|
||||
dd.remove::<Drag>(drag_id());
|
||||
dd.remove::<Pos2>(drag_pos_id());
|
||||
@@ -363,6 +376,8 @@ fn drag_released(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, d: Drag, p: Po
|
||||
});
|
||||
match d {
|
||||
Drag::ZoomBox { start } => {
|
||||
let start = zoom_marquee_start(ui, start, before, p, space);
|
||||
ui.data_mut(|dd| dd.remove::<bool>(zoom_space_free_id()));
|
||||
let r = egui::Rect::from_two_pos(start, p);
|
||||
if let Some(vm) = app.view_mut() {
|
||||
if r.width() > 8.0 && r.height() > 8.0 {
|
||||
@@ -442,6 +457,60 @@ fn double_click(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, p: Pos2) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether Space moves the Zoom tool's marquee in the drag under way ([`zoom_marquee_start`]).
|
||||
fn zoom_space_free_id() -> egui::Id {
|
||||
egui::Id::unique("canvas-zoom-space-free")
|
||||
}
|
||||
|
||||
/// The start corner of the Zoom tool's marquee in this frame. With Space held, the marquee moves
|
||||
/// as far as the pointer did since `before` and keeps its size; with Space let go, the drag sizes
|
||||
/// it again from where it moved to. Space held since the press (Cmd+Space lends the Zoom tool)
|
||||
/// sizes the marquee until it is let go. A marquee with no size yet is sized by the movement, so
|
||||
/// Space pressed before the pointer moves does not keep it at the press point. The frame's pointer
|
||||
/// moves and Space presses and releases are taken in the order they came, so each move follows the
|
||||
/// Space state it came with.
|
||||
fn zoom_marquee_start(ui: &Ui, start: Pos2, before: Option<Pos2>, p: Pos2, space: bool) -> Pos2 {
|
||||
enum Step {
|
||||
Space(bool),
|
||||
Move(Pos2),
|
||||
}
|
||||
let id = zoom_space_free_id();
|
||||
let steps: Vec<Step> = ui.input(|i| {
|
||||
i.events
|
||||
.iter()
|
||||
.filter_map(|e| match e {
|
||||
egui::Event::Key { key: egui::Key::Space, pressed, repeat: false, .. } => Some(Step::Space(*pressed)),
|
||||
egui::Event::PointerMoved(q) => Some(Step::Move(*q)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
// Space as it was when the frame began: the opposite of its first press or release, else as it is now.
|
||||
let mut held = steps.iter().find_map(|s| if let Step::Space(down) = s { Some(!down) } else { None }).unwrap_or(space);
|
||||
let mut free = !held || ui.data(|d| d.get_temp::<bool>(id)).unwrap_or(true);
|
||||
let (mut start, mut at) = (start, before);
|
||||
for s in steps.into_iter().chain(std::iter::once(Step::Move(p))) {
|
||||
match s {
|
||||
Step::Space(down) => {
|
||||
held = down;
|
||||
free |= !down;
|
||||
}
|
||||
Step::Move(q) => {
|
||||
if let Some(b) = at
|
||||
&& held
|
||||
&& free
|
||||
&& b != start
|
||||
{
|
||||
start += q - b;
|
||||
}
|
||||
at = Some(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
ui.data_mut(|d| d.insert_temp(id, free));
|
||||
start
|
||||
}
|
||||
|
||||
/// A marquee around screen rectangle `r`: dark dashes over a light line, so it shows on a white
|
||||
/// artboard and on the grey pasteboard alike (#725).
|
||||
fn marquee(p: &egui::Painter, r: egui::Rect) {
|
||||
|
||||
@@ -4313,6 +4313,168 @@ fn the_zoom_tool_zooms_a_rotated_view_to_the_area_dragged_across() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Space held while the Zoom tool drags a marquee moves the marquee and keeps its size. When Space
|
||||
/// is let go, the drag sizes the marquee again from where it moved to, and the release zooms to the
|
||||
/// moved marquee.
|
||||
/// Space held since a Cmd+Space press sizes the marquee.
|
||||
#[test]
|
||||
fn space_moves_the_zoom_marquee_while_dragging() {
|
||||
use egui::{Event, Modifiers};
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.run("prefs.set", json!({"key": "animatedZoom", "value": false})).unwrap();
|
||||
app.select_tool("zoom");
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let rect = app.canvas_rect.unwrap();
|
||||
let at = rect.center() - vec2(100.0, 50.0);
|
||||
let space = |pressed, modifiers| Event::Key { key: egui::Key::Space, physical_key: None, pressed, repeat: false, modifiers };
|
||||
let button = |pos, pressed, modifiers| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers };
|
||||
let drawn = |app: &mut VectorcraftApp| -> Vec<Pos2> {
|
||||
let lines: Vec<Vec<Pos2>> = shapes(app, &ctx)
|
||||
.into_iter()
|
||||
.filter_map(|s| match s {
|
||||
Shape::Path(p) if p.stroke.color == egui::epaint::ColorMode::Solid(Color32::from_white_alpha(230)) => Some(p.points),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let [line] = &lines[..] else { panic!("one marquee: {lines:?}") };
|
||||
line.clone()
|
||||
};
|
||||
let z0 = app.view().unwrap().zoom;
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at)]);
|
||||
frame(&mut app, &ctx, vec![button(at, true, Modifiers::NONE)]);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at + vec2(100.0, 50.0))]);
|
||||
frame(&mut app, &ctx, vec![space(true, Modifiers::NONE), Event::PointerMoved(at + vec2(140.0, 70.0))]);
|
||||
let moved = drawn(&mut app);
|
||||
assert_eq!(moved[0], at + vec2(40.0, 20.0), "Space moves the marquee: {moved:?}");
|
||||
assert_eq!(moved[2], at + vec2(140.0, 70.0), "and keeps its 100 × 50 size: {moved:?}");
|
||||
frame(&mut app, &ctx, vec![space(false, Modifiers::NONE), Event::PointerMoved(at + vec2(180.0, 95.0))]);
|
||||
let sized = drawn(&mut app);
|
||||
assert_eq!((sized[0], sized[2]), (at + vec2(40.0, 20.0), at + vec2(180.0, 95.0)), "let go, the drag sizes it again: {sized:?}");
|
||||
let middle = Xf::new(rect, app.view().unwrap()).to_doc(at + vec2(110.0, 57.5));
|
||||
frame(&mut app, &ctx, vec![button(at + vec2(180.0, 95.0), false, Modifiers::NONE)]);
|
||||
let v = *app.view().unwrap();
|
||||
let want = z0 * f64::from((rect.width() / 140.0).min(rect.height() / 75.0));
|
||||
assert!((v.zoom - want).abs() < 1e-6, "the release zooms to the moved marquee: {z0} → {}, not {want}", v.zoom);
|
||||
assert!(v.center.distance(middle) < 1e-6, "centered on it: {:?}, not {middle:?}", v.center);
|
||||
// A Cmd+Space press: Space held from the press on sizes the marquee, also once Cmd is let go.
|
||||
app.select_tool("selection");
|
||||
app.view_mut().unwrap().zoom = z0;
|
||||
frame(&mut app, &ctx, vec![Event::ModifiersChanged(Modifiers::COMMAND), space(true, Modifiers::COMMAND), Event::PointerMoved(at)]);
|
||||
frame(&mut app, &ctx, vec![button(at, true, Modifiers::COMMAND)]);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at + vec2(50.0, 25.0))]);
|
||||
frame(&mut app, &ctx, vec![Event::ModifiersChanged(Modifiers::NONE), Event::PointerMoved(at + vec2(100.0, 50.0))]);
|
||||
let held = drawn(&mut app);
|
||||
assert_eq!((held[0], held[2]), (at, at + vec2(100.0, 50.0)), "Space held since the press sizes it: {held:?}");
|
||||
frame(&mut app, &ctx, vec![button(at + vec2(100.0, 50.0), false, Modifiers::NONE)]);
|
||||
frame(&mut app, &ctx, vec![space(false, Modifiers::NONE)]);
|
||||
let want = z0 * f64::from((rect.width() / 100.0).min(rect.height() / 50.0));
|
||||
assert!((app.view().unwrap().zoom - want).abs() < 1e-6, "{z0} → {}, not {want}", app.view().unwrap().zoom);
|
||||
}
|
||||
|
||||
/// Space pressed after the Zoom tool's press and before the drag moves: the first movement sizes
|
||||
/// the marquee, Space then moves it, and the release zooms to it instead of stepping the zoom.
|
||||
#[test]
|
||||
fn space_before_the_zoom_drag_moves_sizes_the_marquee_first() {
|
||||
use egui::{Event, Modifiers};
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.run("prefs.set", json!({"key": "animatedZoom", "value": false})).unwrap();
|
||||
app.select_tool("zoom");
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let rect = app.canvas_rect.unwrap();
|
||||
let at = rect.center() - vec2(100.0, 50.0);
|
||||
let space = |pressed| Event::Key { key: egui::Key::Space, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE };
|
||||
let button = |pos, pressed| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
|
||||
let z0 = app.view().unwrap().zoom;
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at)]);
|
||||
frame(&mut app, &ctx, vec![button(at, true)]);
|
||||
frame(&mut app, &ctx, vec![space(true), Event::PointerMoved(at + vec2(120.0, 60.0))]);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at + vec2(140.0, 70.0))]);
|
||||
let middle = Xf::new(rect, app.view().unwrap()).to_doc(at + vec2(80.0, 40.0));
|
||||
frame(&mut app, &ctx, vec![button(at + vec2(140.0, 70.0), false)]);
|
||||
frame(&mut app, &ctx, vec![space(false)]);
|
||||
let v = *app.view().unwrap();
|
||||
let want = z0 * f64::from((rect.width() / 120.0).min(rect.height() / 60.0));
|
||||
assert!((v.zoom - want).abs() < 1e-6, "the release zooms to the 120 × 60 marquee: {z0} → {}, not {want}", v.zoom);
|
||||
assert!(v.center.distance(middle) < 1e-6, "moved by Space and centered on it: {:?}, not {middle:?}", v.center);
|
||||
}
|
||||
|
||||
/// Space pressed or let go between two pointer moves in one frame: the move before the change and
|
||||
/// the move after it each follow the Space state they came with.
|
||||
#[test]
|
||||
fn the_zoom_marquee_follows_space_and_the_pointer_in_the_order_they_came() {
|
||||
use egui::{Event, Modifiers};
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.run("prefs.set", json!({"key": "animatedZoom", "value": false})).unwrap();
|
||||
app.select_tool("zoom");
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let rect = app.canvas_rect.unwrap();
|
||||
let at = rect.center() - vec2(100.0, 50.0);
|
||||
let space = |pressed| Event::Key { key: egui::Key::Space, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE };
|
||||
let button = |pos, pressed| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
|
||||
let corners = |app: &mut VectorcraftApp| -> (Pos2, Pos2) {
|
||||
let lines: Vec<Vec<Pos2>> = shapes(app, &ctx)
|
||||
.into_iter()
|
||||
.filter_map(|s| match s {
|
||||
Shape::Path(p) if p.stroke.color == egui::epaint::ColorMode::Solid(Color32::from_white_alpha(230)) => Some(p.points),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let [line] = &lines[..] else { panic!("one marquee: {lines:?}") };
|
||||
(line[0], line[2])
|
||||
};
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at)]);
|
||||
frame(&mut app, &ctx, vec![button(at, true)]);
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at + vec2(100.0, 50.0))]);
|
||||
frame(&mut app, &ctx, vec![space(true), Event::PointerMoved(at + vec2(140.0, 70.0))]);
|
||||
// Moved with Space held, then sized after it was let go.
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at + vec2(160.0, 80.0)), space(false), Event::PointerMoved(at + vec2(200.0, 100.0))]);
|
||||
assert_eq!(corners(&mut app), (at + vec2(60.0, 30.0), at + vec2(200.0, 100.0)), "moved, then sized");
|
||||
// Sized before Space was pressed, then moved with it held.
|
||||
frame(&mut app, &ctx, vec![Event::PointerMoved(at + vec2(210.0, 105.0)), space(true), Event::PointerMoved(at + vec2(230.0, 115.0))]);
|
||||
assert_eq!(corners(&mut app), (at + vec2(80.0, 40.0), at + vec2(230.0, 115.0)), "sized, then moved");
|
||||
frame(&mut app, &ctx, vec![button(at + vec2(230.0, 115.0), false)]);
|
||||
frame(&mut app, &ctx, vec![space(false)]);
|
||||
}
|
||||
|
||||
/// Space pressed while the Zoom tool drags keeps the zoom cursor: the drag does not turn into a
|
||||
/// Hand tool pan, and with Shift also held it does not show the rotate cursor.
|
||||
#[test]
|
||||
fn space_during_a_zoom_drag_keeps_the_zoom_cursor() {
|
||||
use egui::{CursorIcon, Event, Modifiers};
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.run("prefs.set", json!({"key": "animatedZoom", "value": false})).unwrap();
|
||||
app.select_tool("zoom");
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let c = app.canvas_rect.unwrap().center();
|
||||
// `shown` returns the system cursor the canvas sets over a window without the focus, as in
|
||||
// `the_rotate_cursor_shows_for_space_shift_and_the_rotate_view_tool`.
|
||||
let shown = |app: &mut VectorcraftApp, events: Vec<Event>| {
|
||||
let screen_rect = Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0)));
|
||||
let mut out = ctx.run_ui(egui::RawInput { screen_rect, focused: false, events, ..Default::default() }, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
out.platform_output.cursor_icon
|
||||
};
|
||||
let space = |pressed| Event::Key { key: egui::Key::Space, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE };
|
||||
let button = |pos, pressed| Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
|
||||
assert_eq!(shown(&mut app, vec![Event::PointerMoved(c)]), CursorIcon::ZoomIn);
|
||||
shown(&mut app, vec![button(c, true)]);
|
||||
shown(&mut app, vec![Event::PointerMoved(c + vec2(40.0, 30.0))]);
|
||||
assert_eq!(shown(&mut app, vec![space(true), Event::PointerMoved(c + vec2(60.0, 40.0))]), CursorIcon::ZoomIn, "Space during the drag");
|
||||
assert_eq!(shown(&mut app, vec![Event::ModifiersChanged(Modifiers::SHIFT)]), CursorIcon::ZoomIn, "Space+Shift during the drag");
|
||||
shown(&mut app, vec![Event::ModifiersChanged(Modifiers::NONE)]);
|
||||
shown(&mut app, vec![button(c + vec2(60.0, 40.0), false)]);
|
||||
assert_eq!(shown(&mut app, vec![]), CursorIcon::Grab, "after the drag, Space shows the hand");
|
||||
shown(&mut app, vec![space(false)]);
|
||||
}
|
||||
|
||||
/// In a rotated view the Selection tool's marquee is drawn where the tool selects: through the
|
||||
/// screen corners of the document rectangle between the press and the pointer.
|
||||
#[test]
|
||||
|
||||
@@ -694,6 +694,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Duplicate Artboards, the Artboard tool's Alt-drag and Paste give the new artboard an id that no other artboard has, also when an artboard's id is the largest possible value (#1718). Deleting the copy or the original then leaves the other's guides.
|
||||
- **Touch Type:** a click with the Touch Type tool (Shift+T) selects one character and shows a box around it. Dragging the box or its lower-left handle moves the character. The upper-left handle scales it vertically, the lower-right handle scales it horizontally, the upper-right handle scales it proportionally, and the upper-middle handle rotates it. Each drag is one undo step that sets the character's kerning, baseline shift, scales or rotation, and the type stays editable (`text.touchType`; behavior reference: [Touch Type tool](https://web.archive.org/web/20261007185617/https://helpx.adobe.com/illustrator/using/tool-techniques/touch-type-tool.html), [Add text with the Type, Area Type, and Touch Type tools](https://web.archive.org/web/20260926095922/https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/add-text-illustrator-text-tools.html)).
|
||||
- **Temporary tools:** Cmd with any other tool drags with the selection tool used last (Selection, Direct Selection or Group Selection). Held Space pans and Cmd+Space zooms (Cmd+Alt+Space out) whatever the tool.
|
||||
Space held while the Zoom tool drags a marquee moves the marquee and keeps its size ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). When Space is let go, the drag sizes the marquee again from where it moved to.
|
||||
A Space+Shift drag rotates the view whatever the tool, as a Rotate View tool drag does, and Shift does not round the angle to 15°.
|
||||
- **Swatches panel clicks:** Ctrl/Cmd-clicking New Swatch makes a spot color, and Ctrl/Cmd+Shift-clicking it makes a global process color.
|
||||
- **Panel drag and drop:** art dragged off the canvas onto the Symbols panel becomes a symbol, art dragged onto the Swatches panel becomes a pattern swatch, and art dropped on the Brushes panel opens New Brush for that art; a symbol dragged onto the canvas places an instance where it is dropped, and a brush dragged onto a path is applied to it.
|
||||
|
||||
Reference in New Issue
Block a user