mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 05:07:51 -06:00
M3.35: gradient handles snap and honour Constrain Angle; double-clicking a tool button opens its options
Gradient tool handles (a drawn vector's start and end, the end handle and the moved start) snap to anchors, edges
and smart guides, with the guide overlays; Shift constrains to 45° steps from the Constrain Angle preference
(`ToolContext::constrain_angle`, `constrain_angle_from` in geom). Double-clicking a toolbar button runs the new UI
command `tool.options {tool}`: the Gradient tool opens the Gradient panel, the Eyedropper opens Eyedropper Options
(`eyedropper.setOptions {appearance?, transparency?}`, which `appearance.copyFrom` honours and its params override).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
93640efb36
commit
0358c4df95
@@ -83,7 +83,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Eyedropper",
|
||||
[],
|
||||
None,
|
||||
"{source: id, ids?} copy fill, stroke, weight, opacity and blend from `source` to ids (default: selection) and to the paint defaults; placed gradients land at the same place relative to each target's bounds (defaults: fitted to new art)",
|
||||
"{source: id, ids?, appearance?: bool, transparency?: bool (default: eyedropper.setOptions)} copy fill, stroke, weight and effects (appearance) and opacity and blend (transparency) from `source` to ids (default: selection), the appearance also to the paint defaults; placed gradients land at the same place relative to each target's bounds (defaults: fitted to new art)",
|
||||
has_doc,
|
||||
copy_from
|
||||
),
|
||||
@@ -478,11 +478,14 @@ fn copy_from(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
ap
|
||||
};
|
||||
s.paint.fill = super::gradient::unplaced(&appearance.fill_paint());
|
||||
s.paint.stroke = super::gradient::unplaced(&appearance.stroke_paint());
|
||||
s.remember_paint(&s.paint.fill.clone());
|
||||
if appearance.stroke().is_some() {
|
||||
s.paint.stroke_width = appearance.stroke_width();
|
||||
let opts = s.eyedropper.with(p);
|
||||
if opts.appearance {
|
||||
s.paint.fill = super::gradient::unplaced(&appearance.fill_paint());
|
||||
s.paint.stroke = super::gradient::unplaced(&appearance.stroke_paint());
|
||||
s.remember_paint(&s.paint.fill.clone());
|
||||
if appearance.stroke().is_some() {
|
||||
s.paint.stroke_width = appearance.stroke_width();
|
||||
}
|
||||
}
|
||||
let ids = match ids_param(p, "ids") {
|
||||
Some(v) => v,
|
||||
@@ -490,15 +493,20 @@ fn copy_from(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let mut targets = leaf_targets(s, &ids)?;
|
||||
targets.retain(|id| *id != src_id);
|
||||
if targets.is_empty() {
|
||||
if targets.is_empty() || !(opts.appearance || opts.transparency) {
|
||||
return Ok(json!({ "ids": [] }));
|
||||
}
|
||||
let (opacity, blend) = (src.opacity, src.blend);
|
||||
s.edit("Eyedropper", |d, _| {
|
||||
for id in &targets {
|
||||
let Some(n) = d.node_mut(*id) else { continue };
|
||||
n.opacity = opacity;
|
||||
n.blend = blend;
|
||||
if opts.transparency {
|
||||
n.opacity = opacity;
|
||||
n.blend = blend;
|
||||
}
|
||||
if !opts.appearance {
|
||||
continue;
|
||||
}
|
||||
if let NodeKind::Text(t) = &mut n.kind {
|
||||
let ap = placed(Some(t.local_bounds()));
|
||||
for r in &mut t.runs {
|
||||
|
||||
@@ -346,3 +346,6 @@ pub(crate) fn parse_range(s: &str, count: usize) -> std::result::Result<Vec<usiz
|
||||
|
||||
/// The Transparency panel's state ([`Session::transparency_info`]).
|
||||
pub use opacitymask::TransparencyInfo;
|
||||
|
||||
/// What the Eyedropper copies (`Session::eyedropper`).
|
||||
pub use xform::EyedropperOptions;
|
||||
|
||||
@@ -31,6 +31,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_doc,
|
||||
sample_color
|
||||
),
|
||||
cmd!(
|
||||
"eyedropper.setOptions",
|
||||
"Eyedropper Options",
|
||||
[],
|
||||
None,
|
||||
"{appearance?: bool (fill, stroke and effects), transparency?: bool (opacity and blend mode)} what the Eyedropper copies (appearance.copyFrom); {} reads them → {appearance, transparency}",
|
||||
always,
|
||||
eyedropper_options
|
||||
),
|
||||
cmd!(
|
||||
"artboard.move",
|
||||
"Move Artboard",
|
||||
@@ -162,6 +171,35 @@ fn distort(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
// ---------- eyedropper ----------
|
||||
|
||||
/// What the Eyedropper copies from the object it clicks (`appearance.copyFrom`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EyedropperOptions {
|
||||
/// The fill, stroke and effects (the appearance stack; type: its run paints).
|
||||
pub appearance: bool,
|
||||
/// The opacity and blend mode.
|
||||
pub transparency: bool,
|
||||
}
|
||||
|
||||
impl Default for EyedropperOptions {
|
||||
fn default() -> Self {
|
||||
Self { appearance: true, transparency: true }
|
||||
}
|
||||
}
|
||||
|
||||
impl EyedropperOptions {
|
||||
/// These options with any `appearance` / `transparency` booleans in `p` applied.
|
||||
pub(crate) fn with(mut self, p: &Value) -> Self {
|
||||
self.appearance = bool_or(p, "appearance", self.appearance);
|
||||
self.transparency = bool_or(p, "transparency", self.transparency);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn eyedropper_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.eyedropper = s.eyedropper.with(p);
|
||||
Ok(serde_json::to_value(s.eyedropper).unwrap_or_default())
|
||||
}
|
||||
|
||||
fn sample_color(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "paint.sampleColor";
|
||||
let stroke = p.get("stroke").and_then(Value::as_bool).unwrap_or(!s.fill_active);
|
||||
|
||||
Reference in New Issue
Block a user