mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 11:47:21 -06:00
M3.44: graphic styles keep opacity and blend, capture groups and type, add on Alt-click
Styles now store the object's opacity, blend mode, isolate and knockout and apply them. New Graphic Style from a group without its own paint captures its topmost object's appearance, and from type its characters' fill and stroke. Names are made unique. graphicStyle.apply takes ids and add (Alt-click in the panel adds the style on top of the existing appearance). graphicStyle.list reports every style. Applying links objects to a stable style id (Node.graphic_style), which the management commands build on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6bd27a05da
commit
eb83efdb19
+81
-12
@@ -254,11 +254,49 @@ impl Default for GridPrefs {
|
||||
}
|
||||
}
|
||||
|
||||
/// A named graphic style (Graphic Styles panel).
|
||||
/// A named graphic style (Graphic Styles panel): an appearance plus the object's transparency.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GraphicStyle {
|
||||
pub name: String,
|
||||
pub appearance: Appearance,
|
||||
/// Stable id linked objects refer to ([`Node::graphic_style`]); 0 until first needed (styles
|
||||
/// from older files get one when an object is first linked to them).
|
||||
#[serde(default, skip_serializing_if = "skip::is_default")]
|
||||
pub id: u32,
|
||||
#[serde(default = "one", skip_serializing_if = "skip::is_one")]
|
||||
pub opacity: f32,
|
||||
#[serde(default, skip_serializing_if = "skip::is_default")]
|
||||
pub blend: vectorcraft_color::BlendMode,
|
||||
#[serde(default, skip_serializing_if = "skip::is_default")]
|
||||
pub isolate: bool,
|
||||
#[serde(default, skip_serializing_if = "skip::is_default")]
|
||||
pub knockout: bool,
|
||||
}
|
||||
|
||||
fn one() -> f32 {
|
||||
1.0
|
||||
}
|
||||
|
||||
/// The name of the style new documents list first (Sort by Name keeps it first).
|
||||
pub const DEFAULT_GRAPHIC_STYLE: &str = "Default Graphic Style";
|
||||
|
||||
impl GraphicStyle {
|
||||
/// A style with `appearance` and default transparency (no id yet).
|
||||
pub fn new(name: impl Into<String>, appearance: Appearance) -> Self {
|
||||
Self { name: name.into(), appearance, id: 0, opacity: 1.0, blend: Default::default(), isolate: false, knockout: false }
|
||||
}
|
||||
/// A style capturing `n`'s transparency and `appearance`.
|
||||
pub fn of(name: impl Into<String>, appearance: Appearance, n: &Node) -> Self {
|
||||
Self { opacity: n.opacity, blend: n.blend, isolate: n.isolate, knockout: n.knockout, ..Self::new(name, appearance) }
|
||||
}
|
||||
/// Give `n` this style's transparency.
|
||||
pub fn apply_transparency(&self, n: &mut Node) {
|
||||
(n.opacity, n.blend, n.isolate, n.knockout) = (self.opacity, self.blend, self.isolate, self.knockout);
|
||||
}
|
||||
/// Does `n` have this style's transparency?
|
||||
pub fn transparency_matches(&self, n: &Node) -> bool {
|
||||
(n.opacity, n.blend, n.isolate, n.knockout) == (self.opacity, self.blend, self.isolate, self.knockout)
|
||||
}
|
||||
}
|
||||
|
||||
/// A symbol definition.
|
||||
@@ -633,20 +671,51 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
/// Graphic styles.
|
||||
impl Document {
|
||||
/// Index of the graphic style named `name`.
|
||||
pub fn graphic_style_index(&self, name: &str) -> Option<usize> {
|
||||
self.graphic_styles.iter().position(|g| g.name == name)
|
||||
}
|
||||
/// The graphic style named `name`.
|
||||
pub fn graphic_style(&self, name: &str) -> Option<&GraphicStyle> {
|
||||
self.graphic_styles.iter().find(|g| g.name == name)
|
||||
}
|
||||
/// The graphic style with id `id` (0 never matches).
|
||||
pub fn graphic_style_by_id(&self, id: u32) -> Option<&GraphicStyle> {
|
||||
self.graphic_styles.iter().find(|g| g.id != 0 && g.id == id)
|
||||
}
|
||||
/// An id no graphic style has.
|
||||
pub fn next_graphic_style_id(&self) -> u32 {
|
||||
self.graphic_styles.iter().map(|g| g.id).max().unwrap_or(0) + 1
|
||||
}
|
||||
/// The id of graphic style `index`, assigning one if it has none yet.
|
||||
pub fn graphic_style_id(&mut self, index: usize) -> u32 {
|
||||
if self.graphic_styles[index].id == 0 {
|
||||
self.graphic_styles[index].id = self.next_graphic_style_id();
|
||||
}
|
||||
self.graphic_styles[index].id
|
||||
}
|
||||
/// The default name of a new graphic style: the first free "Graphic Style N", N counting on
|
||||
/// from the number of styles.
|
||||
pub fn new_graphic_style_name(&self) -> String {
|
||||
(self.graphic_styles.len() + 1..).map(|i| format!("Graphic Style {i}")).find(|n| self.graphic_style(n).is_none()).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_graphic_styles() -> Vec<GraphicStyle> {
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
vec![
|
||||
GraphicStyle { name: "Default Graphic Style".into(), appearance: Appearance::default_art() },
|
||||
GraphicStyle { name: "Black Outline".into(), appearance: Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0) },
|
||||
GraphicStyle {
|
||||
name: "Heavy Ink".into(),
|
||||
appearance: Appearance::basic(Paint::solid(Color::from_hex("#1b1464").unwrap()), Paint::solid(Color::BLACK), 4.0),
|
||||
},
|
||||
GraphicStyle {
|
||||
name: "Sunshine".into(),
|
||||
appearance: Appearance::basic(Paint::solid(Color::from_hex("#fbb03b").unwrap()), Paint::solid(Color::from_hex("#f15a24").unwrap()), 2.0),
|
||||
},
|
||||
let solid = |hex| Paint::solid(Color::from_hex(hex).unwrap());
|
||||
[
|
||||
GraphicStyle::new(DEFAULT_GRAPHIC_STYLE, Appearance::default_art()),
|
||||
GraphicStyle::new("Black Outline", Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0)),
|
||||
GraphicStyle::new("Heavy Ink", Appearance::basic(solid("#1b1464"), Paint::solid(Color::BLACK), 4.0)),
|
||||
GraphicStyle::new("Sunshine", Appearance::basic(solid("#fbb03b"), solid("#f15a24"), 2.0)),
|
||||
]
|
||||
.into_iter()
|
||||
.zip(1..)
|
||||
.map(|(g, id)| GraphicStyle { id, ..g })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -270,6 +270,10 @@ pub struct Node {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub graph: Option<Box<crate::graph::GraphSpec>>,
|
||||
pub kind: NodeKind,
|
||||
/// The [`crate::GraphicStyle::id`] last applied to this object. It stays linked while it keeps
|
||||
/// that style's look: editing its appearance or transparency breaks the link.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub graphic_style: Option<u32>,
|
||||
}
|
||||
|
||||
/// Opacity mask: the luminance of the mask art sets the object's opacity (white = opaque).
|
||||
@@ -314,6 +318,7 @@ impl Node {
|
||||
wrap: None,
|
||||
graph: None,
|
||||
kind,
|
||||
graphic_style: None,
|
||||
}
|
||||
}
|
||||
pub fn path(id: NodeId, path: PathData, appearance: Appearance) -> Self {
|
||||
|
||||
Reference in New Issue
Block a user