vectorcraft/crates/engine/src/cmd/newart.rs

240 lines
10 KiB
Rust

//! The new-art template: what the next object drawn looks like.
//!
//! The fill and stroke proxies (`Session::paint`) give new art its paints and stroke weight; the
//! template ([`PaintDefaults::appearance`]) gives it the rest: the Stroke panel's options (set by
//! `stroke.set` whatever is selected), a graphic style clicked with nothing selected (more fills
//! and strokes, effects, opacity and blend mode, and the link to the style) or, with the Appearance
//! panel's New Art Has Basic Appearance off, the whole appearance of the last selection. With the
//! option on (the default) an inherited appearance gives new art its top stroke's options only.
//! `paint.default` resets it.
use std::borrow::Cow;
use serde_json::{Value, json};
use vectorcraft_color::{BlendMode, Paint};
use vectorcraft_doc::{Appearance, AppearanceItem, FillLayer, GraphicStyle, Node, NodeKind, StrokeLayer};
use vectorcraft_tools::PaintDefaults;
use super::gradient::unplaced;
use super::*;
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
"appearance.setNewArtBasic",
"New Art Has Basic Appearance",
["Window", "Appearance"],
None,
"{on?: bool (omitted: toggle)} the Appearance panel option (the preference newArtBasic, on by default): on, new art takes one fill and stroke (with the Stroke panel's options); off, the whole appearance of the last selected object (every fill, stroke and effect, opacity and blend mode) → {on}",
always,
set_basic
),
cmd!(
query "appearance.newArt",
"New Art Appearance",
[],
None,
"{} what the next object drawn gets → {basic: New Art Has Basic Appearance, appearance: {items, effects} (the model's JSON; placed gradients relative to the unit box), opacity: 0..100, blend, graphicStyle: the style it is linked to or null, inherited: taken from the last selection}",
always,
describe
),
]
}
/// `t` reduced to a basic appearance: an empty fill under its top stroke (options kept, without
/// its own transparency or effects).
fn basic_of(t: &Appearance) -> Appearance {
let mut items = vec![AppearanceItem::Fill(FillLayer::new(Paint::None))];
if let Some(st) = t.stroke() {
items.push(AppearanceItem::Stroke(StrokeLayer { opacity: 1.0, blend: BlendMode::Normal, visible: true, effects: vec![], ..st.clone() }));
}
Appearance { items, ..Default::default() }
}
/// Paint `p` on the top fill (`fill`) or stroke of `ap`, creating it unless `p` is None. A paint
/// that only differs in where its gradient is placed keeps its place.
fn repaint(ap: &mut Appearance, fill: bool, p: Paint) {
match ap.paint_at(None, fill) {
Some(cur) if unplaced(cur) == p => {}
Some(_) => {
ap.set_paint_at(None, fill, p);
}
None if p.is_none() => {}
None if fill => ap.set_fill(p),
None => ap.set_stroke(p),
}
}
/// `ap` with every gradient fitted to the art it lands on.
fn unplace_all(mut ap: Appearance) -> Appearance {
for it in &mut ap.items {
let p = it.paint_mut();
*p = unplaced(p);
}
ap
}
/// How a new object looks ([`Session::new_art_look`]): its appearance and transparency and the
/// graphic style it is linked to.
pub(crate) struct NewArt {
pub(crate) appearance: Appearance,
opacity: f32,
blend: BlendMode,
style: Option<String>,
/// From the template: placed gradients are relative to the unit box.
unit_box: bool,
}
impl NewArt {
/// A plain look: `appearance`, full opacity, no link.
pub(crate) fn plain(appearance: Appearance) -> Self {
Self { appearance, opacity: 1.0, blend: BlendMode::Normal, style: None, unit_box: false }
}
/// Give new object `n` (in `d`) this look: its placed gradients land relative to its bounds, and
/// it links to the style (type keeps its own transparency).
pub(crate) fn apply(&self, d: &mut vectorcraft_doc::Document, n: &mut Node) {
n.appearance = self.appearance.clone().into();
if self.unit_box
&& n.appearance.has_placed_gradient()
&& let Some(b) = n.geometric_bounds()
{
n.appearance.rebase_gradients(GraphicStyle::UNIT_BOX, b);
}
if matches!(n.kind, NodeKind::Text(_)) {
return;
}
(n.opacity, n.blend) = (self.opacity, self.blend);
n.extras_mut().graphic_style = self.style.as_deref().and_then(|name| d.graphic_style_index(name)).map(|i| d.graphic_style_id(i));
}
}
impl Session {
/// The template as new art takes it now (an inherited one reduced to basic while New Art Has
/// Basic Appearance is on).
fn template(&self) -> Option<Cow<'_, Appearance>> {
let t = self.paint.appearance.as_ref()?;
Some(if self.basic_only() { Cow::Owned(basic_of(t)) } else { Cow::Borrowed(t) })
}
/// Does an inherited template give new art only its basic fill and stroke?
fn basic_only(&self) -> bool {
self.paint.inherited && self.prefs.new_art_basic
}
/// The look of new art painted with `fill` and `stroke` (weight `width`) on its top fill and
/// stroke: the template's, else one plain fill and stroke.
pub(crate) fn new_art_look(&self, fill: Paint, stroke: Paint, width: f64) -> NewArt {
let Some(t) = self.template() else { return NewArt::plain(Appearance::basic(fill, stroke, width)) };
let mut appearance = t.into_owned();
repaint(&mut appearance, true, fill);
repaint(&mut appearance, false, stroke);
if let Some(st) = appearance.stroke_mut() {
st.width = width;
}
let (opacity, blend, style) = self.new_art_transparency();
NewArt { appearance, opacity, blend, style: style.map(str::to_string), unit_box: true }
}
/// The appearance of the next object drawn (the Appearance panel shows it with nothing
/// selected). Placed gradients are relative to the unit box.
pub fn new_art(&self) -> Appearance {
self.new_art_look(self.paint.fill.clone(), self.paint.stroke.clone(), self.paint.stroke_width).appearance
}
/// The opacity, blend mode and graphic style (name) of the next object drawn.
pub fn new_art_transparency(&self) -> (f32, BlendMode, Option<&str>) {
if self.basic_only() { (1.0, BlendMode::Normal, None) } else { (self.paint.opacity, self.paint.blend, self.paint.style.as_deref()) }
}
/// The stroke of the next object drawn (the Stroke panel shows it with nothing selected).
pub fn new_art_stroke(&self) -> StrokeLayer {
let template = self.paint.appearance.as_ref().and_then(Appearance::stroke);
let mut st = template.cloned().unwrap_or_else(|| StrokeLayer::new(Paint::None, 0.0));
(st.paint, st.width) = (self.paint.stroke.clone(), self.paint.stroke_width);
st
}
/// The template for an edit (`stroke.set`): it becomes the user's own (an inherited one as new
/// art takes it now), with a stroke to edit.
fn new_art_template_mut(&mut self) -> &mut Appearance {
let own = self.template().map(Cow::into_owned);
if self.basic_only() {
(self.paint.opacity, self.paint.blend, self.paint.style) = (1.0, BlendMode::Normal, None);
}
self.paint.inherited = false;
let t = self.paint.appearance.insert(own.unwrap_or_else(|| Appearance::basic(Paint::None, Paint::None, 0.0)));
if t.stroke().is_none() {
t.set_stroke(Paint::None);
}
t
}
/// The template's top stroke for an edit (see [`Session::new_art_template_mut`], which always
/// gives it one).
pub(crate) fn new_art_stroke_mut(&mut self) -> Option<&mut StrokeLayer> {
self.new_art_template_mut().stroke_mut()
}
/// A graphic style clicked with nothing selected: new art takes it (`add`: its fills, strokes
/// and effects on top of the template's, unlinked).
pub(crate) fn new_art_style(&mut self, g: &GraphicStyle, add: bool) {
let ap = if g.unit_box { g.appearance.clone() } else { unplace_all(g.appearance.clone()) };
if add {
let t = self.new_art_template_mut();
t.items.extend(ap.items);
t.effects.extend(ap.effects);
self.paint.style = None;
return;
}
self.take_new_art(ap, g.opacity, g.blend, Some(g.name.clone()), false);
}
/// Make `ap` (placed gradients relative to the unit box) and this transparency the template,
/// and its top fill, stroke and weight the proxies.
fn take_new_art(&mut self, ap: Appearance, opacity: f32, blend: BlendMode, style: Option<String>, inherited: bool) {
let p = &mut self.paint;
(p.fill, p.stroke) = (unplaced(&ap.fill_paint()), unplaced(&ap.stroke_paint()));
if let Some(st) = ap.stroke() {
p.stroke_width = st.width;
}
(p.appearance, p.opacity, p.blend, p.style, p.inherited) = (Some(ap), opacity, blend, style, inherited);
}
/// After each command: with New Art Has Basic Appearance off, new art takes the appearance of
/// the first selected object (so the last selection's once nothing is selected).
pub(crate) fn inherit_new_art(&mut self) {
if self.prefs.new_art_basic {
return;
}
let Some(st) = self.active() else { return };
let Some(n) = st.selection.subjects().first().and_then(|id| st.doc.node(*id)) else { return };
let style = super::style::linked_style(&st.doc, n).map(|g| g.name.clone());
let (ap, opacity, blend) = (super::style::captured(n), n.opacity, n.blend);
self.take_new_art(ap, opacity, blend, style, true);
}
}
fn set_basic(s: &mut Session, p: &Value) -> Result<Value> {
let on = p.get("on").and_then(Value::as_bool).unwrap_or(!s.prefs.new_art_basic);
s.prefs.new_art_basic = on;
Ok(json!({ "on": on }))
}
fn describe(s: &mut Session, _: &Value) -> Result<Value> {
let (opacity, blend, style) = s.new_art_transparency();
Ok(json!({
"basic": s.prefs.new_art_basic,
"appearance": s.new_art(),
"opacity": (opacity * 100.0).round(),
"blend": blend.label(),
"graphicStyle": style,
"inherited": s.paint.inherited,
}))
}
/// The defaults for new art reset (`paint.default`): white fill, 1 pt black stroke, no template.
pub(crate) fn reset(s: &mut Session) {
s.paint = PaintDefaults::default();
}