1use blockworx_doc::{
17 block_model::Block,
18 commit::CommitBuilder,
19 document::{DocIndex, Document, IndexedDocument},
20 id::{Allocator, BlockId},
21 opcode::{Crud, OpCodes},
22};
23use blockworx_geom::Pos2;
24
25use crate::edit::{clipboard, create, delete::Target};
26use crate::path::Scope;
27
28#[derive(Clone, Copy, Debug)]
31pub struct Embed<'a> {
32 pub source: &'a Document,
33 pub title: &'a str,
36 pub into: Scope,
37 pub at: Pos2,
38}
39
40pub fn embed(
46 indexed: &IndexedDocument<'_>,
47 embed: Embed<'_>,
48 fresh: &mut Allocator,
49 builder: &mut CommitBuilder,
50) -> BlockId {
51 let Embed {
52 source,
53 title,
54 into: scope,
55 at,
56 } = embed;
57 let id = fresh.mint();
58 let stamped = create::stamped_block(at);
59 let block = Block {
60 title: create::block_title(title),
61 ..create::block_value(
62 indexed.doc,
63 create::NewBlock {
64 id,
65 scope,
66 start: stamped.min,
67 end: stamped.max,
68 },
69 )
70 };
71 builder.push(OpCodes::Block(id, Crud::Create(block.clone())));
72
73 let index = DocIndex::of(source);
74 let Some(arriving) = index.view_of(source) else {
78 return id;
79 };
80 let clip = clipboard::copy(&arriving, &root_of(&arriving));
81 clipboard::paste_into_fresh(
82 indexed,
83 &clip,
84 clipboard::FreshScope { id, block: &block },
85 fresh,
86 builder,
87 );
88 id
89}
90
91fn root_of(indexed: &IndexedDocument<'_>) -> Vec<Target> {
95 let Some(root) = indexed.index.scope(BlockId::NULL) else {
96 return Vec::new();
97 };
98 root.children
99 .iter()
100 .map(|&id| Target::Block(id))
101 .chain(root.pins.iter().map(|&id| Target::Pin(id)))
102 .chain(root.texts.iter().map(|&id| Target::Text(id)))
103 .chain(root.areas.iter().map(|&id| Target::Area(id)))
104 .chain(root.images.iter().map(|&id| Target::Image(id)))
105 .chain(root.routes.iter().map(|&id| Target::Route(id)))
106 .collect()
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::edit::harness::{block_create, fold, pin_create, route_create, text_create};
113 use blockworx_doc::{
114 commit::Commit,
115 fixtures::{block_id, pin_id},
116 };
117
118 fn arriving() -> Document {
121 Document::default()
122 .try_apply(&Commit::new(
123 "Drew a sheet".into(),
124 vec![
125 block_create(1),
126 pin_create(3, 1),
127 pin_create(4, 1),
128 route_create(5, 1, 3, 4),
129 text_create(7, 1),
130 pin_create(10, 0),
131 pin_create(11, 0),
132 ],
133 ))
134 .expect("the sheet folds")
135 }
136
137 fn embedded(doc: &Document, source: &Document, title: &str) -> (Document, BlockId) {
139 let index = DocIndex::of(doc);
140 let indexed = index.view_of(doc).expect("the index is this document's");
141 let mut builder = CommitBuilder::new(String::new());
142 let mut fresh = doc.ids();
143 let id = embed(
144 &indexed,
145 Embed {
146 source,
147 title,
148 into: Scope::Root,
149 at: Pos2::ZERO,
150 },
151 &mut fresh,
152 &mut builder,
153 );
154 (fold(builder, doc), id)
155 }
156
157 #[test]
158 fn a_documents_own_ports_become_the_pins_of_the_block_it_embeds_as() {
159 let source = arriving();
160 let root_ports: Vec<_> = source
161 .pins()
162 .filter(|(_, pin)| pin.owner == BlockId::NULL)
163 .map(|(id, _)| id)
164 .collect();
165 assert_eq!(
166 root_ports.len(),
167 2,
168 "precondition: the arriving document has ports on its own boundary, \
169 which is what this is about",
170 );
171
172 let (doc, id) = embedded(&Document::default(), &source, "engine");
173
174 let block = doc.block(&id).expect("the embed left a block");
175 assert_eq!(
176 block.parent,
177 BlockId::NULL,
178 "it lands in the scope it was told"
179 );
180 assert_eq!(block.title.name, "engine");
181
182 let pins: Vec<_> = doc.pins().filter(|(_, pin)| pin.owner == id).collect();
183 assert_eq!(
184 pins.len(),
185 2,
186 "the document's two ports are the block's two pins",
187 );
188 let slots: std::collections::HashSet<_> = pins.iter().map(|(_, pin)| pin.slot).collect();
189 assert_eq!(
190 slots.len(),
191 2,
192 "each took a slot of its own on the boundary"
193 );
194 }
195
196 #[test]
197 fn what_stood_at_the_arriving_documents_root_stands_inside_the_block() {
198 let source = arriving();
199 let (doc, id) = embedded(&Document::default(), &source, "engine");
200
201 let children: Vec<_> = doc
202 .blocks()
203 .filter(|(_, block)| block.parent == id)
204 .collect();
205 assert_eq!(
206 children.len(),
207 1,
208 "the sheet's one block is now the block's"
209 );
210 let inner = children[0].0;
211 assert_eq!(
212 doc.pins().filter(|(_, pin)| pin.owner == inner).count(),
213 2,
214 "and it kept the pins it was drawn with",
215 );
216 assert_eq!(doc.routes().count(), 1, "the wire between them travelled");
217 assert_eq!(
218 doc.texts().filter(|(_, text)| text.owner == inner).count(),
219 1,
220 "and so did the annotation it owned",
221 );
222 }
223
224 #[test]
228 fn an_embed_re_mints_rather_than_bringing_foreign_ids_in() {
229 let standing = Document::default()
230 .try_apply(&Commit::new(
231 "Drew a block".into(),
232 vec![block_create(1), pin_create(10, 1)],
233 ))
234 .expect("the block folds");
235 let source = arriving();
236 assert!(
237 standing.block(&block_id(1)).is_some() && source.block(&block_id(1)).is_some(),
238 "precondition: both documents number a block 1, so keeping ids would collide",
239 );
240
241 let (doc, id) = embedded(&standing, &source, "engine");
242
243 assert_eq!(
244 doc.blocks().count(),
245 3,
246 "the block that stood, the one embedded, and the one inside it",
247 );
248 assert_eq!(
249 doc.block(&block_id(1))
250 .expect("the block that stood")
251 .parent,
252 BlockId::NULL,
253 "what was already here was not re-pointed at the embed",
254 );
255 assert_ne!(id, block_id(1));
256 assert!(
257 doc.pin(&pin_id(10)).expect("the pin that stood").owner == block_id(1),
258 "nor was its pin taken over by the arriving port that shared its number",
259 );
260 }
261
262 #[test]
263 fn an_empty_document_embeds_as_an_empty_block() {
264 let (doc, id) = embedded(&Document::default(), &Document::default(), "blank");
265
266 assert!(doc.block(&id).is_some(), "the block is still made");
267 assert_eq!(doc.blocks().count(), 1);
268 assert_eq!(doc.pins().count(), 0);
269 }
270}