mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-06 18:52:38 +00:00
Stroke Weight Up and Down: Cmd/Ctrl+Alt+. and , step the selected strokes' weight (#5797)
This commit is contained in:
@@ -24,6 +24,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_doc,
|
||||
stroke_set
|
||||
),
|
||||
cmd!(
|
||||
"stroke.weightUp",
|
||||
"Stroke Weight Up",
|
||||
[],
|
||||
Some("Cmd+Alt+."),
|
||||
"{ids?|id?} raise each selected stroke's weight by one step of Units › Stroke (a quarter unit below one unit, then whole units; up to 1000 pt), each from its own weight: the selected objects' active or top stroke, type's stroked characters; objects without a stroke are left alone. With nothing selected (and no ids), the weight of the next object drawn → {objects}",
|
||||
has_doc,
|
||||
|s, p| stroke_step(s, p, true)
|
||||
),
|
||||
cmd!(
|
||||
"stroke.weightDown",
|
||||
"Stroke Weight Down",
|
||||
[],
|
||||
Some("Cmd+Alt+,"),
|
||||
"{ids?|id?} lower each selected stroke's weight by one step, as stroke.weightUp raises it, down to a quarter unit (a thinner weight is kept) → {objects}",
|
||||
has_doc,
|
||||
|s, p| stroke_step(s, p, false)
|
||||
),
|
||||
cmd!(
|
||||
"stroke.setAdvanced",
|
||||
"Stroke Options",
|
||||
@@ -261,6 +279,61 @@ fn stroke_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"objects": ids.iter().collect::<std::collections::BTreeSet<_>>().len()}))
|
||||
}
|
||||
|
||||
/// `w` (pt) one step up or down in `unit` (Units › Stroke): a quarter unit up to one unit,
|
||||
/// whole units above it, from 0.25 unit to 1000 pt. A weight between steps goes to the next step
|
||||
/// in that direction (0.6 pt up is 0.75 pt); a weight below a quarter unit doesn't go down.
|
||||
pub(crate) fn step_weight(w: f64, unit: Unit, up: bool) -> f64 {
|
||||
const EPS: f64 = 1e-9;
|
||||
let k = Some(unit.to_pt(1.0)).filter(|k| k.is_finite() && *k > 0.0).unwrap_or(1.0);
|
||||
let w = if w.is_finite() { w.max(0.0) } else { 0.0 };
|
||||
let v = w / k;
|
||||
let next = if up {
|
||||
if v < 1.0 - EPS { ((v * 4.0 + EPS).floor() + 1.0) / 4.0 } else { (v + EPS).floor() + 1.0 }
|
||||
} else if v <= 1.0 + EPS {
|
||||
(((v * 4.0 - EPS).ceil() - 1.0) / 4.0).max(0.25)
|
||||
} else {
|
||||
(v - EPS).ceil() - 1.0
|
||||
};
|
||||
let r = (next * k).min(1000.0);
|
||||
// Never a step the wrong way: past 1000 pt up stays, below a quarter unit down stays.
|
||||
if up { r.max(w) } else { r.min(w) }
|
||||
}
|
||||
|
||||
/// `stroke.weightUp` / `stroke.weightDown`: each target stroke stepped from its own weight.
|
||||
fn stroke_step(s: &mut Session, p: &Value, up: bool) -> Result<Value> {
|
||||
let c = if up { "stroke.weightUp" } else { "stroke.weightDown" };
|
||||
let unit = s.stroke_unit();
|
||||
let item = item_target(s, p, c)?.of_kind(s, false);
|
||||
let ids = item.targets(s, p)?;
|
||||
if no_targets(&ids, p) {
|
||||
let w = step_weight(s.paint.stroke_width, unit, up);
|
||||
s.paint.stroke_width = w;
|
||||
if let Some(st) = s.new_art_stroke_mut() {
|
||||
st.width = w;
|
||||
}
|
||||
return Ok(json!({"objects": 0}));
|
||||
}
|
||||
let label = if up { "Stroke Weight Up" } else { "Stroke Weight Down" };
|
||||
edit_items(s, &ids, item, c, label, false, |n, index| {
|
||||
if index.is_none()
|
||||
&& let NodeKind::Text(t) = &mut n.kind
|
||||
{
|
||||
// Character strokes are drawn in text space, scaled by the type's transform.
|
||||
let k = Scaling::factor(t.xf).filter(|k| k.is_finite() && *k > 1e-12).unwrap_or(1.0);
|
||||
for r in t.runs.iter_mut().filter(|r| !matches!(r.style.stroke, Paint::None)) {
|
||||
let change = StrokeChange { weight: Some(step_weight(r.style.stroke_width * k, unit, up)), ..StrokeChange::default() };
|
||||
change.apply_char_scaled(&mut r.style, k);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(st) = n.appearance.stroke_at_mut(index) {
|
||||
st.width = step_weight(st.width, unit, up);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({"objects": ids.iter().collect::<std::collections::BTreeSet<_>>().len()}))
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Units > Stroke (Preferences): the unit stroke weights and dash lengths show in.
|
||||
pub fn stroke_unit(&self) -> Unit {
|
||||
@@ -514,3 +587,23 @@ fn profile_list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let current = s.shown_stroke().map(|st| s.profile_entry(st.profile.as_ref()).map_or("custom", |e| e.id).to_string());
|
||||
Ok(json!({ "profiles": profiles, "current": current }))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn weight_steps_follow_units_and_stop_at_their_bounds() {
|
||||
assert_eq!(step_weight(0.6, Unit::Points, true), 0.75);
|
||||
assert_eq!(step_weight(0.6, Unit::Points, false), 0.5);
|
||||
assert_eq!(step_weight(2.5, Unit::Points, true), 3.0);
|
||||
assert_eq!(step_weight(2.5, Unit::Points, false), 2.0);
|
||||
assert_eq!(step_weight(0.1, Unit::Points, false), 0.1, "below a quarter point a step down keeps it");
|
||||
assert_eq!(step_weight(1000.0, Unit::Points, true), 1000.0);
|
||||
assert_eq!(step_weight(2000.0, Unit::Points, true), 2000.0, "a step up never lowers a weight");
|
||||
assert_eq!(step_weight(f64::NAN, Unit::Points, true), 0.25);
|
||||
let mm = Unit::Millimeters.to_pt(1.0);
|
||||
assert!((step_weight(mm, Unit::Millimeters, true) - 2.0 * mm).abs() < 1e-9);
|
||||
assert!((step_weight(mm, Unit::Millimeters, false) - 0.75 * mm).abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,3 +111,49 @@ fn inspect_reports_stroke_options() {
|
||||
let text = doc["layers"][0]["children"].as_array().unwrap().iter().find(|c| c["kind"] == "Text" || c.get("text").is_some()).unwrap().clone();
|
||||
assert_eq!(text["strokeOptions"]["join"], "round", "{text}");
|
||||
}
|
||||
|
||||
/// Stroke Weight Up and Down (Cmd/Ctrl+Alt+. and ,) step each selected stroke from its own
|
||||
/// weight, a quarter point at a time below 1 pt and whole points above, in one undo step each.
|
||||
#[test]
|
||||
fn stroke_weight_up_and_down_step_each_selected_stroke() {
|
||||
let mut s = session();
|
||||
let (a, b, bare) = (rect(&mut s, 10.0), rect(&mut s, 60.0), rect(&mut s, 110.0));
|
||||
select(&mut s, &[a]);
|
||||
run(&mut s, "stroke.set", json!({"weight": 0.6}));
|
||||
select(&mut s, &[b]);
|
||||
run(&mut s, "stroke.set", json!({"weight": 3}));
|
||||
select(&mut s, &[bare]);
|
||||
run(&mut s, "paint.setStroke", json!({"none": true}));
|
||||
let has_stroke = |s: &Session| {
|
||||
s.doc().unwrap().doc.node(bare).unwrap().appearance.stroke().is_some_and(|st| !matches!(st.paint, vectorcraft_color::Paint::None))
|
||||
};
|
||||
let bare_before = has_stroke(&s);
|
||||
let w = |s: &Session, id| s.doc().unwrap().doc.node(id).unwrap().appearance.stroke_width();
|
||||
select(&mut s, &[a, b, bare]);
|
||||
run(&mut s, "stroke.weightUp", json!({}));
|
||||
assert_eq!((w(&s, a), w(&s, b)), (0.75, 4.0));
|
||||
run(&mut s, "stroke.weightUp", json!({}));
|
||||
assert_eq!((w(&s, a), w(&s, b)), (1.0, 5.0));
|
||||
assert_eq!(has_stroke(&s), bare_before, "an object without a stroke takes none");
|
||||
for _ in 0..6 {
|
||||
run(&mut s, "stroke.weightDown", json!({}));
|
||||
}
|
||||
// 1 → 0.75 → 0.5 → 0.25, where it stays; 5 → 4 → 3 → 2 → 1 → 0.75 → 0.5.
|
||||
assert_eq!((w(&s, a), w(&s, b)), (0.25, 0.5));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!((w(&s, a), w(&s, b)), (0.25, 0.75), "one undo step per press");
|
||||
}
|
||||
|
||||
/// With nothing selected the next object drawn takes the stepped weight.
|
||||
#[test]
|
||||
fn stroke_weight_up_with_nothing_selected_sets_up_new_art() {
|
||||
let mut s = session();
|
||||
run(&mut s, "select.none", json!({}));
|
||||
run(&mut s, "stroke.set", json!({"weight": 2}));
|
||||
run(&mut s, "stroke.weightUp", json!({}));
|
||||
assert_eq!(s.paint.stroke_width, 3.0);
|
||||
let r = rect(&mut s, 10.0);
|
||||
assert_eq!(s.doc().unwrap().doc.node(r).unwrap().appearance.stroke_width(), 3.0);
|
||||
assert_eq!(crate::cmd::find_command("stroke.weightUp").unwrap().shortcut, Some("Cmd+Alt+."));
|
||||
assert_eq!(crate::cmd::find_command("stroke.weightDown").unwrap().shortcut, Some("Cmd+Alt+,"));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Parity with Adobe Illustrator
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -1670,6 +1670,10 @@ A VectorCraft addition, not a feature of the reference app, so no area score mov
|
||||
|
||||
A VectorCraft addition (it replaces a script some users ran), so no area score moves. Object › Plug-ins › Color Effects › Create Swatch Legend… (`addon.color.swatchLegend`) opens a dialog (columns, chip size, gap, values, selected swatches only, with a preview) and draws a chip per colour swatch (the selected ones, else all) filled with and linked to its swatch, labelled with its name and values (HEX, RGB, CMYK, Lab or Grayscale; by default its own model; spot swatches marked), black on light chips and white on dark ones, in a grid of chosen columns, chip size and spacing, as one selected group in one undo step (#4919). Tests: `cmd::addons::color::legend::tests` (engine), `addons::color::legend::tests::create_swatch_legend_opens_its_dialog_previews_and_runs_on_ok` (ui-egui).
|
||||
|
||||
## Stroke Weight Up and Down shortcuts (2026-11-06)
|
||||
|
||||
Cmd/Ctrl+Alt+. (`stroke.weightUp`) and Cmd/Ctrl+Alt+, (`stroke.weightDown`) raise and lower the weight of each selected stroke from its own weight, with the keys a [public feature request reply](https://illustrator.uservoice.com/forums/333657-illustrator-desktop-feature-requests/suggestions/32307397-increment-decrement-stroke-weight-key-commands) names: a quarter unit of Units › Stroke at a time up to one unit, whole units above it, no lower than a quarter unit and no higher than 1000 pt. Type steps its stroked characters; objects without a stroke are left alone; with nothing selected the weight of the next object drawn steps. Each press is one undo step (#5797). Evidence: `tests::strokeux::stroke_weight_up_and_down_step_each_selected_stroke`, `stroke_weight_up_with_nothing_selected_sets_up_new_art`, `cmd::stroke::tests::weight_steps_follow_units_and_stop_at_their_bounds`. Scores unchanged.
|
||||
|
||||
## Keyboard shortcuts for Effect menu items (2026-11-01)
|
||||
|
||||
Edit › Keyboard Shortcuts lists every item of the Effect menu under its menu path (Effect › Distort & Transform › Transform…) and gives each one a shortcut of its own (stored as `effect:<id>`; they all run `effect.dialog {effect}`, or `effect.apply` for an effect without options, so a shortcut on the command alone couldn't tell them apart). The menu shows the shortcut, and pressing it opens that effect's dialog for the selection (#3296). Test: `shortcuts::tests::an_effect_menu_item_takes_a_shortcut_of_its_own`.
|
||||
@@ -1698,6 +1702,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-06 | minor | Stroke Weight Up and Down (Cmd/Ctrl+Alt+. and ,) step each selected stroke's weight (#5797); evidence added; scores unchanged. |
|
||||
| 2026-11-06 | minor | Hindi (`hi`): 7,877-row machine-assisted catalog, zero/one singular plurals, desktop font fallback and optional Devanagari craft-fonts fallback; all 55 translation tests pass; native review and a web font remain; scores and estimates unchanged |
|
||||
| 2026-11-06 | minor | Properties in Edit Artboards: a Transform section (reference point, X, Y, W, H, preset, orientation, Move and Scale Artwork) above the Artboards section (name, Artboard Options, Fill), X and Y of the shared reference point (#5219); evidence updated; scores unchanged. |
|
||||
| 2026-11-06 | minor | Scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields; a click still opens the panel (#4088); evidence updated; scores unchanged. |
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -314,6 +314,9 @@ panel-menu commands such as Break Link to Symbol), so they can be given shortcut
|
||||
Align panel's Align Objects, Distribute Objects and Distribute Spacing rows under Align, with no default shortcut, and a
|
||||
shortcut set there runs what a click on the button runs, with the panel's Align To choice and Distribute Spacing value. The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661).
|
||||
|
||||
Cmd/Ctrl+Alt+. (`stroke.weightUp`, Stroke Weight Up) and Cmd/Ctrl+Alt+, (`stroke.weightDown`, Stroke Weight Down), the keys a [public feature request reply](https://illustrator.uservoice.com/forums/333657-illustrator-desktop-feature-requests/suggestions/32307397-increment-decrement-stroke-weight-key-commands) gives them, step the weight of each selected stroke from its own weight: by a quarter unit of Units › Stroke up to one
|
||||
unit, then by whole units, down to a quarter unit; with nothing selected they step the weight of the next object drawn (#5797).
|
||||
|
||||
Cmd/Ctrl+Alt+N (`file.newWithoutDialog`, New Without Dialog in the shortcut editor) makes a document with the
|
||||
settings the New Document dialog opens on (the first Recent entry, or the Print category's Letter while Recent is
|
||||
empty), without opening the dialog.
|
||||
@@ -464,6 +467,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-06 | minor | Keyboard shortcuts: Cmd/Ctrl+Alt+. and Cmd/Ctrl+Alt+, (Stroke Weight Up and Down) step each selected stroke's weight, or the next object drawn's (#5797); scores and estimates unchanged |
|
||||
| 2026-11-06 | minor | Properties: Quick Actions has Repeat Options for one selected radial, grid or mirror repeat, opening Object › Repeat › Options with its values (#5181); scores and estimates unchanged |
|
||||
| 2026-11-06 | minor | Control bar: with type selected, a Paragraph: link (the Paragraph panel in a popover) and the Align left, Align center and Align right buttons follow the Character controls; the bar leaves them out from its right end when short of room, after X/Y/W/H and the Align buttons (#5372); scores and estimates unchanged |
|
||||
| 2026-11-04 | minor | Contour add-on (beyond Illustrator): drag away from a selected shape's edge to grow a live copy of it, or toward the inside to shrink it; the copy carries a live Offset Path effect, so it stays editable until Bake (Expand Appearance), a second drag changes the same Contour instead of nesting one, and a floating bar sets the distance and the Sharp, Round or Square corners. `addon.contour.set` is the command behind it. It is a built-in add-on: Object › Plug-ins › Contour, and its own heading in All Tools, with no default shortcut or toolbar slot. A VectorCraft design built on Offset Path; no other application was inspected. Scores and estimates unchanged. |
|
||||
|
||||
Reference in New Issue
Block a user