use glam::DVec2;
use crate::align::{self, AlignAnchor, AlignOp};
use renamite_animation::Frame;
use renamite_history::{EditorCommand, resolve_property_edit};
use renamite_model::{Document, NodeId, PropPath, Value, evaluate, world_delta_to_parent};
use crate::pathfinder::{geometric_target, selection_roots};
const POSITION: &str = "transform.position";
const SCALE: &str = "transform.scale";
const EPS_SQ: f64 = 1e-24;
fn resolve_targets(doc: &Document, ids: &[NodeId]) -> Vec<Option<NodeId>> {
ids.iter().map(|&id| geometric_target(doc, id)).collect()
}
pub fn move_commands(
doc: &Document,
ids: &[NodeId],
frame: f64,
record: bool,
deltas: &[(NodeId, DVec2)],
) -> Vec<EditorCommand> {
let targets: Vec<Option<NodeId>> = ids.iter().map(|&id| geometric_target(doc, id)).collect();
let target_of: std::collections::HashMap<NodeId, Option<NodeId>> =
ids.iter().copied().zip(targets).collect();
let prop = PropPath::new(POSITION);
let mut cmds = Vec::new();
for (root, delta) in deltas {
if !delta.is_finite() || delta.length_squared() < EPS_SQ {
continue;
}
let Some(Some(id)) = target_of.get(root) else {
continue;
};
if doc.nodes.get(*id).map(|n| n.locked).unwrap_or(true) {
continue;
}
let Ok(Value::DVec2(current)) = doc.value_at(*id, &prop, frame) else {
continue;
};
let local = world_delta_to_parent(doc, *id, frame, *delta).unwrap_or(*delta);
if !local.is_finite() {
continue;
}
cmds.push(resolve_property_edit(
doc,
*id,
&prop,
Value::DVec2(current + local),
Frame(frame.round() as i64),
record,
));
}
cmds
}
pub fn align(
doc: &Document,
comp: renamite_model::CompId,
selection: &[NodeId],
frame: f64,
record: bool,
op: AlignOp,
anchor: AlignAnchor,
) -> Option<Vec<EditorCommand>> {
let roots = selection_roots(doc, selection);
if roots.is_empty() {
return None;
}
let scene = evaluate(doc, comp, frame);
let bounds = align::root_bounds(doc, &scene.items, &roots);
if bounds.is_empty() {
return None;
}
let target = match anchor {
AlignAnchor::Selection => align::union_bounds(&bounds)?,
AlignAnchor::Page => {
let size = doc
.main_composition()
.map(|composition| composition.size)
.unwrap_or((512, 512));
align::page_bounds(size)
}
};
let cmds = move_commands(
doc,
&roots,
frame,
record,
&align::align_deltas(&bounds, target, op),
);
(!cmds.is_empty()).then_some(cmds)
}
pub fn distribute(
doc: &Document,
comp: renamite_model::CompId,
selection: &[NodeId],
frame: f64,
record: bool,
horizontal: bool,
) -> Option<Vec<EditorCommand>> {
let roots = selection_roots(doc, selection);
if roots.len() < 3 {
return None;
}
let scene = evaluate(doc, comp, frame);
let bounds = align::root_bounds(doc, &scene.items, &roots);
if bounds.len() < 3 {
return None;
}
let deltas = align::distribute_deltas(&bounds, horizontal)?;
let cmds = move_commands(doc, &roots, frame, record, &deltas);
(!cmds.is_empty()).then_some(cmds)
}
pub fn flip(
doc: &Document,
selection: &[NodeId],
frame: f64,
record: bool,
horizontal: bool,
) -> Option<Vec<EditorCommand>> {
let roots = selection_roots(doc, selection);
if roots.is_empty() {
return None;
}
let targets = resolve_targets(doc, &roots);
let prop = PropPath::new(SCALE);
let mut cmds = Vec::new();
for target in targets {
let Some(id) = target else {
continue;
};
if doc.nodes.get(id).map(|n| n.locked).unwrap_or(true) {
continue;
}
let Ok(Value::DVec2(current)) = doc.value_at(id, &prop, frame) else {
continue;
};
let next = if horizontal {
DVec2::new(-current.x, current.y)
} else {
DVec2::new(current.x, -current.y)
};
if !next.is_finite() {
continue;
}
cmds.push(resolve_property_edit(
doc,
id,
&prop,
Value::DVec2(next),
Frame(frame.round() as i64),
record,
));
}
(!cmds.is_empty()).then_some(cmds)
}
pub fn nudge(
doc: &Document,
selection: &[NodeId],
frame: f64,
record: bool,
delta: DVec2,
) -> Option<Vec<EditorCommand>> {
if !delta.is_finite() {
return None;
}
let roots = selection_roots(doc, selection);
if roots.is_empty() {
return None;
}
let targets = resolve_targets(doc, &roots);
let prop = PropPath::new(POSITION);
let mut cmds = Vec::new();
for target in targets {
let Some(id) = target else {
continue;
};
if doc.nodes.get(id).map(|n| n.locked).unwrap_or(true) {
continue;
}
let Ok(Value::DVec2(current)) = doc.value_at(id, &prop, frame) else {
continue;
};
let local = world_delta_to_parent(doc, id, frame, delta).unwrap_or(delta);
if !local.is_finite() {
continue;
}
cmds.push(resolve_property_edit(
doc,
id,
&prop,
Value::DVec2(current + local),
Frame(frame.round() as i64),
record,
));
}
(!cmds.is_empty()).then_some(cmds)
}