Files
vectorcraft/crates/engine/src/cmd/newart.rs
T
arhammusheer 5beac4f122 #1244: smaller objects: Node 352 -> 152 bytes, NodeKind 200 -> 80, and a cheaper undo estimate
Rarely set fields move into one optional boxed NodeExtras, and large NodeKind payloads (images,
live shapes, blends, envelopes) are boxed. Every object, and every undo step that copies it,
shrinks: a 50k-path move keeps ~24.5 MB per step instead of ~40 MB. Files stay byte-identical
(Node keeps its flat JSON through a borrowed Serialize and a from-wire Deserialize, no flatten),
and loading is slightly faster. Undo and redo reuse their step's size estimate instead of
walking the document again (8.3 -> 5.6 ms for a 50k-path move).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmMmc1J8XeNLxhRihWv9cc

# Conflicts:
#	crates/engine/src/cmd/graph.rs
#	crates/svg/src/export.rs
2026-10-12 11:02:32 +07:00

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();
}