mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 05:46:46 -06:00
M1: a press on a corner handle resizes instead of picking the corner-radius handle (fixes #292)
The corner-radius handle sits 10 px in from each corner and answers within 6 px, so a press on the inner half of the 8 px corner square picked it: the drag rounded the corners or, dragged outward, did nothing. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the constrain-proportions link); further in the radius handle still answers.
This commit is contained in:
@@ -131,6 +131,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
||||
|
||||
## Progress log
|
||||
|
||||
- **2026-10-14** · Corner handles win over the corner-radius handles (M1, #292): a press on the inner half of a corner handle's square picked the radius handle 10 px in, so the drag neither resized nor (dragging outward) rounded anything. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the link); further in the radius handle still answers. Regression test in the transform behaviour suite. Numbers unchanged.
|
||||
- **2026-10-14** · Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged.
|
||||
- **2026-10-13** · Vector edit mode regions (M4): with the Move tool, the filled area under the pointer is hatched with "/" lines in the selection colour, clipped by the area's own fill rule so holes stay clear; the hatch goes near points (3.5 px) and segments (2 px), and its spacing follows octave folding (8 px at every power-of-two zoom), fixed to the shape. A click selects the area with its points (they move with it but keep the unselected look), Shift adds or removes one, a press on a selected area drags the whole selection as one undo step, and a click on an empty spot clears the areas and stays in edit mode. `vector.edit.select {regions}` and `vector.edit.state` (`regions`, `regionVertices`, `regionHover`); `vector_regions_tests` and `regions_tests`. Numbers unchanged.
|
||||
- **2026-10-14** · SVG/PDF export depth (M0.9, #208): visible descendants deeper than 256 levels below an exported root are refused before bounds or drawing, including masks and raster fallbacks. SVG has a checked API; the engine and CLI report the limit and suggest selecting an inner layer. Existing destination files survive refusal. Boundary, inner-root, hidden-subtree, multi-root/page and command/CLI regressions cover the change. Numbers unchanged.
|
||||
|
||||
@@ -617,6 +617,18 @@ pub fn handle_at(doc: &Document, sel: &[NodeId], p: Point, zoom: f64) -> Option<
|
||||
handle_at_corners(&corners, p, zoom, is_line)
|
||||
}
|
||||
|
||||
/// Half the side of the square the canvas draws on each corner of the handle box (screen pixels,
|
||||
/// upright whatever the box's rotation).
|
||||
const CORNER_SQUARE_PX: f64 = 4.0;
|
||||
|
||||
/// Whether `p` is on a corner square of the selection's handle box as drawn: there the corner
|
||||
/// resizes, although a corner-radius handle 10 px in reaches that far too.
|
||||
fn on_corner_square(doc: &Document, sel: &[NodeId], p: Point, zoom: f64) -> bool {
|
||||
let reach = CORNER_SQUARE_PX / zoom.max(1e-6);
|
||||
matches!(handle_at(doc, sel, p, zoom), Some(Handle::Corner(_)))
|
||||
&& selection_box(doc, sel).is_some_and(|c| c.iter().any(|c| (c.x - p.x).abs() <= reach && (c.y - p.y).abs() <= reach))
|
||||
}
|
||||
|
||||
/// A box whose sides are both shorter than this on screen (pixels) would be covered by its
|
||||
/// handles: the canvas draws just its outline (the handles still answer just outside it).
|
||||
pub const SMALL_BOX_PX: f64 = 16.0;
|
||||
@@ -982,7 +994,11 @@ impl Session {
|
||||
let mut measure_box = None;
|
||||
let arc_hover = if self.arc_handles_shown() { crate::arc_handles::hit(&doc, &sel, ev.p(), ev.zoom).map(|(_, h)| h) } else { None };
|
||||
self.tool.overlay.arc_hover = arc_hover;
|
||||
let shape_hover = if self.arc_handles_shown() { crate::shape_handles::hit(&doc, &sel, ev.p(), ev.zoom).map(|(_, h)| h) } else { None };
|
||||
let shape_hover = if self.arc_handles_shown() && !on_corner_square(&doc, &sel, ev.p(), ev.zoom) {
|
||||
crate::shape_handles::hit(&doc, &sel, ev.p(), ev.zoom).map(|(_, h)| h)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.tool.overlay.shape_hover = shape_hover;
|
||||
if matches!(tool, ToolId::Move | ToolId::Scale) && on_edited {
|
||||
cursor = Cursor::Text;
|
||||
@@ -1090,6 +1106,7 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
if self.arc_handles_shown()
|
||||
&& !on_corner_square(&doc, &sel, ev.p(), ev.zoom)
|
||||
&& let Some((id, handle)) = crate::shape_handles::hit(&doc, &sel, ev.p(), ev.zoom)
|
||||
{
|
||||
self.begin_interaction("Corner radius")?;
|
||||
|
||||
@@ -277,6 +277,35 @@ fn constrain_proportions_keeps_the_other_side_in_ratio() {
|
||||
assert_eq!(size(&s, z), (20.0, 0.0), "a zero height leaves nothing to scale");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_press_anywhere_on_a_corner_handle_resizes() {
|
||||
// The canvas draws each corner handle as an 8 px square centred on the corner, and the
|
||||
// corner-radius handle sits 10 px in along the diagonal: a press on the square's inner half
|
||||
// resizes (both sides, in ratio with the link on), it doesn't pick the radius handle.
|
||||
let radii = |s: &Session, r: NodeId| s.doc().unwrap().node(r).unwrap().corners.radii;
|
||||
for lock in [false, true] {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s, 0.0, 0.0, 1000.0, 600.0, None);
|
||||
set(&mut s, &[r], json!({"constrainProportions": lock}));
|
||||
ex(&mut s, "select.set", json!({"ids": [r]}));
|
||||
let on_square = Point::new(997.0, 597.0);
|
||||
s.pointer(&ev(PointerKind::Hover, on_square, NONE)).unwrap();
|
||||
assert!(matches!(s.tool.overlay.cursor, Cursor::Resize(_)), "lock {lock}: {:?}", s.tool.overlay.cursor);
|
||||
drag(&mut s, on_square, Point::new(1197.0, 637.0), NONE);
|
||||
let (w, h) = size(&s, r);
|
||||
let want_h = if lock { 718.2 } else { 637.0 };
|
||||
assert!(near(w, 1197.0) && near(h, want_h), "lock {lock}: both sides follow the corner, got {w}×{h}");
|
||||
assert_eq!(radii(&s, r), [0.0; 4]);
|
||||
}
|
||||
// Further in, past the square, the radius handle still answers.
|
||||
let mut s = session();
|
||||
let r = rect(&mut s, 0.0, 0.0, 1000.0, 600.0, None);
|
||||
ex(&mut s, "select.set", json!({"ids": [r]}));
|
||||
drag(&mut s, Point::new(993.0, 593.0), Point::new(980.0, 580.0), NONE);
|
||||
assert_eq!(size(&s, r), (1000.0, 600.0));
|
||||
assert!(radii(&s, r)[0] > 0.0, "the radius handle rounds the corners");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frames_resize_with_constraints_and_cmd_ignores_them() {
|
||||
let mut s = session();
|
||||
|
||||
Reference in New Issue
Block a user