Skip to main content

blockworx_editor/edit/
mod.rs

1//! Pure op emitters.
2//!
3//! One emitter per row of `docs/document_mutations.md`: a pure function from
4//! `(indexed document, parameters)` to the primitive ops the gesture produced,
5//! pushed through a [`CommitBuilder`](blockworx_doc::commit::CommitBuilder).
6//! Anything the gesture *measured or solved* — text widths, re-routed
7//! polylines — arrives as a parameter; anything derivable by list arithmetic
8//! — free slots, block growth, trims, cascades — is emitter work. Emitters
9//! filter non-edits (an update equal to what the document holds pushes
10//! nothing, so a no-op gesture seals to no commit), push nothing at absent
11//! targets, and never pre-validate what the fold refuses — `try_apply`
12//! stays the authority.
13//!
14//! That authority covers *edits* alone: a history step adopts the document a
15//! rev was written as rather than folding its way back to it, so the fold
16//! validates what a session writes and the trail then adopts what the fold
17//! already accepted. The one invariant `validate` enforces that no emitter
18//! restates is `dangling_endpoints` — a pin may not leave under a route —
19//! and it is unreachable from here because `delete.rs`'s cascade emits
20//! every dependent route delete itself, wires owned outside the deleted
21//! subtree included
22//! (`delete::tests::deleting_a_block_takes_its_subtree_and_leaves_the_sibling_standing`).
23
24pub mod assets;
25pub mod clipboard;
26pub mod create;
27pub mod delete;
28pub mod describe;
29pub mod embed;
30pub mod geometry;
31pub mod lock;
32pub mod lower;
33pub mod naming;
34
35#[cfg(test)]
36pub(crate) mod harness {
37    use crate::path::Scope;
38    use blockworx_doc::{
39        block_model::{Area, Block, Icon, Image, Label, Pin, Route, RouteLabel, Text},
40        commit::{Commit, CommitBuilder},
41        document::Document,
42        fixtures::{area_id, block_id, image_id, pin_id, route_id, route_label_id, text_id},
43        geometry::{FracVal, GridPoint, GridRect, PinSlot, ScreenRect},
44        hash::AssetHash,
45        opcode::{Crud, OpCodes},
46        values::{LabelSide, PinDir, Role},
47    };
48
49    /// Seal the gesture and fold it, panicking on refusal — the shape every
50    /// emitter test shares. A gesture of pure non-edits is asserted at the
51    /// call site with [`seals_to_nothing`], not here.
52    pub fn fold(builder: CommitBuilder, doc: &Document) -> Document {
53        let commit = builder.seal().expect("the gesture produced ops");
54        doc.try_apply(&commit)
55            .expect("the fold accepts the gesture")
56    }
57
58    /// The non-edit assertion: the emitter left the builder empty.
59    pub fn seals_to_nothing(builder: CommitBuilder) {
60        assert!(
61            builder.seal().is_none(),
62            "a non-edit gesture must push nothing"
63        );
64    }
65
66    pub fn label_init(name: &str) -> Label {
67        Label {
68            name: name.into(),
69            side: LabelSide::default(),
70            offset: FracVal::default(),
71            hidden: false,
72        }
73    }
74
75    pub fn block_create(n: u32) -> OpCodes {
76        OpCodes::Block(
77            block_id(n),
78            Crud::Create(Block {
79                parent: Scope::Root.wire_id(),
80                rect: GridRect::default(),
81                locked: false,
82                role: Role::default(),
83                title: label_init(&format!("b{n}")),
84                type_label: label_init(""),
85                icon: Icon::default(),
86            }),
87        )
88    }
89
90    pub fn pin_create(n: u32, owner: u32) -> OpCodes {
91        OpCodes::Pin(
92            pin_id(n),
93            Crud::Create(Pin {
94                owner: block_id(owner),
95                name: format!("p{n}"),
96                type_name: String::new(),
97                tag: String::new(),
98                tag_hidden: false,
99                rect: GridRect::default(),
100                slot: PinSlot::default(),
101                dir: PinDir::default(),
102                port_accent: Role::default(),
103                flip_lr: false,
104            }),
105        )
106    }
107
108    pub fn route_create(n: u32, owner: u32, from: u32, to: u32) -> OpCodes {
109        OpCodes::Route(
110            route_id(n),
111            Crud::Create(Route {
112                owner: block_id(owner),
113                name: format!("r{n}"),
114                from: pin_id(from),
115                to: pin_id(to),
116                role: Role::default(),
117                waypoints: Vec::new(),
118            }),
119        )
120    }
121
122    pub fn route_label_create(n: u32, owner: u32) -> OpCodes {
123        OpCodes::RouteLabel(
124            route_label_id(n),
125            Crud::Create(RouteLabel {
126                owner: route_id(owner),
127                pos: FracVal::default(),
128            }),
129        )
130    }
131
132    pub fn text_create(n: u32, owner: u32) -> OpCodes {
133        OpCodes::Text(
134            text_id(n),
135            Crud::Create(Text {
136                owner: block_id(owner),
137                text: format!("t{n}"),
138                pos: GridPoint::default(),
139                role: Role::default(),
140                width: None,
141            }),
142        )
143    }
144
145    /// A placed image with no payload — the asset op lands in 10e; every
146    /// row before it only ever moves the box.
147    pub fn image_create(n: u32, owner: u32) -> OpCodes {
148        OpCodes::Image(
149            image_id(n),
150            Crud::Create(Image {
151                owner: block_id(owner),
152                asset: AssetHash::default(),
153                rect: ScreenRect::default(),
154            }),
155        )
156    }
157
158    pub fn area_create(n: u32, owner: u32) -> OpCodes {
159        OpCodes::Area(
160            area_id(n),
161            Crud::Create(Area {
162                owner: block_id(owner),
163                rect: GridRect::default(),
164                role: Role::default(),
165                title: label_init(&format!("c{n}")),
166            }),
167        )
168    }
169
170    /// The shared scene: block 1 at top level holding pins 3 and 4, route 5
171    /// between them, text 7, and area 8 — as a prediction, which is what
172    /// every emitter reads.
173    pub fn wired() -> Document {
174        Document::default()
175            .try_apply(&Commit::new(
176                "Wired a scene".into(),
177                vec![
178                    block_create(1),
179                    pin_create(3, 1),
180                    pin_create(4, 1),
181                    route_create(5, 1, 3, 4),
182                    text_create(7, 1),
183                    area_create(8, 1),
184                ],
185            ))
186            .expect("the scene folds")
187    }
188}