mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Spiral tool: Alt-drag adds or removes whole winds of four segments
Alt in a spiral drag now rounds the fitted segment count to whole winds of four segments, counted from the segment count when Alt went down. Ctrl/Cmd takes precedence when both keys are held, and switching back to Alt starts from the current count with the held inner end kept. Arrow keys, Escape, release and a new drag clear the held Alt count. Tests cover both keys together, a switch mid-drag, Escape, the 2 to 1000 bounds and the committed path's anchor count. docs/mcp.md and docs/target-app-parity.md describe the rule.
This commit is contained in:
@@ -1275,9 +1275,9 @@ fn shape_arc_closed_along_y_closes_through_the_top_left_corner() {
|
||||
assert!((area - std::f64::consts::FRAC_PI_4).abs() < 0.01, "{area}");
|
||||
}
|
||||
|
||||
/// A spiral dragged out with Alt pressed part way gets more segments as its outer end moves out,
|
||||
/// and one dragged with Ctrl/Cmd pressed part way gets the decay that keeps its inner end's
|
||||
/// distance from the center; the Spiral tool keeps both for the next spiral.
|
||||
/// A spiral dragged out with Alt pressed part way gets four more segments per wind as its outer end
|
||||
/// moves out. Ctrl/Cmd pressed part way fits the decay so the inner end keeps its distance from the
|
||||
/// center. The Spiral tool keeps both values for the next spiral.
|
||||
#[test]
|
||||
fn spiral_drags_with_alt_and_ctrl_change_its_segments_and_decay() {
|
||||
let mut s = session();
|
||||
@@ -1293,17 +1293,17 @@ fn spiral_drags_with_alt_and_ctrl_change_its_segments_and_decay() {
|
||||
};
|
||||
s.select_tool("spiral", v).unwrap();
|
||||
let undo = undo_steps(&s);
|
||||
// Alt from a radius of 100 to 200: 10 segments become 13, the outer end at the pointer.
|
||||
// Alt from a radius of 100 to 200: 10 segments become 14, one full wind of four quarter turns.
|
||||
let (anchors, outer) = spiral(&mut s, Mods { alt: true, ..Mods::default() }, (300.0, 100.0));
|
||||
assert_eq!(anchors, 14);
|
||||
assert_eq!(anchors, 15);
|
||||
assert!(near(outer, Point::new(300.0, 100.0)), "{outer:?}");
|
||||
assert_eq!(s.tool_options()["segments"], json!(13));
|
||||
// Ctrl/Cmd from 100 to 50: the decay rises from 80 % to 80 × 2^(1/13) %.
|
||||
assert_eq!(s.tool_options()["segments"], json!(14));
|
||||
// Ctrl/Cmd from 100 to 50: the decay rises from 80 % to 80 × 2^(1/14) %.
|
||||
let (anchors, outer) = spiral(&mut s, Mods { cmd: true, ..Mods::default() }, (150.0, 100.0));
|
||||
assert_eq!(anchors, 14);
|
||||
assert_eq!(anchors, 15);
|
||||
assert!(near(outer, Point::new(150.0, 100.0)), "{outer:?}");
|
||||
let decay = s.tool_options()["decay"].as_f64().unwrap();
|
||||
assert!((decay - 80.0 * 2f64.powf(1.0 / 13.0)).abs() < 1e-9, "{decay}");
|
||||
assert!((decay - 80.0 * 2f64.powf(1.0 / 14.0)).abs() < 1e-9, "{decay}");
|
||||
assert_eq!(undo_steps(&s) - undo, 2, "one undo step a spiral");
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
//! While dragging, ↑/↓ change the arc's slope, the spiral's segments, the grid rows or the
|
||||
//! concentric dividers; ←/→ change grid columns / radial dividers. C switches the arc between open
|
||||
//! and closed, and F switches its base between the X and Y axes. While a spiral is dragged out, Alt
|
||||
//! and Ctrl/Cmd keep its inner end at the distance from the center it had when the key went down as
|
||||
//! the outer end follows the pointer: Alt adds or removes segments, and Ctrl/Cmd changes the decay.
|
||||
//! and Ctrl/Cmd keep its inner end at the distance from the center on the first modified drag as
|
||||
//! the outer end follows the pointer: Alt adds or removes whole winds of four segments, and
|
||||
//! Ctrl/Cmd changes the decay.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_geom::Point;
|
||||
@@ -38,6 +39,8 @@ pub struct FamilyTool {
|
||||
/// The natural logarithm of the radius of the spiral's inner end, held while Alt or Ctrl/Cmd
|
||||
/// is down in a drag.
|
||||
spiral_end: Option<f64>,
|
||||
/// The segment count when Alt alone starts adjusting winds.
|
||||
spiral_alt_base: Option<u32>,
|
||||
/// Rows / concentric dividers.
|
||||
pub rows: u32,
|
||||
/// Columns / radial dividers.
|
||||
@@ -81,6 +84,7 @@ impl FamilyTool {
|
||||
segments: 10,
|
||||
clockwise: true,
|
||||
spiral_end: None,
|
||||
spiral_alt_base: None,
|
||||
rows: 5,
|
||||
columns: 5,
|
||||
skew: [0.0; 2],
|
||||
@@ -121,9 +125,10 @@ impl FamilyTool {
|
||||
}
|
||||
|
||||
/// Alt or Ctrl/Cmd in a spiral drag from `start` to `p`: the inner end keeps the distance from
|
||||
/// the center it had when the key went down while the outer end follows the pointer. With
|
||||
/// the center it had on the first modified drag while the outer end follows the pointer. With
|
||||
/// Ctrl/Cmd the decay changes to fit (within 5…99.99 %, the decays `shapes::spiral` draws);
|
||||
/// with Alt alone the segments change to the nearest whole number that fits (within 2…1000).
|
||||
/// with Alt alone the count changes in whole winds of four segments from its Alt entry count
|
||||
/// (within 2…1000). Ctrl/Cmd takes precedence when both modifiers are held.
|
||||
/// The new values stay once the key is let go.
|
||||
fn hold_spiral_end(&mut self, start: Point, p: Point, m: Mods) {
|
||||
let r = p.distance(start).max(0.01);
|
||||
@@ -132,6 +137,11 @@ impl FamilyTool {
|
||||
(false, Some(_)) => self.spiral_end = None,
|
||||
_ => {}
|
||||
}
|
||||
if m.alt && !m.cmd {
|
||||
self.spiral_alt_base.get_or_insert(self.segments);
|
||||
} else {
|
||||
self.spiral_alt_base = None;
|
||||
}
|
||||
let Some(end) = self.spiral_end.filter(|e| e.is_finite()) else { return };
|
||||
if m.cmd {
|
||||
let ratio = ((end - r.ln()) / f64::from(self.segments)).exp();
|
||||
@@ -139,9 +149,10 @@ impl FamilyTool {
|
||||
self.decay = (ratio * 100.0).clamp(5.0, 99.99);
|
||||
}
|
||||
} else {
|
||||
let winds = (r.ln() - end) / -(self.decay / 100.0).clamp(0.05, 0.9999).ln();
|
||||
if winds.is_finite() {
|
||||
self.segments = winds.round().clamp(2.0, 1000.0) as u32;
|
||||
let fitted = (r.ln() - end) / -(self.decay / 100.0).clamp(0.05, 0.9999).ln();
|
||||
if fitted.is_finite() {
|
||||
let base = f64::from(self.spiral_alt_base.unwrap_or(self.segments));
|
||||
self.segments = (base + 4.0 * ((fitted - base) / 4.0).round()).clamp(2.0, 1000.0) as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,6 +238,7 @@ impl Tool for FamilyTool {
|
||||
self.last = p;
|
||||
self.began = false;
|
||||
self.spiral_end = None;
|
||||
self.spiral_alt_base = None;
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Drag => {
|
||||
@@ -255,6 +267,7 @@ impl Tool for FamilyTool {
|
||||
let Some(s) = self.start.take() else { return vec![] };
|
||||
self.snap.clear();
|
||||
self.spiral_end = None;
|
||||
self.spiral_alt_base = None;
|
||||
if std::mem::take(&mut self.began) { vec![Action::Commit] } else { vec![Action::Dialog(self.id.into(), json!({"x": s.x, "y": s.y}))] }
|
||||
}
|
||||
_ => vec![],
|
||||
@@ -309,6 +322,7 @@ impl Tool for FamilyTool {
|
||||
// An arrow with Alt or Ctrl/Cmd still held: the held inner end moves with the new segment count.
|
||||
if self.spiral_end.is_some() {
|
||||
self.spiral_end = Some(self.spiral_inner_end_ln(self.last.distance(s).max(0.01)));
|
||||
self.spiral_alt_base = (self.mods.alt && !self.mods.cmd).then_some(self.segments);
|
||||
}
|
||||
let (c, v) = self.command_at(s, self.last, self.mods, cx.constrain_angle);
|
||||
vec![Action::Preview(c, v)]
|
||||
@@ -316,6 +330,7 @@ impl Tool for FamilyTool {
|
||||
self.start = None;
|
||||
self.began = false;
|
||||
self.spiral_end = None;
|
||||
self.spiral_alt_base = None;
|
||||
self.snap.clear();
|
||||
vec![Action::Cancel]
|
||||
} else {
|
||||
@@ -512,8 +527,9 @@ mod tests {
|
||||
|
||||
/// While a spiral is dragged out, Alt and Ctrl/Cmd keep its inner end at the distance from the
|
||||
/// center it had when the key went down as the outer end follows the pointer: Alt adds or
|
||||
/// removes segments and Ctrl/Cmd changes the decay. The new values stay once the key is let go,
|
||||
/// and for the next spiral. Both keys work with the lowest decay and many segments.
|
||||
/// removes whole winds of four segments and Ctrl/Cmd changes the decay. The new values stay
|
||||
/// once the key is let go, and for the next spiral. Both keys work with the lowest decay and
|
||||
/// many segments.
|
||||
#[test]
|
||||
fn spiral_drag_alt_changes_its_segments_and_ctrl_its_decay() {
|
||||
let (d, _) = doc_with_rect();
|
||||
@@ -532,33 +548,34 @@ mod tests {
|
||||
let mut t = FamilyTool::new("spiral");
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 100.0))), (100.0, 80.0, 10));
|
||||
// Alt: twice the radius adds ln 2 / ln 1.25 ≈ 3.1 segments, half of it takes them away.
|
||||
// Alt rounds the fitted count to whole winds from the count when Alt goes down.
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 100.0).with_mods(alt))), (100.0, 80.0, 10));
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 300.0, 100.0).with_mods(alt))), (200.0, 80.0, 13));
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 300.0, 100.0).with_mods(alt))), (200.0, 80.0, 14));
|
||||
// ↑ with Alt held adds one, and the held inner end moves with it.
|
||||
assert_eq!(spiral(t.key(&cx, ToolKey::Up, alt)).2, 14);
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 300.0, 100.0).with_mods(alt))).2, 14);
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 100.0).with_mods(alt))), (50.0, 80.0, 8));
|
||||
assert_eq!(spiral(t.key(&cx, ToolKey::Up, alt)).2, 15);
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 300.0, 100.0).with_mods(alt))).2, 15);
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 100.0).with_mods(alt))), (50.0, 80.0, 7));
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 101.0, 100.0).with_mods(alt))).2, 2, "at least two");
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 100.0).with_mods(alt))).2, 8);
|
||||
// Let go: the eight segments stay as the radius follows the pointer again.
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 100.0))), (100.0, 80.0, 8));
|
||||
// Ctrl/Cmd: the decay fits the held inner end, 80 % × (100 / r)^(1/8).
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 100.0).with_mods(alt))).2, 7);
|
||||
// The count stays as the radius follows the pointer again.
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 100.0))), (100.0, 80.0, 7));
|
||||
// Ctrl/Cmd fits the decay to the held inner end.
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 100.0).with_mods(ctrl));
|
||||
let (r, decay, n) = spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 100.0).with_mods(ctrl)));
|
||||
assert!((r, n) == (50.0, 8) && (decay - 80.0 * 2f64.powf(1.0 / 8.0)).abs() < 1e-9, "{r} {decay} {n}");
|
||||
assert!((r, n) == (50.0, 7) && (decay - 80.0 * 2f64.powf(1.0 / 7.0)).abs() < 1e-9, "{r} {decay} {n}");
|
||||
let (_, decay, _) = spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 400.0, 100.0).with_mods(ctrl)));
|
||||
assert!((decay - 80.0 * (1.0f64 / 3.0).powf(1.0 / 8.0)).abs() < 1e-9, "{decay}");
|
||||
assert!((decay - 80.0 * (1.0f64 / 3.0).powf(1.0 / 7.0)).abs() < 1e-9, "{decay}");
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 100.5, 100.0).with_mods(ctrl))).1, 99.99, "at most 99.99 %");
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 1e20, 100.0).with_mods(ctrl))).1, 5.0, "at least 5 %");
|
||||
let (_, decay, _) = spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 300.0, 100.0).with_mods(ctrl)));
|
||||
assert!((decay - 80.0 * 0.5f64.powf(1.0 / 8.0)).abs() < 1e-9, "{decay}");
|
||||
assert!((decay - 80.0 * 0.5f64.powf(1.0 / 7.0)).abs() < 1e-9, "{decay}");
|
||||
// Let go: the decay stays, and the spiral drawn next keeps both.
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 250.0, 100.0))), (150.0, decay, 8));
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 250.0, 100.0))), (150.0, decay, 7));
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 250.0, 100.0)), vec![Action::Commit]);
|
||||
assert_eq!((t.options()["decay"].as_f64(), t.options()["segments"].clone()), (Some(decay), json!(8)));
|
||||
assert_eq!((t.options()["decay"].as_f64(), t.options()["segments"].clone()), (Some(decay), json!(7)));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
|
||||
let (_, d2, n2) = spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 100.0, 140.0).with_mods(alt)));
|
||||
assert_eq!((d2, n2), (decay, 8), "a new drag holds the end anew");
|
||||
assert_eq!((d2, n2), (decay, 7), "a new drag holds the end anew");
|
||||
// 5 % and 300 segments: an inner radius of 100 × 0.05^300 is below the smallest f64.
|
||||
let mut t = FamilyTool::new("spiral");
|
||||
t.set_option("decay", &json!(5.0));
|
||||
@@ -568,9 +585,86 @@ mod tests {
|
||||
let (_, decay, _) = spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 100.0).with_mods(ctrl)));
|
||||
assert!((decay - 5.0 * 2f64.powf(1.0 / 300.0)).abs() < 1e-9, "{decay}");
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 150.0, 100.0));
|
||||
// Alt: 200 times the radius adds ln 200 / ln (100 / decay) ≈ 1.8 segments.
|
||||
// Alt: a large radius increase adds a whole wind.
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 120.0, 100.0).with_mods(alt));
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 4100.0, 100.0).with_mods(alt))).2, 302);
|
||||
assert_eq!(spiral(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200100.0, 100.0).with_mods(alt))).2, 304);
|
||||
}
|
||||
|
||||
/// Switching between Alt and Ctrl/Cmd keeps the held inner end and uses the current segment
|
||||
/// count when Alt resumes. Escape clears the held end before the next drag.
|
||||
#[test]
|
||||
fn spiral_drag_switches_between_alt_and_ctrl_and_escape_lets_go_of_the_inner_end() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let mut cx = cx(&d, &s, &p);
|
||||
cx.smart_guides = false;
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
let ctrl = Mods { cmd: true, ..Mods::default() };
|
||||
let both = Mods { alt: true, cmd: true, ..Mods::default() };
|
||||
let mut t = FamilyTool::new("spiral");
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 100.0).with_mods(alt));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 300.0, 100.0).with_mods(alt));
|
||||
assert_eq!(t.segments, 14);
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 100.0).with_mods(both));
|
||||
assert_eq!(t.segments, 14, "Ctrl/Cmd takes precedence");
|
||||
let decay = t.decay;
|
||||
assert!(decay > 80.0);
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 100.0).with_mods(ctrl));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 300.0, 100.0).with_mods(alt));
|
||||
assert_eq!(t.segments, 18, "Alt resumes from the current count with the held end");
|
||||
assert_eq!(t.decay, decay);
|
||||
t.key(&cx, ToolKey::Escape, alt);
|
||||
assert_eq!(t.spiral_end, None);
|
||||
assert_eq!(t.spiral_alt_base, None);
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 100.0).with_mods(alt));
|
||||
assert_eq!(t.segments, 18, "a new drag holds a new inner end");
|
||||
}
|
||||
|
||||
/// Alt adjusts the segment count in whole winds while keeping it within 2 to 1000.
|
||||
#[test]
|
||||
fn spiral_drag_alt_keeps_the_segments_within_2_to_1000() {
|
||||
let mut t = FamilyTool::new("spiral");
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
let start = Point::new(0.0, 0.0);
|
||||
t.hold_spiral_end(start, Point::new(100.0, 0.0), alt);
|
||||
t.hold_spiral_end(start, Point::new(200.0, 0.0), alt);
|
||||
assert_eq!(t.segments, 14);
|
||||
t.hold_spiral_end(start, Point::new(0.01, 0.0), alt);
|
||||
assert_eq!(t.segments, 2);
|
||||
t.hold_spiral_end(start, Point::new(1e150, 0.0), alt);
|
||||
assert_eq!(t.segments, 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spiral_drag_arrows_and_release_clear_the_right_held_state() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let mut cx = cx(&d, &s, &p);
|
||||
cx.smart_guides = false;
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
let both = Mods { alt: true, cmd: true, ..Mods::default() };
|
||||
let mut t = FamilyTool::new("spiral");
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 100.0).with_mods(both));
|
||||
t.key(&cx, ToolKey::Up, both);
|
||||
assert_eq!(t.segments, 11);
|
||||
assert_eq!(t.spiral_alt_base, None, "Ctrl/Cmd still takes precedence after Up");
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 200.0, 100.0).with_mods(alt));
|
||||
assert_eq!(t.spiral_alt_base, Some(11), "Alt starts from the current count");
|
||||
t.key(&cx, ToolKey::Down, alt);
|
||||
assert_eq!(t.segments, 10);
|
||||
assert_eq!(t.spiral_alt_base, Some(10));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 200.0, 100.0).with_mods(alt));
|
||||
assert_eq!(t.spiral_end, None);
|
||||
assert_eq!(t.spiral_alt_base, None);
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 100.0, 100.0));
|
||||
assert_eq!(t.spiral_end, None, "Up after Down clears the held end");
|
||||
assert_eq!(t.spiral_alt_base, None, "Up after Down clears the Alt base");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+6
-5
@@ -487,11 +487,12 @@ dragged, `c` chooses the tool bound to C, and `f` runs the shortcut bound to F i
|
||||
binding for it).
|
||||
|
||||
While a spiral is being dragged out with the Spiral tool, a `drag` event with `mods.alt` or `mods.cmd` keeps the
|
||||
spiral's inner end at the distance from the center it had at the first such event, and the outer end follows the
|
||||
pointer. With `mods.alt` the number of segments changes to the nearest whole number of segments that fits (within
|
||||
2…1000), so dragging outward adds segments and dragging inward removes them; with `mods.cmd` the decay changes to fit
|
||||
(within 5…99.99 %). The spiral drawn on release uses the new values, and they stay as the Spiral tool's `segments` and
|
||||
`decay` options for the next spiral:
|
||||
spiral's inner end at the distance from the center it had on the first modified `drag` event, and the outer end follows
|
||||
the pointer. With `mods.alt` alone, dragging outward adds whole winds of four segments and dragging inward removes
|
||||
them. The fitted segment count rounds to the nearest whole wind from the count on the first Alt-only `drag`, within 2…1000.
|
||||
With `mods.cmd`, decay changes to fit within 5…99.99 %; it takes precedence when both modifiers are held. Switching
|
||||
back to Alt uses the current count as its new base while keeping the held inner end. The spiral drawn on release uses
|
||||
the new values, and they stay as the Spiral tool's `segments` and `decay` options for the next spiral:
|
||||
|
||||
```json
|
||||
{"name":"pointer_gesture","arguments":{"tool":"spiral","events":[{"kind":"down","x":100,"y":100},{"kind":"drag","x":200,"y":100,"mods":{"alt":true}},{"kind":"drag","x":300,"y":100,"mods":{"alt":true}},{"kind":"up","x":300,"y":100}]}}
|
||||
|
||||
@@ -673,7 +673,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. The Flare tool's center drag turns the rays with the pointer, Shift keeps them in their original orientation (unturned, or turned by a nonzero General › Constrain Angle), and Space held during either of its drags moves the flare. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812).
|
||||
- **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. The Flare tool's center drag turns the rays with the pointer, Shift keeps them in their original orientation, and Space held during either of its drags moves the flare. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812).
|
||||
While an arc is dragged out, C switches it between open and closed, F switches its base between the X and Y axes in the same box, and Up/Down raise and lower its slope by 10 within −100…100; each press switches once, and the values stay as the Arc tool's options. The [default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) (archived copy) list C, F and Up/Down for an arc being dragged out. F as the base switch, the arrows changing the slope, the step of 10 and one switch per press are our own rules.
|
||||
While a spiral is dragged out, Alt adds or removes segments and Ctrl/Cmd changes the decay; both keep the spiral's inner end at the distance from the center it had when the key went down while the outer end follows the pointer, and the values stay as the Spiral tool's options. The [default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) (archived copy) list Alt-drag to add or subtract winds and Ctrl-drag to change the decay rate. The held inner radius, rounding to whole segments, the ranges (2…1000 segments, 5…99.99 % decay) and the new values staying are our own rules.
|
||||
While a spiral is dragged out, Alt adds or removes whole winds of four segments and Ctrl/Cmd changes the decay. The spiral's inner end keeps the distance from the center it had on the first modified drag event, while the outer end follows the pointer. The [default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) (archived copy) list Alt-drag to add or subtract winds and Ctrl-drag to change the decay rate; [Draw spirals](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-spirals.html) defines four segments per full wind. Rounding the fitted count to whole winds from the count on the first Alt-only drag event, the held inner radius, the ranges (2…1000 segments, 5…99.99 % decay), Ctrl/Cmd taking precedence when both are held, and the new values staying as Spiral tool options are our own rules. Switching back to Alt uses the current count as its new base while keeping the held inner end.
|
||||
- **Cutting:** Eraser, Scissors, Knife, Mirror & Cut, Line Cut and Rectangle Cut (real geometry, compound paths keep their holes).
|
||||
While Alt is held, the Scissors tool works as the Add Anchor Point tool ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)): a click on a segment adds an anchor point without cutting (`path.insertAnchor`), and the pointer shows the Add Anchor Point cursor. When Alt is let go, a click cuts again.
|
||||
Eraser Tool Options has Angle and Roundness besides Size, as [Erase paths using the Eraser tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/erase-paths-using-eraser-tool.html) describes: the Eraser's tip is an ellipse Size across, its long axis at Angle and its short axis Roundness percent of the long one, and a drag with a tip that is not round shows the outline of the area it erases (`path.eraseRegion` `angle` and `roundness`).
|
||||
|
||||
Reference in New Issue
Block a user