mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 14:16:56 -06:00
Merge wp/B2
# Conflicts: # crates/doc/src/lib.rs # crates/engine/src/cmd/style.rs # crates/engine/src/lib.rs # crates/ui-egui/src/canvas.rs # crates/ui-egui/src/menus.rs # crates/ui-egui/src/panels/swatches.rs # docs/control-protocol.md
This commit is contained in:
@@ -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",
|
||||
"{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
|
||||
),
|
||||
@@ -461,18 +461,31 @@ fn move_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn copy_from(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let src_id = id_param(p, "source").ok_or_else(|| bad("appearance.copyFrom", "missing `source` id"))?;
|
||||
let src = s.doc()?.doc.node(src_id).cloned().ok_or(EngineError::NoNode(src_id))?;
|
||||
let appearance = match &src.kind {
|
||||
NodeKind::Text(t) => match t.runs.first() {
|
||||
Some(r) => Appearance::basic(r.style.fill.clone(), r.style.stroke.clone(), r.style.stroke_width),
|
||||
None => src.appearance.clone(),
|
||||
},
|
||||
_ => src.appearance.clone(),
|
||||
// The appearance and the box its placed gradients are relative to (type: its first run's
|
||||
// paints, in text space).
|
||||
let (appearance, src_box) = match &src.kind {
|
||||
NodeKind::Text(t) if !t.runs.is_empty() => {
|
||||
let r = &t.runs[0];
|
||||
(Appearance::basic(r.style.fill.clone(), r.style.stroke.clone(), r.style.stroke_width), Some(t.local_bounds()))
|
||||
}
|
||||
_ => (src.appearance.clone(), src.geometric_bounds()),
|
||||
};
|
||||
s.paint.fill = appearance.fill_paint();
|
||||
s.paint.stroke = appearance.stroke_paint();
|
||||
s.remember_paint(&s.paint.fill.clone());
|
||||
if appearance.stroke().is_some() {
|
||||
s.paint.stroke_width = appearance.stroke_width();
|
||||
// `appearance` placed on an object whose box (in its paint space) is `to`.
|
||||
let placed = |to: Option<vectorcraft_geom::Rect>| {
|
||||
let mut ap = appearance.clone();
|
||||
if let (Some(f), Some(t)) = (src_box, to) {
|
||||
ap.rebase_gradients(f, t);
|
||||
}
|
||||
ap
|
||||
};
|
||||
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,
|
||||
@@ -480,24 +493,30 @@ 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 {
|
||||
r.style.fill = appearance.fill_paint();
|
||||
r.style.stroke = appearance.stroke_paint();
|
||||
r.style.stroke_width = appearance.stroke_width();
|
||||
r.style.fill = ap.fill_paint();
|
||||
r.style.stroke = ap.stroke_paint();
|
||||
r.style.stroke_width = ap.stroke_width();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
n.appearance = appearance.clone();
|
||||
n.appearance = placed(n.geometric_bounds());
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,11 +4,16 @@
|
||||
//! Applying a style links the targeted objects to it ([`Node::graphic_style`]). An object stays
|
||||
//! linked while it keeps the style's look; editing its appearance or transparency breaks the link
|
||||
//! ([`in_sync`]), and Redefine Graphic Style updates only the objects still linked.
|
||||
//!
|
||||
//! Styles keep placed gradients relative to the unit box ([`GraphicStyle::unit_box`]): a new or
|
||||
//! redefined style stores them relative to its source's bounds, and each object a style is applied
|
||||
//! to gets them at the same place relative to its own.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Appearance, DEFAULT_GRAPHIC_STYLE, Document, GraphicStyle, Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::Rect;
|
||||
|
||||
use super::*;
|
||||
use crate::EngineError;
|
||||
@@ -20,7 +25,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Apply Graphic Style",
|
||||
["Window", "Graphic Styles"],
|
||||
None,
|
||||
"{name, add?: bool, ids?} give the objects (default: the selection; a group's contents) the style's appearance, and themselves its opacity, blend mode, isolate and knockout, and link them to it; add: true (Alt-click) adds its fills, strokes and effects on top of the existing appearance instead and unlinks",
|
||||
"{name, add?: bool, ids?} give the objects (default: the selection; a group's contents) the style's appearance, and themselves its opacity, blend mode, isolate and knockout, and link them to it (placed gradients land at the same place relative to each object's bounds); add: true (Alt-click) adds its fills, strokes and effects on top of the existing appearance instead and unlinks",
|
||||
has_doc,
|
||||
style_apply
|
||||
),
|
||||
@@ -29,7 +34,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"New Graphic Style",
|
||||
["Window", "Graphic Styles"],
|
||||
None,
|
||||
"{name?, id?} a style from object `id` or the first selected object: its appearance, opacity, blend mode, isolate and knockout (a group without fills or strokes of its own lends its topmost object's, type its characters'); links the object. Names are made unique → {name}",
|
||||
"{name?, id?} a style from object `id` or the first selected object: its appearance, opacity, blend mode, isolate and knockout (a group without fills or strokes of its own lends its topmost object's, type its characters'; placed gradients are kept relative to its bounds); links the object. Names are made unique → {name}",
|
||||
has_doc,
|
||||
style_new
|
||||
),
|
||||
@@ -136,7 +141,7 @@ fn paint_mut(n: &mut Node, top: bool, f: &mut impl FnMut(&mut Node)) {
|
||||
pub(crate) fn in_sync(n: &Node, g: &GraphicStyle) -> bool {
|
||||
let mut leaves = vec![];
|
||||
painted(n, true, &mut leaves);
|
||||
g.transparency_matches(n) && leaves.iter().all(|l| l.appearance == g.appearance)
|
||||
g.transparency_matches(n) && leaves.iter().all(|l| g.appearance_on(l).approx_eq(&l.appearance))
|
||||
}
|
||||
|
||||
/// The style `n` is linked to (and still looks like).
|
||||
@@ -170,18 +175,29 @@ fn links(d: &Document) -> HashMap<u32, Vec<NodeId>> {
|
||||
out
|
||||
}
|
||||
|
||||
/// `ap` with its placed gradients moved from box `from` (an object's bounds) to the unit box.
|
||||
fn unitized(mut ap: Appearance, from: Option<Rect>) -> Appearance {
|
||||
if let Some(b) = from.filter(|_| ap.has_placed_gradient()) {
|
||||
ap.rebase_gradients(b, GraphicStyle::UNIT_BOX);
|
||||
}
|
||||
ap
|
||||
}
|
||||
|
||||
/// The appearance a new or redefined style takes from `n`: its own. A group without fills or
|
||||
/// strokes of its own lends those of its topmost painted object, and type its characters' fill
|
||||
/// and stroke.
|
||||
/// and stroke. Placed gradients come relative to the unit box (from the bounds of the object
|
||||
/// they're on).
|
||||
fn captured(n: &Node) -> Appearance {
|
||||
let mut ap = n.appearance.clone();
|
||||
if !ap.items.is_empty() {
|
||||
return ap;
|
||||
return unitized(ap, n.geometric_bounds());
|
||||
}
|
||||
match &n.kind {
|
||||
NodeKind::Text(t) => {
|
||||
if let Some(r) = t.runs.first() {
|
||||
ap.items = Appearance::basic(r.style.fill.clone(), r.style.stroke.clone(), r.style.stroke_width).items;
|
||||
// Type paints its characters in text space.
|
||||
ap = unitized(ap, Some(t.local_bounds()));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -201,7 +217,7 @@ fn captured(n: &Node) -> Appearance {
|
||||
/// Give `id` style `g` (its painted objects the appearance, itself the transparency) and link it.
|
||||
fn apply_style(d: &mut Document, id: NodeId, g: &GraphicStyle) {
|
||||
if let Some(n) = d.node_mut(id) {
|
||||
paint_mut(n, true, &mut |l| l.appearance = g.appearance.clone());
|
||||
paint_mut(n, true, &mut |l| l.appearance = g.appearance_on(l).into_owned());
|
||||
g.apply_transparency(n);
|
||||
n.graphic_style = Some(g.id);
|
||||
}
|
||||
@@ -251,12 +267,13 @@ fn style_apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
s.edit("Apply Graphic Style", |d, _| {
|
||||
if add {
|
||||
let ap = d.graphic_styles[i].appearance.clone();
|
||||
let g = d.graphic_styles[i].clone();
|
||||
for id in &ids {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
paint_mut(n, true, &mut |l| {
|
||||
l.appearance.items.extend(ap.items.iter().cloned());
|
||||
l.appearance.effects.extend(ap.effects.iter().cloned());
|
||||
let ap = g.appearance_on(l).into_owned();
|
||||
l.appearance.items.extend(ap.items);
|
||||
l.appearance.effects.extend(ap.effects);
|
||||
});
|
||||
n.graphic_style = None;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::{Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::{Affine, Point, Rect, Vec2};
|
||||
|
||||
@@ -26,10 +27,19 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Sample Color",
|
||||
[],
|
||||
None,
|
||||
"{color, stroke?: bool (default: whichever proxy is active), ids?} sample a colour into the active fill/stroke (and the selection)",
|
||||
"{color, stroke?: bool (default: whichever proxy is active), ids?, stop?: index} sample a colour into the active fill/stroke (and the selection); with `stop`, recolour that stop of the gradient behind the proxy instead (paint.editGradient)",
|
||||
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",
|
||||
@@ -161,14 +171,57 @@ 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);
|
||||
let c = p.get("color").ok_or_else(|| bad("paint.sampleColor", "missing color"))?;
|
||||
let mut q = json!({ "color": c });
|
||||
let c = p.get("color").ok_or_else(|| bad(C, "missing color"))?;
|
||||
let (id, mut q) = match p.get("stop") {
|
||||
None | Some(Value::Null) => (if stroke { "paint.setStroke" } else { "paint.setFill" }, json!({ "color": c })),
|
||||
Some(v) => {
|
||||
let i = v.as_u64().ok_or_else(|| bad(C, "`stop` must be a stop index"))? as usize;
|
||||
let color = color_value(c).ok_or_else(|| bad(C, format!("bad color {c}")))?;
|
||||
let (fill, stroke_paint) = s.proxy_paints();
|
||||
let Paint::Gradient(g) = (if stroke { stroke_paint } else { fill }) else {
|
||||
return Err(bad(C, "the paint behind the proxy is not a gradient"));
|
||||
};
|
||||
let mut stops = g.gradient.stops;
|
||||
let n = stops.len();
|
||||
stops.get_mut(i).ok_or_else(|| bad(C, format!("no stop {i} (the gradient has {n})")))?.color = color;
|
||||
("paint.editGradient", json!({ "stops": vectorcraft_tools::params::stops_json(&stops), "stroke": stroke }))
|
||||
}
|
||||
};
|
||||
if let Some(ids) = p.get("ids") {
|
||||
q["ids"] = ids.clone();
|
||||
}
|
||||
let id = if stroke { "paint.setStroke" } else { "paint.setFill" };
|
||||
let spec = find_command(id).ok_or_else(|| EngineError::UnknownCommand(id.into()))?;
|
||||
(spec.run)(s, &q)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user