Files
vectorcraft/crates/render/src/tests_adjust.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

122 lines
4.4 KiB
Rust

//! Live colour adjustments (Effect → Color Adjustments) as drawn: on paths, type, embedded images,
//! groups and symbol instances, alone and with other effects.
use std::sync::Arc;
use serde_json::{Value, json};
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::text::{CharStyle, TextObject};
use vectorcraft_doc::{Appearance, Effect, ImageBlob, ImageObject, Node, NodeKind, Symbol};
use vectorcraft_geom::{Point, shapes};
use super::*;
fn fx(id: &str, params: Value) -> Effect {
Effect { id: id.into(), params, visible: true }
}
/// Hue +120°: red becomes green, green blue.
fn hue() -> Effect {
fx("adjust.hueSaturation", json!({"hue": 120}))
}
fn doc_with(nodes: Vec<Node>) -> Document {
let mut d = Document::new(100.0, 100.0);
let l = d.layers[0].id;
for mut n in nodes {
n.id = d.alloc_id();
d.insert(Some(l), 0, n).unwrap();
}
d
}
fn render(d: &Document) -> Rendered {
let mut r = Renderer::new();
r.threads = 0;
r.render(d, 100, 100, Affine::IDENTITY, &RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() })
}
fn red_square(x: f64, y: f64) -> Node {
Node::path(
NodeId(0),
shapes::rectangle(Rect::new(x, y, x + 30.0, y + 30.0)),
Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0),
)
}
fn red_image() -> (String, ImageBlob) {
let img = image::RgbaImage::from_pixel(30, 30, image::Rgba([255, 0, 0, 255]));
let mut png = vec![];
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
("red".into(), ImageBlob::new("image/png", png))
}
#[test]
fn a_path_takes_its_adjusted_colour_and_keeps_its_other_effects() {
let mut n = red_square(10.0, 10.0);
n.appearance.effects = vec![hue(), fx("distort.transform", json!({"moveH": 40}))];
let img = render(&doc_with(vec![n]));
assert_eq!(img.pixel(65, 25), [0, 255, 0, 255], "green, and moved by the geometry effect");
assert_eq!(img.pixel(25, 25), [255, 255, 255, 255]);
// Hidden, it changes nothing.
let mut n = red_square(10.0, 10.0);
n.appearance.effects = vec![Effect { visible: false, ..hue() }];
assert_eq!(render(&doc_with(vec![n])).pixel(25, 25), [255, 0, 0, 255]);
}
#[test]
fn a_group_recolours_its_members_and_type() {
let style = CharStyle { size: 60.0, fill: Paint::solid(Color::rgb(1.0, 0.0, 0.0)), ..Default::default() };
let text = Node::new(NodeId(0), NodeKind::Text(Box::new(TextObject::point(Point::new(50.0, 90.0), "I", style))));
let mut g = Node::group(NodeId(0), vec![Arc::new(red_square(5.0, 5.0)), Arc::new(text)]);
g.appearance.effects.push(hue());
let d = doc_with(vec![g]);
let img = render(&d);
assert_eq!(img.pixel(20, 20), [0, 255, 0, 255]);
let inked: Vec<[u8; 4]> =
(50..70).flat_map(|x| (50..90).map(move |y| (x, y))).map(|(x, y)| img.pixel(x, y)).filter(|p| p[0] < 128 || p[2] < 128).collect();
assert!(inked.contains(&[0, 255, 0, 255]), "the type turns green too");
assert!(inked.iter().all(|p| p[1] == 255 && p[0] == p[2]), "no red left: {:?}", inked.iter().find(|p| p[1] != 255 || p[0] != p[2]));
}
#[test]
fn an_embedded_image_is_recoloured_and_the_original_pixels_stay() {
let mut d = doc_with(vec![]);
let (key, blob) = red_image();
d.images.insert(key.clone(), blob.clone());
let mut n = Node::new(
NodeId(0),
NodeKind::Image(Box::new(ImageObject {
key,
width: 30,
height: 30,
xf: Affine::translate((20.0, 20.0)),
link: None,
placement: Default::default(),
})),
);
n.appearance.effects.push(hue());
d = {
let l = d.layers[0].id;
n.id = d.alloc_id();
d.insert(Some(l), 0, n).unwrap();
d
};
let img = render(&d);
assert_eq!(img.pixel(35, 35), [0, 255, 0, 255]);
assert_eq!(d.images.get("red"), Some(&blob), "non-destructive");
}
#[test]
fn a_symbol_instance_recolours_its_art() {
let mut d = Document::new(100.0, 100.0);
let art = red_square(-15.0, -15.0);
d.symbols.push(Symbol { name: "Red".into(), art: Arc::new(art) });
let mut n = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Red".into(), xf: Affine::translate((50.0, 50.0)) });
n.appearance.effects.push(hue());
let l = d.layers[0].id;
n.id = d.alloc_id();
d.insert(Some(l), 0, n).unwrap();
assert_eq!(render(&d).pixel(50, 50), [0, 255, 0, 255]);
}