mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
Fix #442: alike circular corners on rectangles that aren't square
Live rectangles drew elliptical corners after an uneven scale in v0.4.0; inconsistent on a rectangle that isn't square: - doc: LiveShape::fold_scale moves an uneven scale left in `xf` (files saved before #291) into the size and radii; `transform` uses it, and `folded()`/`corner_style` give Live Corners document units. - engine: object.setLiveShape folds that scale first, so setting a radius or kind (fields, dialog, widgets, MCP) gives circular corners on such shapes instead of keeping the ellipses. - geom: max_corner_radius/fitted_corner_radius are the one limit every corner shares (half the shorter side). - tools: corner widgets sit at the centres of the corners as drawn (a radius past the limit put them off the arc, further in along the long side), drag in document units on stretched shapes, and outline the edited corners in red at the limit, as the reference app does. - svg: CSS export writes the drawn radii (CSS would draw a radius past the limit when its neighbours leave room). - docs: mcp.md Live Corners, ROADMAP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7c7ce7f4fb
commit
44c7e64e1b
@@ -1038,6 +1038,9 @@ fn set_live_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit("Live Shape", |d, sel| {
|
||||
for id in &ids {
|
||||
let Some(NodeKind::Path { path, live: Some(live), .. }) = d.node_mut(*id).map(|n| &mut n.kind) else { continue };
|
||||
// Radii are document lengths and corners circular, also on a rectangle from a file
|
||||
// that kept an uneven scale in its transform (#442).
|
||||
live.fold_scale();
|
||||
let partial = sel.anchors.get_mut(id);
|
||||
let picked = corners.unwrap_or_else(|| live.picked_corners(partial.as_deref()));
|
||||
let (layout, selected) = (live.anchor_corners(), partial.as_deref().map(|a| live.corners_of(a)));
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::collections::BTreeSet;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{AnchorRef, LiveShape, NodeKind};
|
||||
use vectorcraft_geom::Affine;
|
||||
use vectorcraft_geom::shapes::CornerKind;
|
||||
|
||||
use super::*;
|
||||
@@ -105,3 +106,47 @@ fn a_drag_previews_one_corner_and_commits_one_step() {
|
||||
assert_eq!(selected_anchors(&s, id), Some(anchors(&[2, 3])));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
}
|
||||
|
||||
/// How far each corner's cut reaches along x and y (top-left, top-right, bottom-right,
|
||||
/// bottom-left) of a rectangle with four cut corners: equal for a circular arc.
|
||||
fn spans(s: &Session, id: NodeId) -> Vec<(f64, f64)> {
|
||||
let NodeKind::Path { path, .. } = &s.doc().unwrap().doc.node(id).unwrap().kind else { panic!("not a path") };
|
||||
let a: Vec<_> = path.subpaths[0].anchors.iter().map(|a| a.p).collect();
|
||||
assert_eq!(a.len(), 8, "four cut corners");
|
||||
[(7, 0), (1, 2), (3, 4), (5, 6)].iter().map(|(i, j)| ((a[*j].x - a[*i].x).abs(), (a[*j].y - a[*i].y).abs())).collect()
|
||||
}
|
||||
|
||||
fn assert_spans(s: &Session, id: NodeId, want: [f64; 4]) {
|
||||
let got = spans(s, id);
|
||||
let ok = got.iter().zip(want).all(|((x, y), r)| (x - r).abs() < 1e-9 && (y - r).abs() < 1e-9);
|
||||
assert!(ok, "corners span {got:?}, want circles of radii {want:?}");
|
||||
}
|
||||
|
||||
/// #442: on a rectangle that isn't square, every corner is the same circle, also past the limit
|
||||
/// and on a shape from a file that kept an uneven scale in its transform (before #291).
|
||||
#[test]
|
||||
fn corners_of_a_non_square_rectangle_are_alike_circles() {
|
||||
let (mut s, id) = session();
|
||||
run(&mut s, json!({"radius": 12}));
|
||||
assert_spans(&s, id, [12.0; 4]);
|
||||
// Past half the shorter side, every corner stops there.
|
||||
run(&mut s, json!({"radius": 45}));
|
||||
assert_spans(&s, id, [30.0; 4]);
|
||||
// A 50 × 60 rectangle with 10 pt corners, saved stretched to 100 × 60: 20 × 10 ellipses.
|
||||
{
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let NodeKind::Path { path, live: Some(live), .. } = &mut d.node_mut(id).unwrap().kind else { panic!("not live") };
|
||||
let xf = Affine::translate((10.0, 10.0)) * Affine::scale_non_uniform(2.0, 1.0);
|
||||
*live = LiveShape::Rectangle { w: 50.0, h: 60.0, radii: [10.0; 4], kinds: Default::default(), xf };
|
||||
*path = live.to_path();
|
||||
}
|
||||
assert_eq!(spans(&s, id)[0], (20.0, 10.0));
|
||||
// Rounding one corner makes every corner a circle: the others of the mean radius.
|
||||
run(&mut s, json!({"corners": [1], "radius": 12}));
|
||||
let mean = 10.0 * 2f64.sqrt();
|
||||
assert_spans(&s, id, [mean, 12.0, mean, mean]);
|
||||
let b = s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap();
|
||||
assert!((b.x0 - 10.0).abs() < 1e-9 && (b.x1 - 110.0).abs() < 1e-9 && (b.y1 - 70.0).abs() < 1e-9, "same bounds: {b:?}");
|
||||
run(&mut s, json!({"radius": 8}));
|
||||
assert_spans(&s, id, [8.0; 4]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user