mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 01:25:24 +00:00
Merge main into #2163
This commit is contained in:
@@ -496,7 +496,9 @@ fn collect_flatten_parts(
|
||||
}
|
||||
let part = match &node.kind {
|
||||
NodeKind::Vector { network } => {
|
||||
let mut part = network.clone();
|
||||
// Rounded points bake into the path (the arc's end points joined by curves), drawn the way
|
||||
// the layer draws them, so the shape looks the same and the box fits the real geometry.
|
||||
let mut part = figcraft_doc::geometry::drawn_network(network, node.corners.smoothing).into_owned();
|
||||
if !part.regions.is_empty() && part.regions.iter().all(|region| region.fills.is_none() && region.paint_space.is_none()) {
|
||||
let winding = if part.regions.iter().any(|region| region.winding == WindingRule::EvenOdd) {
|
||||
WindingRule::EvenOdd
|
||||
|
||||
@@ -881,3 +881,68 @@ fn vector_points_take_the_requested_size() {
|
||||
let e = s.execute("vector.create", &json!({"points": points, "width": 16, "height": -1})).unwrap_err().to_string();
|
||||
assert!(e.contains("positive"), "{e}");
|
||||
}
|
||||
|
||||
/// A vector from `points` with a corner radius per point (`radii`), via `vector.create`.
|
||||
fn rounded_vector(s: &mut Session, points: &[(f64, f64)], radii: &[f64], closed: bool) -> NodeId {
|
||||
let pts: Vec<Value> = points.iter().map(|p| json!([p.0, p.1])).collect();
|
||||
let id = id_of(&ex(s, "vector.create", json!({"points": pts, "closed": closed}))["id"]).unwrap();
|
||||
if let NodeKind::Vector { network } = &mut s.doc_mut().unwrap().get_mut(id).unwrap().kind {
|
||||
for (v, r) in network.vertices.iter_mut().zip(radii) {
|
||||
v.radius = *r;
|
||||
}
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
/// #2195: Flatten bakes a vector's point radii into its path: the rounded vertex becomes the
|
||||
/// arc's end points joined by curves, the layer stays a vector, the box fits the real geometry.
|
||||
#[test]
|
||||
fn flatten_bakes_a_vectors_point_radius() {
|
||||
// An open V with the apex rounded.
|
||||
let mut s = Session::new();
|
||||
let v = rounded_vector(&mut s, &[(0.0, 0.0), (184.0, 245.0), (368.0, 0.0)], &[0.0, 60.0, 0.0], false);
|
||||
let before = net(&s, v);
|
||||
let drawn = before.rounded().unwrap().stroke_path().perimeter(1e-4);
|
||||
let h_before = s.doc().unwrap().node(v).unwrap().height;
|
||||
let out = ex(&mut s, "vector.flatten", json!({"ids": [v]}));
|
||||
let f = id_of(&out["id"]).unwrap();
|
||||
let after = net(&s, f);
|
||||
assert_eq!(after.vertices.len(), 4, "the apex became two points: 3 + 1");
|
||||
assert!(after.vertices.iter().all(|p| p.radius == 0.0), "no radius left");
|
||||
let len = after.stroke_path().perimeter(1e-4);
|
||||
assert!((len - drawn).abs() < 0.5, "the outline is the same: {len} vs {drawn}");
|
||||
let h = s.doc().unwrap().node(f).unwrap().height;
|
||||
let bb = after.bounds().unwrap();
|
||||
assert!(h < h_before && (h - 245.0).abs() > 1.0, "the box shrinks from 245: {h}");
|
||||
assert!((h - bb.height()).abs() < 1e-6 && bb.y0.abs() < 1e-6, "the box is the baked path's bounds: {h} vs {bb:?}");
|
||||
println!("open V: points {} -> {}, height {h_before} -> {h}", before.vertices.len(), after.vertices.len());
|
||||
s.undo().unwrap();
|
||||
assert!(s.doc().unwrap().node(v).is_some() && s.doc().unwrap().node(f).is_none(), "one undo step");
|
||||
assert_eq!(net(&s, v).vertices[1].radius, 60.0);
|
||||
|
||||
// A closed triangle rounded at every point.
|
||||
let mut s = Session::new();
|
||||
let v = rounded_vector(&mut s, &[(0.0, 0.0), (368.0, 0.0), (184.0, 245.0)], &[40.0; 3], true);
|
||||
let drawn = net(&s, v).rounded().unwrap();
|
||||
let (len, area) = (drawn.stroke_path().perimeter(1e-4), drawn.fill_path().area().abs());
|
||||
let f = id_of(&ex(&mut s, "vector.flatten", json!({"ids": [v]}))["id"]).unwrap();
|
||||
let after = net(&s, f);
|
||||
assert_eq!(after.vertices.len(), 6, "each corner became two points");
|
||||
assert!(after.vertices.iter().all(|p| p.radius == 0.0));
|
||||
assert_eq!(after.regions.len(), 1, "still one filled region");
|
||||
assert!((after.stroke_path().perimeter(1e-4) - len).abs() < 0.5);
|
||||
assert!((after.fill_path().area().abs() - area).abs() < 0.5, "same fill area");
|
||||
let n = s.doc().unwrap().node(f).unwrap().clone();
|
||||
let bb = after.bounds().unwrap();
|
||||
assert!(n.height < 245.0 && (n.height - bb.height()).abs() < 1e-6 && (n.width - bb.width()).abs() < 1e-6);
|
||||
println!("triangle: points 3 -> {}, height 245 -> {}", after.vertices.len(), n.height);
|
||||
|
||||
// Guard: no radius, nothing changes.
|
||||
let mut s = Session::new();
|
||||
let v = rounded_vector(&mut s, &[(0.0, 0.0), (184.0, 245.0), (368.0, 0.0)], &[0.0; 3], false);
|
||||
let before = net(&s, v);
|
||||
let f = id_of(&ex(&mut s, "vector.flatten", json!({"ids": [v]}))["id"]).unwrap();
|
||||
assert_eq!(net(&s, f).vertices.len(), 3);
|
||||
assert_eq!(s.doc().unwrap().node(f).unwrap().height, 245.0);
|
||||
assert_eq!(net(&s, f).vertices, before.vertices);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,11 @@ impl FigApp {
|
||||
/// state.
|
||||
pub fn new(session: Session, services: Box<dyn Services>) -> Self {
|
||||
// The main window's panels open as wide as they were left.
|
||||
// Widths saved under other limits are clamped to the current ones, not rejected; one that
|
||||
// isn't finite falls back to the default.
|
||||
let (left_width, right_width) = session.prefs.panel_widths;
|
||||
let within = |w: f32, range: std::ops::RangeInclusive<f32>| if w.is_finite() { w.clamp(*range.start(), *range.end()) } else { PANEL_DEFAULT };
|
||||
let (left_width, right_width) = (within(left_width, LEFT_PANEL), within(right_width, RIGHT_PANEL));
|
||||
let pages_height = session.prefs.pages_height;
|
||||
Self {
|
||||
ui: UiState { left_width, right_width, pages_height, ..UiState::default() },
|
||||
|
||||
@@ -78,7 +78,15 @@ fn panels_fit_the_window() {
|
||||
((744.0, None, Some(480.0)), (0.0, 480.0), "with the left panel closed the right one has room"),
|
||||
((300.0, Some(240.0), Some(240.0)), (l0, r0), "a window smaller than any layout: the least widths"),
|
||||
((1384.0, Some(f32::NAN), Some(f32::INFINITY)), (PANEL_DEFAULT, PANEL_DEFAULT), "junk widths: the default"),
|
||||
((744.0, Some(480.0), None), (480.0, 0.0), "the right panel closed (a board's): the left one keeps its width"),
|
||||
((744.0, Some(480.0), None), (l1, 0.0), "the right panel closed (a board's): the left one keeps as much of its width as it may"),
|
||||
((744.0, Some(450.0), None), (450.0, 0.0), "the right panel closed: the left one at its most"),
|
||||
((1384.0, Some(200.0), Some(480.0)), (l0, 480.0), "a stored left width under 240 (the old limit was 200) is brought up to 240"),
|
||||
((1384.0, Some(470.0), Some(520.0)), (l1, r1), "stored widths over the limits (the old ones were 480) are brought down to 450 and 500"),
|
||||
(
|
||||
(744.0, Some(300.0), Some(500.0)),
|
||||
(240.0, 344.0),
|
||||
"the smallest window with the right panel at its most: the right gives way last, the left to its 240 floor",
|
||||
),
|
||||
];
|
||||
for ((avail, left, right), want, why) in rows {
|
||||
assert_eq!(fit_panels(avail, left, right), want, "{why}");
|
||||
@@ -122,14 +130,17 @@ fn dragging_an_edge_sizes_its_panel_within_its_limits_and_keeps_the_views_centre
|
||||
let (left, right) = edges(&u);
|
||||
drag(&mut u, pos2(left, 300.0), pos2(left + 700.0, 300.0));
|
||||
assert_eq!(widths(&u).0, *LEFT_PANEL.end());
|
||||
assert_eq!(widths(&u).0, 450.0, "Figma's left sidebar stops at 450");
|
||||
let (left, _) = edges(&u);
|
||||
drag(&mut u, pos2(left, 300.0), pos2(20.0, 300.0));
|
||||
assert_eq!(widths(&u).0, *LEFT_PANEL.start());
|
||||
assert_eq!(widths(&u).0, 240.0, "and at 240 the other way");
|
||||
drag(&mut u, pos2(right, 300.0), pos2(right + 500.0, 300.0));
|
||||
assert_eq!(widths(&u).1, *RIGHT_PANEL.start());
|
||||
let (_, right) = edges(&u);
|
||||
u.drag_to(pos2(right, 300.0), pos2(right - 900.0, 300.0));
|
||||
assert_eq!(widths(&u).1, *RIGHT_PANEL.end(), "at its most while the pointer is past it");
|
||||
assert_eq!(widths(&u).1, 500.0, "Figma's right sidebar stops at 500");
|
||||
u.frame(vec![Event::PointerMoved(pos2(right - 100.0, 300.0))]);
|
||||
assert_eq!(widths(&u).1, *RIGHT_PANEL.start() + 100.0, "back with the pointer");
|
||||
u.button(pos2(right - 100.0, 300.0), false);
|
||||
@@ -187,6 +198,17 @@ fn the_widths_come_back_with_the_preferences() {
|
||||
u.frame(vec![]);
|
||||
let (left, right) = edges(&u);
|
||||
assert_eq!((left, 1440.0 - right), (56.0 + *LEFT_PANEL.start(), PANEL_DEFAULT));
|
||||
// Widths saved under the old limits (left 200 to 480, right up to 480) or over the new ones
|
||||
// are clamped when loaded, not rejected.
|
||||
for (stored, want) in
|
||||
[((200.0, 240.0), (240.0, 240.0)), ((239.0, 500.0), (240.0, 500.0)), ((451.0, 480.0), (450.0, 480.0)), ((480.0, 600.0), (450.0, 500.0))]
|
||||
{
|
||||
let mut s = Session::new();
|
||||
s.prefs.panel_widths = stored;
|
||||
let mut u = Ui::new(s);
|
||||
u.frame(vec![]);
|
||||
assert_eq!(widths(&u), want, "stored {stored:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -264,7 +286,7 @@ fn minimize_collapses_the_rail_and_both_sidebars_and_the_right_one_follows_the_s
|
||||
|
||||
#[test]
|
||||
fn the_smallest_window_keeps_its_canvas_whatever_the_widths() {
|
||||
for asked in [(240.0, 240.0), (480.0, 480.0), (480.0, 240.0), (200.0, 480.0)] {
|
||||
for asked in [(240.0, 240.0), (450.0, 500.0), (450.0, 240.0), (240.0, 500.0)] {
|
||||
let mut s = Session::new();
|
||||
s.prefs.panel_widths = asked;
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
@@ -27,10 +27,11 @@ pub enum TitleBar {
|
||||
pub type AutoSaveEncoding = std::thread::JoinHandle<(figcraft_engine::cmd::file::FileSnapshot, figcraft_engine::Result<Vec<u8>>)>;
|
||||
|
||||
/// The side panels' widths: the least and most a drag of their edge gives, what a double-click on
|
||||
/// the edge puts back, and how much of the window they leave the canvas.
|
||||
pub const LEFT_PANEL: std::ops::RangeInclusive<f32> = 200.0..=480.0;
|
||||
/// the edge puts back, and how much of the window they leave the canvas. The limits are Figma's,
|
||||
/// measured in the web app: left 240 to 450, right 240 to 500, both 240 by default.
|
||||
pub const LEFT_PANEL: std::ops::RangeInclusive<f32> = 240.0..=450.0;
|
||||
|
||||
pub const RIGHT_PANEL: std::ops::RangeInclusive<f32> = 240.0..=480.0;
|
||||
pub const RIGHT_PANEL: std::ops::RangeInclusive<f32> = 240.0..=500.0;
|
||||
|
||||
pub const PANEL_DEFAULT: f32 = 240.0;
|
||||
|
||||
|
||||
+5
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user