mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 05:16:56 -06:00
270 lines
11 KiB
Rust
270 lines
11 KiB
Rust
use super::{
|
|
Affine, BTreeMap, BTreeSet, Node, NodeId, Result, Session, Value, bad, boolean, json, num, num_or, ok, req_num, req_string, req_targets, roots,
|
|
};
|
|
|
|
pub(super) fn layers_at(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let (x, y) = (req_num("select.layersAt", p, "x")?, req_num("select.layersAt", p, "y")?);
|
|
let doc = s.doc()?;
|
|
let zoom = s.ds()?.view.zoom.max(1e-6);
|
|
let sel = s.selection();
|
|
let result = crate::booleans::result_outline(doc, &sel);
|
|
let layers: Vec<Value> = figcraft_doc::geometry::layers_at(doc, s.page()?, figcraft_geom::Point::new(x, y), 2.0 / zoom, Some(&result))
|
|
.into_iter()
|
|
.filter_map(|(id, depth)| doc.node(id).map(|n| json!({"id": id, "name": n.name, "type": n.kind.type_name(), "depth": depth})))
|
|
.collect();
|
|
Ok(json!({"layers": layers}))
|
|
}
|
|
|
|
pub(super) fn select_all(s: &mut Session, _p: &Value) -> Result<Value> {
|
|
let doc = s.doc()?;
|
|
let sel = s.selection();
|
|
// Select all siblings of the current selection, else the page's top level.
|
|
let parent = sel.first().and_then(|i| doc.node(*i)).and_then(|n| n.parent).unwrap_or(s.page()?);
|
|
let ids: Vec<NodeId> = doc.get(parent)?.children.iter().copied().filter(|c| doc.node(*c).is_some_and(|n| n.visible && !n.locked)).collect();
|
|
s.set_selection(ids.clone());
|
|
Ok(json!({"selection": ids}))
|
|
}
|
|
|
|
/// What Select all would select, less the selection (hidden and locked layers neither).
|
|
pub(super) fn select_inverse(s: &mut Session, _p: &Value) -> Result<Value> {
|
|
let doc = s.doc()?;
|
|
let sel = s.selection();
|
|
let parent = sel.first().and_then(|i| doc.node(*i)).and_then(|n| n.parent).unwrap_or(s.page()?);
|
|
let ids: Vec<NodeId> =
|
|
doc.get(parent)?.children.iter().copied().filter(|c| !sel.contains(c) && doc.node(*c).is_some_and(|n| n.visible && !n.locked)).collect();
|
|
s.set_selection(ids.clone());
|
|
Ok(json!({"selection": ids}))
|
|
}
|
|
|
|
/// `select.area`: what a marquee from (x, y) over the box selects; no box asks the app for one.
|
|
pub(super) fn select_area(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cmd = "select.area";
|
|
let keys = ["x", "y", "width", "height"];
|
|
if keys.iter().all(|k| p.get(*k).is_none()) {
|
|
s.request_ui("selectArea", json!({}));
|
|
return ok();
|
|
}
|
|
let [x, y, w, h] = keys.map(|k| num(p, k));
|
|
let (Some(x), Some(y), Some(w), Some(h)) = (x, y, w, h) else { return Err(bad(cmd, "pass x, y, width and height (numbers)")) };
|
|
if ![x, y, w, h].iter().all(|v| v.is_finite() && v.abs() < 1e9) {
|
|
return Err(bad(cmd, "the box is out of range"));
|
|
}
|
|
let start = figcraft_geom::Point::new(x, y);
|
|
let r = figcraft_geom::Rect::from_points(start, figcraft_geom::Point::new(x + w, y + h));
|
|
let add = boolean(p, "add") == Some(true);
|
|
let base = if add { s.selection() } else { Vec::new() };
|
|
let within = s.marquee_within(start)?;
|
|
let sel = s.marquee_selection(r, within, boolean(p, "deep") == Some(true), &base, add)?;
|
|
s.set_selection(sel.clone());
|
|
Ok(json!({"selection": sel}))
|
|
}
|
|
|
|
pub(super) fn select_children(s: &mut Session, _p: &Value) -> Result<Value> {
|
|
let doc = s.doc()?;
|
|
let mut ids = Vec::new();
|
|
for i in s.selection() {
|
|
if let Some(n) = doc.node(i) {
|
|
ids.extend(n.children.iter().copied());
|
|
}
|
|
}
|
|
if ids.is_empty() {
|
|
return Ok(json!({"selection": s.selection()}));
|
|
}
|
|
s.set_selection(ids.clone());
|
|
Ok(json!({"selection": ids}))
|
|
}
|
|
|
|
pub(super) fn select_parent(s: &mut Session, _p: &Value) -> Result<Value> {
|
|
let doc = s.doc()?;
|
|
let mut ids: Vec<NodeId> = Vec::new();
|
|
for i in s.selection() {
|
|
if let Some(p) = doc.node(i).and_then(|n| n.parent)
|
|
&& !doc.node(p).is_some_and(Node::is_page)
|
|
&& !ids.contains(&p)
|
|
{
|
|
ids.push(p);
|
|
}
|
|
}
|
|
if ids.is_empty() {
|
|
return Ok(json!({"selection": s.selection()}));
|
|
}
|
|
s.set_selection(ids.clone());
|
|
Ok(json!({"selection": ids}))
|
|
}
|
|
|
|
pub(super) fn select_sibling(s: &mut Session, dir: i64) -> Result<Value> {
|
|
let doc = s.doc()?;
|
|
let sel = s.selection();
|
|
let Some(cur) = sel.last().copied() else {
|
|
let first = doc.get(s.page()?)?.children.last().copied();
|
|
s.set_selection(first.into_iter().collect());
|
|
return Ok(json!({"selection": s.selection()}));
|
|
};
|
|
let parent = doc.get(cur)?.parent.ok_or_else(|| bad("select.next", "no parent"))?;
|
|
let kids = &doc.get(parent)?.children;
|
|
let idx = kids.iter().position(|c| *c == cur).unwrap_or(0) as i64;
|
|
let n = kids.len() as i64;
|
|
// Layers panel order is reversed (top of the list = last child): Tab goes down the list.
|
|
let next = (idx - dir).rem_euclid(n.max(1)) as usize;
|
|
let id = kids.get(next).copied().unwrap_or(cur);
|
|
s.set_selection(vec![id]);
|
|
Ok(json!({"selection": [id]}))
|
|
}
|
|
|
|
pub(super) fn select_matching(s: &mut Session, _p: &Value) -> Result<Value> {
|
|
let doc = s.doc()?;
|
|
let sel = s.selection();
|
|
let Some(first) = sel.first().and_then(|i| doc.node(*i)) else { return Ok(json!({"selection": []})) };
|
|
let (name, ty) = (first.name.clone(), first.kind.type_name());
|
|
let page = s.page()?;
|
|
let ids: Vec<NodeId> =
|
|
doc.subtree(page).into_iter().filter(|i| doc.node(*i).is_some_and(|n| n.name == name && n.kind.type_name() == ty)).collect();
|
|
s.set_selection(ids.clone());
|
|
Ok(json!({"selection": ids}))
|
|
}
|
|
|
|
pub(super) fn select_same(s: &mut Session, same: impl Fn(&Node, &Node) -> bool) -> Result<Value> {
|
|
let doc = s.doc()?;
|
|
let sel = s.selection();
|
|
let Some(first) = sel.first().and_then(|i| doc.node(*i)).cloned() else { return Ok(json!({"selection": []})) };
|
|
let page = s.page()?;
|
|
let ids: Vec<NodeId> = doc.subtree(page).into_iter().filter(|i| *i != page && doc.node(*i).is_some_and(|n| same(&first, n))).collect();
|
|
s.set_selection(ids.clone());
|
|
Ok(json!({"selection": ids}))
|
|
}
|
|
|
|
/// The opacity number keys give: one digit is tens of percent (0 is 100%), two digits typed in
|
|
/// quick succession the exact percentage.
|
|
pub fn opacity_from_digits(digits: &str) -> Option<f32> {
|
|
let mut n = 0u32;
|
|
let mut count = 0;
|
|
for c in digits.chars() {
|
|
// Input-derived: a number that outgrows `u32` stops the scan (None); it never wraps.
|
|
n = n.checked_mul(10)?.checked_add(c.to_digit(10)?)?;
|
|
count += 1;
|
|
}
|
|
match count {
|
|
1 if n == 0 => Some(1.0),
|
|
1 => Some(n as f32 / 10.0),
|
|
2 => Some(n as f32 / 100.0),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub(super) fn opacity_keys(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let digits = req_string("object.opacityKeys", p, "digits")?;
|
|
let o = opacity_from_digits(&digits).ok_or_else(|| bad("object.opacityKeys", "`digits` must be one or two digits"))?;
|
|
let ids = req_targets("object.opacityKeys", s, p)?;
|
|
let doc = s.doc_mut()?;
|
|
let before = crate::cmd::styles::bound_before(doc, &ids);
|
|
for i in ids {
|
|
doc.get_mut(i)?.opacity = o;
|
|
}
|
|
crate::cmd::styles::detach_edited(doc, &before);
|
|
Ok(json!({"opacity": o}))
|
|
}
|
|
|
|
pub(super) fn nudge(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let big = boolean(p, "big").unwrap_or(false);
|
|
let k = if big { s.prefs.nudge_big } else { s.prefs.nudge_small };
|
|
let (ux, uy) = (num_or(p, "dx", 0.0), num_or(p, "dy", 0.0));
|
|
let (dx, dy) = (ux * k, uy * k);
|
|
let ids = req_targets("transform.nudge", s, p)?;
|
|
// Edit a copy and keep it only if something moved, so an arrow that can't move anything adds
|
|
// no undo step.
|
|
let mut work = s.doc()?.clone();
|
|
let doc = &mut work;
|
|
let ids = roots(doc, &ids);
|
|
let mut changed = false;
|
|
// Children in an auto layout flow move one place instead (see `al_keys`).
|
|
for i in &ids {
|
|
if crate::al_keys::flowing_parent(doc, *i).is_none() && (dx, dy) != (0.0, 0.0) {
|
|
let n = doc.get_mut(*i)?;
|
|
n.transform = Affine::translate((dx, dy)) * n.transform;
|
|
changed = true;
|
|
}
|
|
}
|
|
for dir in crate::al_keys::Dir::from_units(ux, uy) {
|
|
// Grouped per arrow: a move into a neighbouring frame changes the parent.
|
|
let mut groups: BTreeMap<NodeId, Vec<NodeId>> = BTreeMap::new();
|
|
for i in &ids {
|
|
if let Some(parent) = crate::al_keys::flowing_parent(doc, *i) {
|
|
groups.entry(parent).or_default().push(*i);
|
|
}
|
|
}
|
|
for (parent, block) in groups {
|
|
changed |= crate::al_keys::arrow_move(doc, parent, &block, dir)?;
|
|
}
|
|
}
|
|
if changed {
|
|
s.ds_mut()?.doc = std::sync::Arc::new(work);
|
|
}
|
|
ok()
|
|
}
|
|
|
|
pub(super) fn nudge_size(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let big = boolean(p, "big").unwrap_or(false);
|
|
let k = if big { s.prefs.nudge_big } else { s.prefs.nudge_small };
|
|
let (dw, dh) = (num_or(p, "dw", 0.0) * k, num_or(p, "dh", 0.0) * k);
|
|
let ids = req_targets("transform.nudgeSize", s, p)?;
|
|
let ids = roots(s.doc()?, &ids);
|
|
for i in ids {
|
|
let Some((w, h)) = s.doc()?.node(i).map(|n| (n.width, n.height)) else { continue };
|
|
let mut q = serde_json::Map::new();
|
|
q.insert("ids".into(), json!([i]));
|
|
// Only the axis that changes becomes fixed.
|
|
if dw != 0.0 {
|
|
q.insert("width".into(), json!((w + dw).max(0.0)));
|
|
}
|
|
if dh != 0.0 {
|
|
q.insert("height".into(), json!((h + dh).max(0.0)));
|
|
}
|
|
crate::cmd::arrange::resize_cmd(s, &Value::Object(q))?;
|
|
}
|
|
ok()
|
|
}
|
|
|
|
pub(super) fn toggle(s: &mut Session, p: &Value, what: &str) -> Result<Value> {
|
|
let ids = req_targets("object.toggle", s, p)?;
|
|
let doc = s.doc_mut()?;
|
|
let explicit = boolean(p, what);
|
|
// Figma: if any is visible, hide all (and vice versa).
|
|
let value = explicit.unwrap_or_else(|| match what {
|
|
"visible" => !ids.iter().all(|i| doc.node(*i).is_some_and(|n| n.visible)),
|
|
_ => !ids.iter().all(|i| doc.node(*i).is_some_and(|n| n.locked)),
|
|
});
|
|
for i in &ids {
|
|
let n = doc.get_mut(*i)?;
|
|
if what == "visible" {
|
|
n.visible = value;
|
|
} else {
|
|
n.locked = value;
|
|
}
|
|
}
|
|
if what == "locked" && value {
|
|
s.set_selection(vec![]);
|
|
}
|
|
Ok(json!({what: value}))
|
|
}
|
|
|
|
pub(super) fn hide_others(s: &mut Session, _p: &Value) -> Result<Value> {
|
|
let sel = s.selection();
|
|
let page = s.page()?;
|
|
let doc = s.doc_mut()?;
|
|
let keep: BTreeSet<NodeId> = sel
|
|
.iter()
|
|
.flat_map(|i| {
|
|
let mut v = doc.ancestors(*i);
|
|
v.push(*i);
|
|
v
|
|
})
|
|
.collect();
|
|
let tops: Vec<NodeId> = doc.get(page)?.children.clone();
|
|
for t in tops {
|
|
if !keep.contains(&t) {
|
|
doc.get_mut(t)?.visible = false;
|
|
}
|
|
}
|
|
ok()
|
|
}
|