1use blockworx_doc::{
11 block_model::Label,
12 commit::Commit,
13 document::{DocIndex, Document},
14 geometry::GridVec,
15 id::{BlockId, PinId},
16 opcode::{Crud, OpCodes},
17};
18use blockworx_geom::Pos2;
19use blockworx_store::doc::DocumentNonce;
20
21use crate::grid::grid_point;
22use crate::{
23 edit::{
24 clipboard::{Clipboard, Paste, PasteTarget, PinPaste, Snapshot},
25 delete::Target,
26 geometry::Shape,
27 },
28 shape::ShapeId,
29 widget::drawing::Drawing,
30};
31
32const UNTARGETED_OFFSET: GridVec = GridVec::new(2, 2);
35
36pub fn is_object_clipboard(text: &str) -> bool {
40 Clipboard::from_json(text).is_some() || crate::import::from_clipboard(text).is_some()
41}
42
43fn copy_target(id: ShapeId) -> Option<Target> {
46 Some(match id {
47 ShapeId::Rect(id) => Target::Block(id),
48 ShapeId::Port(id) => Target::Pin(id),
49 ShapeId::Text(id) => Target::Text(id),
50 ShapeId::Area(id) => Target::Area(id),
51 ShapeId::Image(id) => Target::Image(id),
52 ShapeId::Icon(_) => return None,
53 })
54}
55
56fn pasted_shape(shape: Shape) -> ShapeId {
58 match shape {
59 Shape::Block(id) => ShapeId::Rect(id),
60 Shape::Port(id) => ShapeId::Port(id),
61 Shape::Text(id) => ShapeId::Text(id),
62 Shape::Area(id) => ShapeId::Area(id),
63 Shape::Image(id) => ShapeId::Image(id),
64 Shape::Icon(id) => ShapeId::Icon(id),
65 }
66}
67
68impl Drawing<'_> {
69 pub fn copy_selection(&self, shapes: &[ShapeId]) -> Option<Clipboard> {
73 let targets: Vec<Target> = shapes.iter().filter_map(|&id| copy_target(id)).collect();
74 let clip = crate::edit::clipboard::copy(&self.indexed(), &targets);
75 (!clip.snapshot().is_empty()).then_some(clip)
76 }
77
78 pub fn copy_pins(&self, pins: &[PinId]) -> Option<Clipboard> {
81 let targets: Vec<Target> = pins.iter().copied().map(Target::Pin).collect();
82 let clip = crate::edit::clipboard::copy(&self.indexed(), &targets);
83 (!clip.snapshot().pins.is_empty()).then(|| clip.into_pins())
84 }
85
86 pub fn cut_selection(&mut self, shapes: &[ShapeId], from: DocumentNonce) -> Option<Clipboard> {
93 let targets: Vec<Target> = shapes.iter().filter_map(|&id| copy_target(id)).collect();
94 let mut clip = None;
95 self.author("cut_selection", |indexed, sink| {
96 clip = Some(crate::edit::clipboard::cut(indexed, &targets, from, sink));
97 });
98 clip.filter(|clip| !clip.snapshot().is_empty())
99 }
100
101 pub fn cut_pins(&mut self, pins: &[PinId], from: DocumentNonce) -> Option<Clipboard> {
103 let targets: Vec<Target> = pins.iter().copied().map(Target::Pin).collect();
104 let mut clip = None;
105 self.author("cut_pins", |indexed, sink| {
106 clip = Some(crate::edit::clipboard::cut(indexed, &targets, from, sink));
107 });
108 clip.filter(|clip| !clip.snapshot().pins.is_empty())
109 .map(Clipboard::into_pins)
110 }
111
112 pub fn paste_snapshot(
123 &mut self,
124 clip: &Clipboard,
125 into: DocumentNonce,
126 target_top_left: Option<Pos2>,
127 ) -> Vec<ShapeId> {
128 let snapshot = clip.snapshot();
129 let offset = match (target_top_left, snapshot.origin()) {
130 (Some(target), Some(origin)) => {
131 let target = grid_point(target);
132 GridVec::new(target.x - origin.x, target.y - origin.y)
133 }
134 _ => UNTARGETED_OFFSET,
135 };
136 let scope = self.current_scope();
137 let mut ids = self.ids();
138 let mut roots = Vec::new();
139 self.author("paste", |indexed, sink| {
140 roots = crate::edit::clipboard::paste(
141 indexed,
142 Paste {
143 clip,
144 into,
145 target: PasteTarget { scope, offset },
146 },
147 &mut ids,
148 sink,
149 );
150 });
151 roots.into_iter().map(pasted_shape).collect()
152 }
153
154 pub fn paste_pins(&mut self, snapshot: &Snapshot, owner: crate::path::Scope) -> Vec<PinId> {
157 let mut ids = self.ids();
158 let mut pasted = Vec::new();
159 self.author("paste_pins", |indexed, sink| {
160 pasted = crate::edit::clipboard::paste_pins(
161 indexed,
162 PinPaste {
163 pins: &snapshot.pins,
164 owner,
165 },
166 &mut ids,
167 sink,
168 );
169 });
170 pasted
171 }
172}
173
174pub fn block_from_document(parsed: &Document, title: &str) -> Option<Clipboard> {
189 let mut ops = parsed.creating_commit("Imported a diagram")?.ops().to_vec();
190 retitle_outermost(
191 &mut ops,
192 outermost_ancestor(parsed, parsed.title_block().top),
193 title,
194 );
195 let doc = Document::default()
196 .try_apply(&Commit::new("Imported a diagram".into(), ops))
197 .ok()?;
198 let mut index = DocIndex::of(&doc);
199 let indexed = index.view(&doc);
200 let clip = crate::edit::clipboard::copy(&indexed, &root_contents(&indexed));
201 (!clip.snapshot().is_empty()).then_some(clip)
202}
203
204fn root_contents(indexed: &blockworx_doc::document::IndexedDocument<'_>) -> Vec<Target> {
209 let Some(root) = indexed.index.scope(BlockId::NULL) else {
210 return Vec::new();
211 };
212 if !root.pins.is_empty() {
213 tracing::warn!(
214 "{} boundary port(s) of the inserted diagram have no boundary to join",
215 root.pins.len(),
216 );
217 }
218 root.children
219 .iter()
220 .map(|&id| Target::Block(id))
221 .chain(root.texts.iter().map(|&id| Target::Text(id)))
222 .chain(root.areas.iter().map(|&id| Target::Area(id)))
223 .chain(root.images.iter().map(|&id| Target::Image(id)))
224 .collect()
225}
226
227fn retitle_outermost(ops: &mut [OpCodes], outermost: BlockId, title: &str) {
232 for op in ops {
233 if let OpCodes::Block(id, Crud::Create(block)) = op
234 && *id == outermost
235 {
236 block.title = Label {
237 name: title.to_owned(),
238 ..Label::default()
239 };
240 }
241 }
242}
243
244fn outermost_ancestor(parsed: &Document, id: BlockId) -> BlockId {
247 let mut current = id;
248 for _ in 0..parsed.blocks().count() {
249 match parsed.block(¤t).map(|block| block.parent) {
250 Some(parent) if parent != BlockId::NULL => current = parent,
251 _ => break,
252 }
253 }
254 current
255}