Skip to main content

blockworx_editor/edit/
embed.rs

1//! Embedding: another document, grafted in as a block of this one.
2//!
3//! The model already has the shape. A block's pins *are* its interior's
4//! sheet ports — [`Pin::rect`](blockworx_doc::block_model::Pin) is the body
5//! inside the block's own view, `slot` places the same pin on the
6//! block-as-child — and a root-level port is a pin owned by
7//! [`BlockId::NULL`]. So a document's root scope, re-pointed at a fresh
8//! block, arrives with its ports already that block's pins, in the interior
9//! it was drawn in.
10//!
11//! A copy, not a reference: nothing records where the block came from, and
12//! editing the source afterwards does not touch it. Which is also why the
13//! ids are re-minted — the arriving document numbered them, and this one
14//! never issued them (`docs/log-vs-snapshot.md` §5.1).
15
16use 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/// One embed: the document arriving, the name its block takes, and where in
29/// this document it lands.
30#[derive(Clone, Copy, Debug)]
31pub struct Embed<'a> {
32    pub source: &'a Document,
33    /// What the block is called — the document's own name, which the
34    /// arriving drawing does not carry.
35    pub title: &'a str,
36    pub into: Scope,
37    pub at: Pos2,
38}
39
40/// Inventory row "Embed": a document becomes a block of this one.
41///
42/// Returns the block, which is what the caller selects — the graft inside it
43/// is left unnamed, as a paste leaves the structure that travelled inside
44/// its roots.
45pub 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    // Built from this very document a line ago, so the pairing holds; an
75    // embed of nothing is still an empty block, which is what a document
76    // with nothing at its root should arrive as.
77    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
91/// Everything standing at a document's root, as copy targets. The closure
92/// the copy walks pulls in each block's subtree and the wires adjacent to
93/// what it carries, so this lists only what the root holds directly.
94fn 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    /// A document with something at every level: two ports on its own
119    /// boundary, a block with two wired pins inside it, and an annotation.
120    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    /// Embed `source` into `doc` at its root, and fold.
138    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    /// The arriving document numbered its own entities; this one never
225    /// issued those numbers, so an embed that kept them would collide with
226    /// what is already standing.
227    #[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}