mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-12 05:12:39 +00:00
Direct Selection points: each moves in its own spread; Delete deletes them; Transform measures them
`path.moveAnchors` points may carry their own spread-space movement (`[id, subpath, index, dx, dy]`). Dragging and the arrow keys send each selected point's movement converted for its spread, so on a spread turned on screen a point moves the way the pointer or arrow does. Delete and Backspace with points selected run `path.deletePoints`. Properties › Transform for selected points shows the points' bounds; X/Y move them and W/H spread or gather them about the reference point, in one step (`transform_anchors`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
66b51ee15f
commit
d744b760fe
6 files changed
+205
-19
No files matched your search
@@ -501,6 +501,27 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Join Path closes one open path, and joins two open paths at their nearest ends.
|
||||
/// `path.moveAnchors` points may each carry their own movement (Direct Selection sends each
|
||||
/// point's in its own spread); without one a point takes the shared `dx`, `dy`.
|
||||
#[test]
|
||||
fn move_anchors_takes_a_movement_per_point() {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &serde_json::json!({})).unwrap();
|
||||
let id = s.execute("frame.create", &serde_json::json!({"rect": [100, 100, 200, 200]})).unwrap()["id"].as_u64().unwrap();
|
||||
let at = |s: &crate::Session, i: usize| {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let it = d.item(designcraft_doc::ItemId(id)).unwrap();
|
||||
it.xf * it.path.subpaths[0].anchors[i].p
|
||||
};
|
||||
let (a0, a1) = (at(&s, 0), at(&s, 1));
|
||||
s.execute("path.moveAnchors", &serde_json::json!({"points": [[id, 0, 0, 5, 0], [id, 0, 1]], "dx": 0, "dy": 7})).unwrap();
|
||||
assert_eq!(at(&s, 0) - a0, designcraft_geom::Vec2::new(5.0, 0.0), "its own");
|
||||
assert_eq!(at(&s, 1) - a1, designcraft_geom::Vec2::new(0.0, 7.0), "the shared one");
|
||||
for bad in [serde_json::json!([[id, 0, 0, "x", 1]]), serde_json::json!([[id, 0, 0, 5]])] {
|
||||
assert!(s.execute("path.moveAnchors", &serde_json::json!({"points": bad})).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_path_closes_one_path_and_joins_two() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -685,7 +685,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Move Anchors",
|
||||
[],
|
||||
None,
|
||||
"{id, anchors: [[subpath, index]], dx, dy, handle?: in|out} | {points: [[id, subpath, index]], dx, dy} (anchors of several paths, as Direct Selection drags them: one step)",
|
||||
"{id, anchors: [[subpath, index]], dx, dy, handle?: in|out} | {points: [[id, subpath, index, dx?, dy?]], dx?, dy?} (anchors of several paths, as Direct Selection drags them: one step; a point's own dx, dy win over the shared ones) — spread space",
|
||||
has_doc,
|
||||
path_move_anchors
|
||||
),
|
||||
@@ -3352,22 +3352,31 @@ fn path_close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn path_move_anchors(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// The anchors to move, by path: `points` (several paths) or `id` with `anchors`.
|
||||
let mut targets: Vec<(ItemId, Vec<(usize, usize)>)> = Vec::new();
|
||||
// Each anchor with its own spread-space movement, when the points give one.
|
||||
let mut targets: Vec<(ItemId, Vec<(usize, usize, Option<Vec2>)>)> = Vec::new();
|
||||
if let Some(points) = p.get("points").and_then(Value::as_array) {
|
||||
for v in points {
|
||||
let n = |i: usize| v.get(i).and_then(Value::as_u64).and_then(|x| usize::try_from(x).ok());
|
||||
let (Some(id), Some(si), Some(ai)) = (v.get(0).and_then(Value::as_u64), n(1), n(2)) else {
|
||||
return Err(bad("path.moveAnchors", "each point is [id, subpath, index]"));
|
||||
return Err(bad("path.moveAnchors", "each point is [id, subpath, index] or [id, subpath, index, dx, dy]"));
|
||||
};
|
||||
let own = match (v.get(3), v.get(4)) {
|
||||
(None, None) => None,
|
||||
(Some(x), Some(y)) => match (x.as_f64(), y.as_f64()) {
|
||||
(Some(x), Some(y)) if x.is_finite() && y.is_finite() => Some(Vec2::new(x, y)),
|
||||
_ => return Err(bad("path.moveAnchors", "a point's dx and dy are finite numbers")),
|
||||
},
|
||||
_ => return Err(bad("path.moveAnchors", "a point gives both dx and dy, or neither")),
|
||||
};
|
||||
match targets.iter_mut().find(|(i, _)| i.0 == id) {
|
||||
Some((_, which)) => which.push((si, ai)),
|
||||
None => targets.push((ItemId(id), vec![(si, ai)])),
|
||||
Some((_, which)) => which.push((si, ai, own)),
|
||||
None => targets.push((ItemId(id), vec![(si, ai, own)])),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let id = super::id_param(p, "id").ok_or_else(|| bad("path.moveAnchors", "missing id"))?;
|
||||
let which: Vec<(usize, usize)> = p.get("anchors").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
|
||||
targets.push((id, which));
|
||||
targets.push((id, which.into_iter().map(|(s, a)| (s, a, None)).collect()));
|
||||
}
|
||||
let d0 = Vec2::new(f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0));
|
||||
let handle = str_param(p, "handle").map(str::to_string);
|
||||
@@ -3381,9 +3390,10 @@ fn path_move_anchors(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let m = d.parent_xf(&loc) * d.item_at(&loc).map(|i| i.xf).unwrap_or_default();
|
||||
// Spread-space delta → inner-space delta (linear part only).
|
||||
let inv = m.inverse();
|
||||
let dv = (inv * Point::new(d0.x, d0.y)) - (inv * Point::ZERO);
|
||||
let inner = |d: Vec2| (inv * Point::new(d.x, d.y)) - (inv * Point::ZERO);
|
||||
let it = d.item_mut(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
|
||||
for (si, ai) in which {
|
||||
for (si, ai, own) in which {
|
||||
let dv = inner(own.unwrap_or(d0));
|
||||
let Some(a) = it.path.anchor_mut(*si, *ai) else { continue };
|
||||
match handle.as_deref() {
|
||||
Some("in") => {
|
||||
|
||||
@@ -263,6 +263,18 @@ fn anchor_selection(cx: &ToolContext, points: &[(ItemId, usize, usize)], keep: b
|
||||
json!({"ids": ids, "anchors": anchors})
|
||||
}
|
||||
|
||||
/// `path.moveAnchors` points for `points` moved `d` on the canvas: each with its own movement in
|
||||
/// its spread (a rotated spread view turns it).
|
||||
fn moved_points(cx: &ToolContext, points: impl IntoIterator<Item = (ItemId, usize, usize)>, d: Vec2) -> Vec<Value> {
|
||||
points
|
||||
.into_iter()
|
||||
.map(|(i, s, a)| {
|
||||
let v = cx.doc.find(i).map_or(d, |loc| cx.layout.delta_to_spread(loc.spread, d));
|
||||
json!([i.0, s, a, v.x, v.y])
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `d` held to the nearest multiple of 45°, as Shift does.
|
||||
fn constrain_45(d: Vec2) -> Vec2 {
|
||||
if d.hypot() < 1e-9 {
|
||||
@@ -1212,12 +1224,14 @@ impl Tool for SelectionTool {
|
||||
dy += corr.y;
|
||||
self.guides = hit.guides;
|
||||
}
|
||||
let mut params = json!({"id": id, "anchors": [[si, ai]], "dx": dx, "dy": dy});
|
||||
// The command moves in spread space: each point by the drag in its own spread.
|
||||
let own = at.map_or(Vec2::new(dx, dy), |(spread, _)| cx.layout.delta_to_spread(spread, Vec2::new(dx, dy)));
|
||||
let mut params = json!({"id": id, "anchors": [[si, ai]], "dx": own.x, "dy": own.y});
|
||||
if let Some(h) = handle {
|
||||
params["handle"] = json!(h);
|
||||
} else if !others.is_empty() {
|
||||
let points: Vec<Value> = std::iter::once((id, si, ai)).chain(others).map(|(i, s, a)| json!([i, s, a])).collect();
|
||||
params = json!({"points": points, "dx": dx, "dy": dy});
|
||||
let points = std::iter::once((id, si, ai)).chain(others).map(|(i, s, a)| (ItemId(i), s, a));
|
||||
params = json!({"points": moved_points(cx, points, Vec2::new(dx, dy))});
|
||||
}
|
||||
vec![Action::Preview("path.moveAnchors".into(), params)]
|
||||
}
|
||||
@@ -1300,15 +1314,20 @@ impl Tool for SelectionTool {
|
||||
if key == ToolKey::Escape && self.live.take().is_some_and(|id| cx.selection.items == [id]) {
|
||||
return vec![Action::Handled];
|
||||
}
|
||||
let points: Vec<Value> = cx.selection.anchors.iter().map(|(i, s, a)| json!([i.0, s, a])).collect();
|
||||
let direct_points = self.direct && !points.is_empty();
|
||||
let direct_points = self.direct && !cx.selection.anchors.is_empty();
|
||||
let mv = |dx: f64, dy: f64| {
|
||||
// Direct Selection with anchor points selected: the arrow keys move the points.
|
||||
// Direct Selection with anchor points selected: the arrow keys move the points, each
|
||||
// the way the arrow points on screen.
|
||||
if direct_points {
|
||||
return vec![Action::Exec("path.moveAnchors".into(), json!({"points": points.clone(), "dx": dx, "dy": dy}))];
|
||||
let points = moved_points(cx, cx.selection.anchors.iter().copied(), Vec2::new(dx, dy));
|
||||
return vec![Action::Exec("path.moveAnchors".into(), json!({"points": points}))];
|
||||
}
|
||||
vec![Action::Exec("transform.move".into(), json!({"dx": dx, "dy": dy, "copy": mods.alt}))]
|
||||
};
|
||||
// Selected points: Delete and Backspace delete them, not their paths.
|
||||
if direct_points && matches!(key, ToolKey::Delete | ToolKey::Backspace) {
|
||||
return vec![Action::Exec("path.deletePoints".into(), json!({}))];
|
||||
}
|
||||
if cx.selection.items.is_empty() {
|
||||
// Selected ruler guides: the arrows nudge them, Delete deletes them.
|
||||
if cx.selection.guides.is_empty() {
|
||||
|
||||
@@ -1990,14 +1990,18 @@ fn direct_selection_marquee_selects_points_of_several_frames_and_drags_them_toge
|
||||
assert_eq!(cmd, "path.moveAnchors");
|
||||
assert_eq!(p["points"].as_array().map(Vec::len), Some(4), "{p}");
|
||||
// Shift keeps the move horizontal.
|
||||
assert_eq!((p["dx"].as_f64(), p["dy"].as_f64()), (Some(36.0), Some(0.0)));
|
||||
// Each point carries its own movement (in its spread): here all the same.
|
||||
let moves = |p: &serde_json::Value| -> Vec<(f64, f64)> {
|
||||
p["points"].as_array().unwrap().iter().map(|v| (v[3].as_f64().unwrap(), v[4].as_f64().unwrap())).collect()
|
||||
};
|
||||
assert!(moves(p).iter().all(|&(x, y)| (x - 36.0).abs() < 1e-9 && y.abs() < 1e-9), "{p}");
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, at.x + 36.0, at.y)), vec![Action::Commit]);
|
||||
// A Shift-drag that starts on a selected point keeps it: all four move, constrained.
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, at.x, at.y).with_mods(shift));
|
||||
let drag = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, at.x + 3.0, at.y + 30.0).with_mods(shift));
|
||||
let Some(Action::Preview(_, p)) = drag.last() else { panic!("{drag:?}") };
|
||||
assert_eq!(p["points"].as_array().map(Vec::len), Some(4), "{p}");
|
||||
assert!(p["dx"].as_f64().unwrap().abs() < 1e-9 && (p["dy"].as_f64().unwrap() - 30.0).abs() < 1e-9, "vertical: {p}");
|
||||
assert!(moves(p).iter().all(|&(x, y)| x.abs() < 1e-9 && (y - 30.0).abs() < 1e-9), "vertical: {p}");
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, at.x, at.y + 30.0).with_mods(shift)), vec![Action::Commit]);
|
||||
// Shift+click on a selected point takes it out of the selection, on release; the arrow keys
|
||||
// move the points.
|
||||
@@ -2012,4 +2016,18 @@ fn direct_selection_marquee_selects_points_of_several_frames_and_drags_them_toge
|
||||
matches!(keys.as_slice(), [Action::Exec(cmd, p)] if cmd == "path.moveAnchors" && p["points"].as_array().map(Vec::len) == Some(4)),
|
||||
"{keys:?}"
|
||||
);
|
||||
// Delete deletes the selected points, not their paths.
|
||||
assert_eq!(t.key(&cx, ToolKey::Delete, Mods::default()), vec![Action::Exec("path.deletePoints".into(), serde_json::json!({}))]);
|
||||
// On a spread turned a quarter on screen, → moves the points right on screen: up or down in
|
||||
// the spread, not across it.
|
||||
let mut turned = d.clone();
|
||||
std::sync::Arc::make_mut(&mut turned.spreads[0]).pages[0].view_rotation = 1;
|
||||
let lt = CanvasLayout::new(&turned, false);
|
||||
let cxt = ctx(&turned, &sel, &c, <);
|
||||
let keys = t.key(&cxt, ToolKey::Right, Mods::default());
|
||||
let [Action::Exec(_, p)] = keys.as_slice() else { panic!("{keys:?}") };
|
||||
let inc = turned.settings.keyboard_increment;
|
||||
let want = lt.delta_to_spread(SpreadRef::Doc(0), designcraft_geom::Vec2::new(inc, 0.0));
|
||||
assert!(want.x.abs() < 1e-9 && (want.y.abs() - inc).abs() < 1e-9, "a quarter turn: {want:?}");
|
||||
assert!(moves(p).iter().all(|&(x, y)| (x - want.x).abs() < 1e-9 && (y - want.y).abs() < 1e-9), "{p}");
|
||||
}
|
||||
@@ -160,6 +160,22 @@ pub fn guide_field(app: &DesignApp) -> Option<GuideField> {
|
||||
value.is_finite().then_some(GuideField { guide: *g, orientation: guide.orientation, value, origin, unit })
|
||||
}
|
||||
|
||||
/// The anchor points selected with Direct Selection, each with where it is in its spread.
|
||||
pub fn selected_anchor_points(app: &DesignApp) -> Vec<((designcraft_doc::ItemId, usize, usize), designcraft_geom::Point)> {
|
||||
let Some(st) = app.session.active() else { return vec![] };
|
||||
let d = &st.doc;
|
||||
st.selection
|
||||
.anchors
|
||||
.iter()
|
||||
.filter_map(|&(id, si, ai)| {
|
||||
let loc = d.find(id)?;
|
||||
let it = d.item(id)?;
|
||||
let a = it.path.subpaths.get(si)?.anchors.get(ai)?;
|
||||
Some(((id, si, ai), (d.parent_xf(&loc) * it.xf) * a.p))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn sel_info(app: &DesignApp) -> Option<SelInfo> {
|
||||
let st = app.session.active()?;
|
||||
if st.selection.items.is_empty() {
|
||||
@@ -181,7 +197,12 @@ pub fn sel_info(app: &DesignApp) -> Option<SelInfo> {
|
||||
};
|
||||
b = Some(b.map_or(ib, |r| r.union(ib)));
|
||||
}
|
||||
let b = b?;
|
||||
// Points selected with Direct Selection: Transform measures the points, not their paths.
|
||||
let points = selected_anchor_points(app);
|
||||
let b = match points.split_first() {
|
||||
Some(((_, p0), rest)) => rest.iter().fold(Rect::from_points(*p0, *p0), |r, (_, p)| r.union_pt(*p)),
|
||||
None => b?,
|
||||
};
|
||||
let first = d.item(st.selection.items[0])?;
|
||||
let loc = d.find(first.id)?;
|
||||
let o = d.ruler_origin(loc.spread, b.center().x)?;
|
||||
|
||||
@@ -630,6 +630,72 @@ fn transform_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties › Transform for points selected with Direct Selection: X/Y/W/H of the points'
|
||||
/// bounds (from the reference point); X and Y move the points, W and H spread or gather them
|
||||
/// about the reference point. No angles: those turn objects.
|
||||
fn anchor_transform_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
|
||||
let u = units(app);
|
||||
let rp = app.ui.ref_point;
|
||||
let b = block(ui, 2.0 * FIELD_H + 4.0);
|
||||
let r = i.page_rect;
|
||||
let (fx, fy) = ((rp % 3) as f64 / 2.0, (rp / 3) as f64 / 2.0);
|
||||
let (x, y) = (r.x0 + r.width() * fx, r.y0 + r.height() * fy);
|
||||
if let Some(p) = ref_point_proxy(ui, sub(b, 5.0, 10.0, 26.0, 26.0), rp) {
|
||||
app.ui.ref_point = p;
|
||||
}
|
||||
let mut set = serde_json::Map::new();
|
||||
for (row, (lx, vx, kx, lw, vw, kw)) in
|
||||
[("X:", x, "x", "W:", r.width(), "width"), ("Y:", y, "y", "H:", r.height(), "height")].into_iter().enumerate()
|
||||
{
|
||||
let yy = row as f32 * 25.0;
|
||||
place(ui, sub(b, 40.0, yy, 18.0, FIELD_H), |ui| widgets::caption_w(ui, lx, 18.0));
|
||||
if let Some(v) = place(ui, sub(b, 59.0, yy, 54.0, FIELD_H), |ui| measure(ui, &format!("pt_{kx}"), Some(vx), u, 54.0)) {
|
||||
set.insert(kx.into(), json!(v));
|
||||
}
|
||||
place(ui, sub(b, 120.0, yy, 19.0, FIELD_H), |ui| widgets::caption_w(ui, lw, 19.0));
|
||||
if let Some(v) = place(ui, sub(b, 140.0, yy, 54.0, FIELD_H), |ui| measure(ui, &format!("pt_{kw}"), Some(vw), u, 54.0)) {
|
||||
set.insert(kw.into(), json!(v));
|
||||
}
|
||||
}
|
||||
if !set.is_empty() {
|
||||
let _ = transform_anchors(app, &Value::Object(set));
|
||||
}
|
||||
if more_options(ui, &app.ui.language).clicked() {
|
||||
show_panel(app, "info");
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the selected points as Properties › Transform's `set` asks ({x?, y?, width?, height?},
|
||||
/// in the page coordinates the fields show, from the reference point): one `path.moveAnchors`
|
||||
/// step, each point by its own movement. A width or height of a single point (0) can't scale.
|
||||
pub(crate) fn transform_anchors(app: &mut DesignApp, set: &Value) -> Result<Value, String> {
|
||||
let points = super::selected_anchor_points(app);
|
||||
let Some(i) = super::sel_info(app) else { return Ok(Value::Null) };
|
||||
if points.is_empty() {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
let rp = app.ui.ref_point;
|
||||
let r = i.page_rect;
|
||||
let (fx, fy) = ((rp % 3) as f64 / 2.0, (rp / 3) as f64 / 2.0);
|
||||
// Page coordinates and spread coordinates differ by the ruler origin only.
|
||||
let b = points.iter().skip(1).fold(designcraft_geom::Rect::from_points(points[0].1, points[0].1), |b, (_, p)| b.union_pt(*p));
|
||||
let origin = (b.x0 - r.x0, b.y0 - r.y0);
|
||||
let reference = (r.x0 + r.width() * fx + origin.0, r.y0 + r.height() * fy + origin.1);
|
||||
let num = |k: &str| set.get(k).and_then(Value::as_f64).filter(|v| v.is_finite());
|
||||
let scale = |new: Option<f64>, old: f64| new.filter(|_| old > 1e-9).map_or(1.0, |v| v.max(0.0) / old);
|
||||
let (kx, ky) = (scale(num("width"), r.width()), scale(num("height"), r.height()));
|
||||
let shift = (num("x").map_or(0.0, |v| v - (r.x0 + r.width() * fx)), num("y").map_or(0.0, |v| v - (r.y0 + r.height() * fy)));
|
||||
let moved: Vec<Value> = points
|
||||
.iter()
|
||||
.map(|&((id, s, a), p)| {
|
||||
let nx = reference.0 + (p.x - reference.0) * kx + shift.0;
|
||||
let ny = reference.1 + (p.y - reference.1) * ky + shift.1;
|
||||
json!([id.0, s, a, nx - p.x, ny - p.y])
|
||||
})
|
||||
.collect();
|
||||
app.run("path.moveAnchors", json!({"points": moved}))
|
||||
}
|
||||
|
||||
/// Properties › Transform for selected ruler guides: X for one vertical guide, Y for one
|
||||
/// horizontal guide; with several guides both fields are empty.
|
||||
fn guide_section(app: &mut DesignApp, ui: &mut Ui) {
|
||||
@@ -2256,7 +2322,7 @@ const CONVERT_SHAPE: [(&str, &str, &str); 9] = [
|
||||
/// Appearance, Convert Points (the point type of the selected points) and Delete Point(s).
|
||||
fn anchor_point_sections(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) {
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Transform"), true);
|
||||
transform_section(app, ui, i);
|
||||
anchor_transform_section(app, ui, i);
|
||||
divider(ui);
|
||||
section(ui, crate::i18n::tr(&app.ui.language, "Appearance"), true);
|
||||
appearance_section(app, ui, i);
|
||||
@@ -3512,6 +3578,37 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// With points selected, Transform measures and moves the points: one corner of a frame shows
|
||||
/// that corner, not the frame, and X moves the corner alone; W spreads two points apart.
|
||||
#[test]
|
||||
fn transform_measures_and_moves_the_selected_points() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let a = app.run("frame.create", json!({"rect": [72, 72, 172, 172]})).unwrap()["id"].as_u64().unwrap();
|
||||
let corner = |app: &DesignApp, i: usize| {
|
||||
let d = &app.session.active().unwrap().doc;
|
||||
let it = d.item(designcraft_doc::ids::ItemId(a)).unwrap();
|
||||
it.xf * it.path.subpaths[0].anchors[i].p
|
||||
};
|
||||
let first = corner(&app, 0);
|
||||
app.run("selection.set", json!({"anchors": [[a, 0, 0]]})).unwrap();
|
||||
let i = super::super::sel_info(&app).unwrap();
|
||||
assert_eq!((i.page_rect.width(), i.page_rect.height()), (0.0, 0.0), "a point, not the frame");
|
||||
app.ui.ref_point = 0;
|
||||
transform_anchors(&mut app, &json!({"x": i.page_rect.x0 + 10.0})).unwrap();
|
||||
assert_eq!(corner(&app, 0), first + designcraft_geom::Vec2::new(10.0, 0.0), "the corner moved");
|
||||
assert_eq!(corner(&app, 2), designcraft_geom::Point::new(172.0, 172.0), "the opposite corner stayed");
|
||||
// Two points 100 pt apart, W 200 from the left: the right one goes 100 further.
|
||||
app.run("selection.set", json!({"anchors": [[a, 0, 2], [a, 0, 3]]})).unwrap();
|
||||
let (p2, p3) = (corner(&app, 2), corner(&app, 3));
|
||||
let w = super::super::sel_info(&app).unwrap().page_rect.width();
|
||||
transform_anchors(&mut app, &json!({"width": w * 2.0})).unwrap();
|
||||
let (left, right) = if p2.x < p3.x { (2, 3) } else { (3, 2) };
|
||||
let (pl, pr) = if left == 2 { (p2, p3) } else { (p3, p2) };
|
||||
assert_eq!(corner(&app, left), pl, "the reference side stays");
|
||||
assert!((corner(&app, right).x - (pr.x + w)).abs() < 1e-9, "spread to twice the width");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_flips_and_rotates_from_properties() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
|
||||
Reference in new issue
Block a user