brep_app/document/ecad.rs
1//! The eCAD workbenches' documents, kept inside the BREP document.
2//!
3//! Each eCAD editor edits a copy of one top-level block of its tab's document:
4//! Diagram and PCB edit the assembly's `diagram` and `pcb`, Symbol and Pads the
5//! part's `symbol` and `pads` (`brep_render::history`). The editor commits
6//! every edit at once and reports it through `take_change`; [`BlockSync::store`]
7//! writes it into the block as one undo checkpoint, a typing run coalesced.
8//! That write is also what lights the tab's unsaved dot and re-arms the
9//! autosave, since both key on the history revision it moves
10//! ([`super::Document::edit_key`]).
11//!
12//! When the block moves under the editor — BREP's undo or redo, an import that
13//! writes it, a document replaced by a load — [`BlockSync::pull`] hands back
14//! the host's copy for the editor's `set_document` (or `set_symbol`,
15//! `set_footprint`), which keeps the view and clears the editor's own history.
16//! So there is one undo stack, BREP's, and Ctrl+Z reverts an eCAD edit and a
17//! feature edit newest first.
18
19use brep_render::history::{History, DIAGRAM, PADS, PCB, SYMBOL};
20use serde_json::Value;
21
22/// Which block an editor reads and writes.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum Block {
25 /// The assembly's wiring diagram (an eCAD `Document` of kind `Wiring`).
26 Diagram,
27 /// The assembly's schematic and board (an eCAD `Document`).
28 Pcb,
29 /// The part's schematic symbol (an eCAD `Symbol`).
30 Symbol,
31 /// The part's footprint pads (an eCAD `Footprint`).
32 Pads,
33}
34
35impl Block {
36 /// The top-level key in the saved document.
37 pub fn key(self) -> &'static str {
38 match self {
39 Block::Diagram => DIAGRAM,
40 Block::Pcb => PCB,
41 Block::Symbol => SYMBOL,
42 Block::Pads => PADS,
43 }
44 }
45
46 /// The block as the document holds it; `None` when no editor wrote it.
47 pub fn read(self, history: &History) -> Option<&Value> {
48 match self {
49 Block::Diagram => history.diagram_block(),
50 Block::Pcb => history.pcb_block(),
51 Block::Symbol => history.symbol_block(),
52 Block::Pads => history.pads_block(),
53 }
54 }
55
56 /// Write the block as one user edit; `coalesce` is eCAD's
57 /// `Change::coalesce`, which the setter namespaces by block.
58 pub fn write(self, history: &mut History, value: Value, coalesce: Option<&str>) {
59 match self {
60 Block::Diagram => history.set_diagram_block(Some(value), coalesce),
61 Block::Pcb => history.set_pcb_block(Some(value), coalesce),
62 Block::Symbol => history.set_symbol_block(Some(value), coalesce),
63 Block::Pads => history.set_pads_block(Some(value), coalesce),
64 }
65 }
66}
67
68/// One editor's standing with its block: the value it holds and the history
69/// revision it was last in step at, so a frame in which nothing changed costs
70/// one integer comparison.
71#[derive(Debug)]
72pub struct BlockSync {
73 block: Block,
74 /// The revision the editor was last in step at; `None` before the first
75 /// [`Self::pull`], which therefore always loads.
76 revision: Option<u64>,
77 /// The block value the editor holds, as far as the host knows.
78 held: Option<Value>,
79 /// Why the editor could not read the block last pulled (a block from a
80 /// newer eCAD, say). While set, [`Self::store`] refuses, so the editor's
81 /// stand-in document can never be written over the block it failed to read.
82 unreadable: Option<String>,
83 /// The per-component problems this block last reported ([`Self::unreported`]).
84 reported: Vec<String>,
85 /// The last edit a read-only document refused ([`Self::refused_again`]).
86 refused: Option<Value>,
87 /// How many times a read-only document took an edit back ([`Self::reload`]).
88 reloads: u64,
89}
90
91impl BlockSync {
92 pub fn new(block: Block) -> Self {
93 Self { block, revision: None, held: None, unreadable: None, reported: Vec::new(), refused: None, reloads: 0 }
94 }
95
96 pub fn block(&self) -> Block {
97 self.block
98 }
99
100 /// Which of `lines` this block has not already reported, remembering the
101 /// set for next time.
102 ///
103 /// An UNLINKED component is a STANDING state, not an event: the fold
104 /// reports it on every sheet edit, and one toast per keystroke is noise.
105 /// A problem that goes and comes back is reported again, which is right —
106 /// it is news both times.
107 pub fn unreported(&mut self, lines: Vec<String>) -> Vec<String> {
108 let fresh = lines.iter().filter(|line| !self.reported.contains(line)).cloned().collect();
109 self.reported = lines;
110 fresh
111 }
112
113 /// Write an edit the editor reported: `value` is the editor's document
114 /// (`to_value`), `coalesce` its `Change::coalesce`. Refused while the
115 /// block is [`Self::unreadable`].
116 pub fn store(&mut self, history: &mut History, value: Value, coalesce: Option<&str>) -> Result<(), String> {
117 if let Some(reason) = &self.unreadable {
118 return Err(format!("the {} block was not written: {reason}", self.block.key()));
119 }
120 self.block.write(history, value.clone(), coalesce);
121 self.held = Some(value);
122 self.revision = Some(history.revision());
123 Ok(())
124 }
125
126 /// Whether [`Self::pull`] has run at all: `false` until the editor first
127 /// loads this block, which is the document's OPEN as far as the editor can
128 /// tell (a document is built with its syncs, and a load builds a document).
129 pub fn pulled(&self) -> bool {
130 self.revision.is_some()
131 }
132
133 /// Write `value` as the block with NO undo step: `document` is the whole
134 /// saved document with `value` already in it, adopted as it stands. For
135 /// what the editor did to a block the frame it first loaded it — an older
136 /// file brought up to the current shape, which is part of opening it and
137 /// not an edit anyone made (see `viewport::ecad::store`). Refused while
138 /// the block is [`Self::unreadable`], as [`Self::store`] is.
139 pub fn adopt(&mut self, history: &mut History, document: &Value, value: Value) -> Result<(), String> {
140 if let Some(reason) = &self.unreadable {
141 return Err(format!("the {} block was not written: {reason}", self.block.key()));
142 }
143 history.adopt_document(&document.to_string())?;
144 self.held = Some(value);
145 self.revision = Some(history.revision());
146 Ok(())
147 }
148
149 /// The host's copy when the editor does not hold it: `Some(Some(block))`,
150 /// or `Some(None)` when the block is absent (never written, or undone past
151 /// its first edit), for the editor to load its empty document. `None` when
152 /// the editor is in step. The first call always loads.
153 ///
154 /// Every `Some` clears [`Self::unreadable`], so the caller owes this after
155 /// EACH `Some(Some(block))`: try to read it (`from_value`) and call
156 /// [`Self::refuse`] if that fails. Refusing once and not again on a later
157 /// `Some` lets the stand-in the editor shows be stored over the block.
158 pub fn pull(&mut self, history: &History) -> Option<Option<Value>> {
159 let revision = history.revision();
160 let first = self.revision.is_none();
161 if self.revision == Some(revision) {
162 return None;
163 }
164 self.revision = Some(revision);
165 let current = self.block.read(history);
166 if !first && current == self.held.as_ref() {
167 return None;
168 }
169 self.held = current.cloned();
170 self.unreadable = None;
171 Some(self.held.clone())
172 }
173
174 /// On a read-only document, the editor's first frame brought the block up
175 /// to the current shape: keep that on screen as held, unwritten. The saved
176 /// bytes do not move (`History::lock`), and the next pull does not undo
177 /// what the editor showed on opening.
178 pub fn hold_unwritten(&mut self, history: &History, value: Value) {
179 self.held = Some(value);
180 self.revision = Some(history.revision());
181 }
182
183 /// On a read-only document an edit is taken back: the next [`Self::pull`]
184 /// loads the stored block again whatever the editor holds.
185 pub fn reload(&mut self) {
186 self.revision = None;
187 self.reloads += 1;
188 }
189
190 /// How many edits a read-only document has taken back from this editor.
191 pub fn reloads(&self) -> u64 {
192 self.reloads
193 }
194
195 /// Whether `value` is the edit a read-only document refused last time, so
196 /// an editor that reports the same change every frame is told once.
197 pub fn refused_again(&mut self, value: &Value) -> bool {
198 let again = self.refused.as_ref() == Some(value);
199 self.refused = Some(value.clone());
200 again
201 }
202
203 /// The editor could not read the block [`Self::pull`] just handed it.
204 pub fn refuse(&mut self, reason: String) {
205 self.unreadable = Some(reason);
206 }
207
208 /// Why the block is not being edited, while the editor cannot read it.
209 pub fn unreadable(&self) -> Option<&str> {
210 self.unreadable.as_deref()
211 }
212}
213
214