dotloom_document/
clipboard.rs1use std::collections::{BTreeMap, BTreeSet};
15
16use dotloom_geometry::{Affine, Vector};
17use serde::{Deserialize, Serialize};
18
19use crate::{Constraint, ConstraintId, Document, Entity, EntityId, Group, GroupId, LayerId, PropValue, RuleSpec};
20
21#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
23pub struct Clipboard {
24 pub entities: Vec<Entity>,
26 pub constraints: Vec<Constraint>,
28 pub groups: Vec<Group>,
30}
31
32#[derive(Debug, Clone, Default, PartialEq, Eq)]
34pub struct PasteReport {
35 pub entities: BTreeMap<EntityId, EntityId>,
37 pub constraints: Vec<ConstraintId>,
39 pub groups: Vec<GroupId>,
41 pub dropped_refs: Vec<(EntityId, String)>,
43}
44
45impl Document {
46 #[must_use]
48 pub fn copy(&self, ids: &[EntityId]) -> Clipboard {
49 let set: BTreeSet<EntityId> = ids.iter().copied().filter(|id| self.entity(*id).is_some()).collect();
50 let entities: Vec<Entity> = self.entities().filter(|e| set.contains(&e.id)).cloned().collect();
51 let constraints = self
52 .constraints()
53 .filter(|c| {
54 let ents = c.rule.entities();
55 !ents.is_empty() && ents.iter().all(|e| set.contains(e)) && c.owner.is_none_or(|o| set.contains(&o))
56 })
57 .cloned()
58 .collect();
59 let groups = self
60 .groups()
61 .filter(|g| !g.members.is_empty() && g.members.iter().all(|m| set.contains(m)))
62 .cloned()
63 .collect();
64 Clipboard { entities, constraints, groups }
65 }
66
67 pub fn paste(&mut self, clip: &Clipboard, offset: Vector, fallback_layer: LayerId) -> PasteReport {
70 let mut report = PasteReport::default();
71 for e in &clip.entities {
72 let new = EntityId(self.alloc_id());
73 report.entities.insert(e.id, new);
74 }
75 let t = Affine::translate(offset);
76 for e in &clip.entities {
77 let Some(&new_id) = report.entities.get(&e.id) else { continue };
78 let mut n = e.clone();
79 n.id = new_id;
80 if self.layer(n.layer).is_none() {
81 n.layer = fallback_layer;
82 }
83 n.transform = n.transform.then(t);
84 let mut dropped = Vec::new();
85 for (k, v) in &mut n.props {
86 match v {
87 PropValue::Ref(r) => {
88 if let Some(m) = report.entities.get(&r.entity) {
89 r.entity = *m;
90 } else if self.entity(r.entity).is_none() {
91 dropped.push(k.clone());
92 }
93 }
94 PropValue::Anchor(a) => {
95 if let Some(m) = report.entities.get(&a.entity) {
96 a.entity = *m;
97 } else if self.entity(a.entity).is_none() {
98 dropped.push(k.clone());
99 }
100 }
101 _ => {}
102 }
103 }
104 for k in dropped {
105 n.props.remove(&k);
106 report.dropped_refs.push((new_id, k));
107 }
108 self.insert_entity(n);
109 }
110 for c in &clip.constraints {
111 let mut n = c.clone();
112 if !n.rule.remap(&report.entities) {
113 continue;
114 }
115 if let Some(o) = n.owner {
116 n.owner = report.entities.get(&o).copied();
117 }
118 if let RuleSpec::FixPoint { at, .. } = &mut n.rule {
119 *at += offset;
120 }
121 n.id = ConstraintId(self.alloc_id());
122 report.constraints.push(n.id);
123 self.upsert_constraint(n);
124 }
125 for g in &clip.groups {
126 let mut n = g.clone();
127 n.id = GroupId(self.alloc_id());
128 n.members = g.members.iter().filter_map(|m| report.entities.get(m).copied()).collect();
129 n.children.clear();
130 report.groups.push(n.id);
131 self.upsert_group(n);
132 }
133 report
134 }
135}