Files
figcraft/crates/engine/src/default_props_tests.rs
T
Filip StrajnarandClaude Opus 5.5 20f1219fbc M1: Set default properties gives new shapes and text the layer's properties (#2028)
Edit ▸ Set default properties put the selected layer's properties into the Paste properties
clipboard and did nothing else: a rectangle drawn afterwards was still the default grey, and what
Copy properties had copied was replaced.

The defaults are now kept apart (`Session::default_props`: one set for shapes, set from any layer
but text, and one for text). Rectangles, ellipses, polygons and stars drawn or clicked with their
tools take the shape defaults: fills, strokes and their settings, effects, opacity, blend and
corners (only where the shape has corners), through `apply_copied_props`, the rules Paste
properties already used (now shared). New text from the Text tool takes a text layer's fills and
font and is laid out at that size. Frames, sections, slices, lines, arrows, vectors and layers made
by commands keep their own look; the help center is silent on those. `edit.setDefaultProperties`
answers `{for: shapes|text, properties}`.

Tests: `default_props_tests::new_shapes_drawn_with_the_tools_take_the_default_properties`,
`setting_defaults_leaves_the_copied_properties_alone`,
`new_text_takes_a_text_layers_defaults_and_shapes_keep_theirs`. Docs: ui-parity.md, ROADMAP.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2LxvJ6MuFmiLA6Zqm
2026-11-06 12:55:21 +01:00

118 lines
6.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Edit ▸ Set default properties (#2028): new rectangles, ellipses, polygons and stars drawn with
//! the tools take the properties of the layer it was set from, and new text takes a text layer's;
//! it no longer only copied them for Paste properties.
use figcraft_color::Color;
use figcraft_doc::{Node, NodeId, NodeKind};
use serde_json::{Value, json};
use crate::Session;
use crate::params::id_of;
use crate::tools::{PointerEvent, PointerKind, ToolId};
/// Run `cmd`, failing the test on an error.
fn ex(s: &mut Session, cmd: &str, p: Value) -> Value {
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd}: {e}"))
}
/// The layer `id` as it is now.
fn node(s: &Session, id: NodeId) -> Node {
s.doc().unwrap().node(id).unwrap().clone()
}
/// Draw with `tool` from `a` to `b` (a click when they're equal); the new layer.
fn draw(s: &mut Session, tool: ToolId, a: (f64, f64), b: (f64, f64)) -> NodeId {
s.set_tool(tool);
s.pointer(&PointerEvent::at(PointerKind::Down, a.0, a.1, 1.0)).unwrap();
if a != b {
s.pointer(&PointerEvent::at(PointerKind::Move, (a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0, 1.0)).unwrap();
s.pointer(&PointerEvent::at(PointerKind::Move, b.0, b.1, 1.0)).unwrap();
}
s.pointer(&PointerEvent::at(PointerKind::Up, b.0, b.1, 1.0)).unwrap();
*s.selection().first().unwrap()
}
/// The first solid fill's colour of layer `n`.
fn fill(n: &Node) -> Option<Color> {
n.fills.first().and_then(|p| p.solid_color())
}
/// A red, half-transparent rectangle with a 4 pt blue stroke and 12 pt corners.
fn styled(s: &mut Session) -> NodeId {
let id = id_of(&ex(s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"]).unwrap();
ex(s, "node.set", json!({"id": id, "props": {"fill": "#FF0000", "stroke": "#0000FF", "strokeWeight": 4, "opacity": 0.5, "cornerRadius": 12}}));
id
}
/// Rectangles, ellipses, polygons and stars drawn or clicked with their tools take the shape
/// defaults (corners only where a shape has them, its own drawn size); frames, lines and layers made
/// by commands keep their own look; undo takes a drawn shape away whole.
#[test]
fn new_shapes_drawn_with_the_tools_take_the_default_properties() {
let mut s = Session::new();
let source = styled(&mut s);
let set = ex(&mut s, "edit.setDefaultProperties", json!({"id": source}));
assert_eq!(set["for"], "shapes");
let red = Some(Color::from_hex("#FF0000").unwrap());
// Dragged and clicked (a click places a 100×100 shape) alike.
for (tool, a, b) in
[(ToolId::Rectangle, (200.0, 0.0), (300.0, 80.0)), (ToolId::Ellipse, (400.0, 0.0), (400.0, 0.0)), (ToolId::Star, (600.0, 0.0), (660.0, 60.0))]
{
let id = draw(&mut s, tool, a, b);
let n = node(&s, id);
assert_eq!(fill(&n), red, "{tool:?}");
assert_eq!((n.stroke.weight, n.opacity), (4.0, 0.5), "{tool:?}");
assert_eq!(n.strokes.first().and_then(|p| p.solid_color()), Some(Color::from_hex("#0000FF").unwrap()), "{tool:?}");
}
let rect = draw(&mut s, ToolId::Rectangle, (0.0, 200.0), (80.0, 260.0));
assert_eq!(node(&s, rect).corners.radii, [12.0; 4], "corners go to layers with corners");
assert_eq!((node(&s, rect).width, node(&s, rect).height), (80.0, 60.0), "the drawn size, not the source's");
// Frames and lines keep their own look; a shape made by a command too.
let frame = draw(&mut s, ToolId::Frame, (0.0, 400.0), (100.0, 500.0));
assert_ne!(fill(&node(&s, frame)), red);
let line = draw(&mut s, ToolId::Line, (0.0, 600.0), (100.0, 600.0));
assert_ne!(node(&s, line).stroke.weight, 4.0);
let made = id_of(&ex(&mut s, "shape.rectangle", json!({"x": 0, "y": 700, "width": 10, "height": 10}))["id"]).unwrap();
assert_ne!(fill(&node(&s, made)), red);
// Undo takes a drawn shape away whole, defaults included.
let drawn = draw(&mut s, ToolId::Polygon, (800.0, 0.0), (860.0, 60.0));
ex(&mut s, "edit.undo", json!({}));
assert!(s.doc().unwrap().node(drawn).is_none());
}
/// Setting defaults no longer overwrites what Copy properties copied.
#[test]
fn setting_defaults_leaves_the_copied_properties_alone() {
let mut s = Session::new();
let copied = id_of(&ex(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"]).unwrap();
ex(&mut s, "node.set", json!({"id": copied, "props": {"fill": "#00FF00"}}));
ex(&mut s, "edit.copyProperties", json!({"id": copied}));
let source = styled(&mut s);
ex(&mut s, "edit.setDefaultProperties", json!({"id": source}));
let target = id_of(&ex(&mut s, "shape.ellipse", json!({"x": 0, "y": 100, "width": 50, "height": 50}))["id"]).unwrap();
ex(&mut s, "edit.pasteProperties", json!({"ids": [target]}));
assert_eq!(fill(&node(&s, target)), Some(Color::from_hex("#00FF00").unwrap()), "Paste properties pastes what was copied");
}
/// A text layer sets the defaults of new text (its font and fills, laid out at that size), and the
/// shapes' defaults stay as they were.
#[test]
fn new_text_takes_a_text_layers_defaults_and_shapes_keep_theirs() {
let mut s = Session::new();
let shape = styled(&mut s);
ex(&mut s, "edit.setDefaultProperties", json!({"id": shape}));
let text = id_of(&ex(&mut s, "text.create", json!({"text": "Title", "x": 0, "y": 200}))["id"]).unwrap();
ex(&mut s, "node.set", json!({"id": text, "props": {"fontSize": 32, "fill": "#008000"}}));
let set = ex(&mut s, "edit.setDefaultProperties", json!({"id": text}));
assert_eq!(set["for"], "text");
let id = draw(&mut s, ToolId::Text, (300.0, 300.0), (300.0, 300.0));
let n = node(&s, id);
let NodeKind::Text(t) = &n.kind else { panic!("a text layer") };
assert_eq!(t.style.font_size, 32.0);
assert_eq!(fill(&n), Some(Color::from_hex("#008000").unwrap()));
assert!(n.height > 30.0, "laid out at the default size: {}", n.height);
// The shapes' defaults are still the rectangle's.
let rect = draw(&mut s, ToolId::Rectangle, (0.0, 500.0), (80.0, 560.0));
assert_eq!(fill(&node(&s, rect)), Some(Color::from_hex("#FF0000").unwrap()));
}