Skip to main content

brep_render/
history.rs

1//! [`History`] — the engine-owned, editable model recipe: the ordered feature
2//! history plus the rollback index (the feature the model is currently built up
3//! to). This is the SINGLE SOURCE OF TRUTH for the model — the UI keeps NO copy;
4//! it mutates and reads the history only through [`crate::engine_state::EngineState`]
5//! methods. That keeps one engine-owned history (UI-agnostic) and converges with
6//! the in-flight "whole history in Rust" pipeline migration — later this sinks
7//! into `brep-kernel-rs` proper without touching the UI.
8//!
9//! Rolling to a step re-runs `features[0..=rollback]` through the SAME kernel
10//! pipeline: the full feature list stays in the request and `stopAtId` (the
11//! editor's "stop at the expanded feature") halts execution AFTER the rolled-to
12//! feature, so the kernel's incremental history cache is RETAINED across rolls
13//! (no thrash) and the viewport shows the model as of that step.
14
15use serde_json::Value;
16
17/// The cap on the undo (and redo) stack depth — old entries fall off the bottom.
18const MAX_UNDO: usize = 100;
19
20/// A restorable model state: the whole document PLUS the rolled-to step, captured
21/// together so an undo returns both the geometry and the view to the state they
22/// were in right before the mutation.
23#[derive(Debug, Clone)]
24struct Snapshot {
25    request: Value,
26    rollback: usize,
27    /// The parts-library block as it stood before the mutation. An `Rc`, so
28    /// capturing it costs a pointer bump however large the library is — undo
29    /// and redo still rewind it exactly as they did when it lived inside
30    /// `request` (a redo of a component insert must restore the entry its
31    /// ACOMP references).
32    parts_library: std::rc::Rc<Value>,
33    /// Names this undo step, so a caller can later ask whether the step it
34    /// took is still the newest one ([`History::undo_top`]). Drawn from one
35    /// process-wide counter; a redo entry keeps no serial of its own.
36    serial: u64,
37}
38
39/// Hands out every [`Snapshot::serial`] in this process.
40static NEXT_STEP: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
41
42fn next_step() -> u64 {
43    NEXT_STEP.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
44}
45
46/// The engine-owned mutable history.
47#[derive(Debug, Clone)]
48pub struct History {
49    /// The whole `HistoryRequest` document
50    /// (`{expressions, configurator, features: [...]}`).
51    ///
52    /// PRIVATE and reached only through [`Self::document`] /
53    /// [`Self::document_mut`] — the `_mut` door bumps [`Self::revision`], which
54    /// is what lets a per-frame cache key off the document without having to
55    /// enumerate the mutation sites. Naming the field `document` (not
56    /// `request`) is what makes that total: the compiler, not a reviewer, finds
57    /// a site that bypasses the doors.
58    document: Value,
59    /// A ticket drawn from one process-wide counter on construction and again on
60    /// every `&mut` reach into [`Self::document`]. Deliberately coarse — it
61    /// counts mutation OPPORTUNITIES, so it moves when nothing actually changed
62    /// (a cache then rebuilds once, harmlessly) but it cannot fail to move when
63    /// something did.
64    ///
65    /// Drawn from a shared counter rather than incremented per instance, because
66    /// a `History` is not only mutated, it is REPLACED: `set_history_json`
67    /// installs a fresh one on the engine a document ALREADY owns — the
68    /// `?loadModel=` boot drain, a recovery restore, an STL import accept, New.
69    /// A per-instance counter restarts there, so a replacement would hand a
70    /// cache a revision it had already seen for the same document and the cache
71    /// would serve the PREVIOUS model's values. One counter makes the guarantee
72    /// the same for both: no two states of any `History` in this process ever
73    /// share a revision.
74    revision: u64,
75    /// Index into `features` the model is rolled to (clamped to the last).
76    rollback: usize,
77    /// Undo/redo over the MODEL document. A snapshot is pushed BEFORE each model
78    /// mutation (edit / add / delete / reorder); roll-to-step is view state and
79    /// is NOT snapshotted. Rapid same-target edits (a slider drag) coalesce into a
80    /// single undo entry via `last_edit_key`. The stacks live here in the engine
81    /// core — the model is engine-owned, so its undo history is too; the UI only
82    /// triggers `undo()` / `redo()`.
83    undo_stack: Vec<Snapshot>,
84    redo_stack: Vec<Snapshot>,
85    /// The coalescing token of the most recently recorded edit (see `checkpoint`).
86    last_edit_key: Option<String>,
87    /// The persistent GLOBAL feature counter: bumped by one on every new-feature
88    /// mint ([`Self::next_feature_id`]), so a new id is `{shortName}{counter}`.
89    /// MONOTONIC and NEVER reused — deleting a feature does not free its number,
90    /// and (deliberately) undo does NOT rewind it, so re-doing an add can't collide
91    /// with a number already handed out. It is kept OFF `self.document` in memory
92    /// (so the undo snapshots that clone `request` never rewind it) and folded into
93    /// the serialized document under `"featureCounter"` so it round-trips save/load
94    /// (see [`Self::request_json`] / [`Self::from_request_json`]).
95    feature_counter: u64,
96    /// The assemblies PARTS LIBRARY block (`partsLibrary`), kept OFF
97    /// `self.document` for the same reason as `feature_counter`: every per-edit
98    /// pass over the document — the undo checkpoint clone, the kernel request
99    /// built by [`Self::prefix_request`], the assembly pose fold's serialize →
100    /// parse round trip — would otherwise copy the whole library, which for an
101    /// imported STEP assembly is megabytes of embedded part payload PER EDIT
102    /// and froze the browser UI. Folded back in by [`Self::request_json`] so
103    /// the SAVED document is byte-identical to before, and lifted back out by
104    /// [`Self::from_request_json`] / the adopt doors.
105    ///
106    /// `Rc` because undo/redo MUST rewind it (a redo of a component insert has
107    /// to restore the entry its ACOMP references) while a checkpoint must stay
108    /// a pointer copy — the whole point of moving it off `request`.
109    parts_library: std::rc::Rc<Value>,
110    /// Why the latest edit of a part's pins or ports was not followed to the
111    /// other side, naming the label that blocks it (see the pins-and-ports
112    /// block below). `None` once an edit binds, and after undo, redo or a
113    /// replaced document, which restore both sides together.
114    pin_port_hold: Option<String>,
115    /// A symbol edit changed the part's declared connection points, which only
116    /// a history run publishes. The engine's `pump` re-runs and clears it, so
117    /// no host has to remember to.
118    ports_followed: bool,
119    /// A ticket (from the same counter as [`Self::revision`]) redrawn on every
120    /// USER edit: every undo checkpoint, coalesced or not, and every undo and
121    /// redo. Machine writes (a solver fold, a parts-library heal, a
122    /// `_no_undo` block) and rolling do not move it. See [`Self::edit_serial`].
123    edit_serial: u64,
124    /// Why this document may not change — a PLM revision that is released, or
125    /// that this user has not checked out (`BREP_app`'s `Document::access`).
126    /// `None`: editable, as every file-based document always is. See
127    /// [`Self::lock`].
128    lock: Option<String>,
129    /// Where a mutation lands while [`Self::lock`] holds: a copy of the
130    /// document taken by [`Self::document_mut`] and thrown away by the next.
131    scratch: Value,
132    /// How many mutations were refused since construction — the count a host
133    /// watches to tell the user "check out to edit" ([`Self::refused_edits`]).
134    refused: u64,
135    /// Of those, the ones that were a USER's edit ([`Self::refused_user_edits`]).
136    refused_user: u64,
137}
138
139/// Hands out every revision ticket in this process — see [`History::revision`].
140/// Relaxed is enough: the value is only ever compared for equality against one
141/// a single thread read earlier, never used to order anything.
142static NEXT_REVISION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
143
144fn next_revision() -> u64 {
145    NEXT_REVISION.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
146}
147
148impl Default for History {
149    fn default() -> Self {
150        Self {
151            document: empty_request(),
152            revision: next_revision(),
153            edit_serial: next_revision(),
154            rollback: 0,
155            undo_stack: Vec::new(),
156            redo_stack: Vec::new(),
157            last_edit_key: None,
158            feature_counter: 0,
159            parts_library: std::rc::Rc::new(Value::Null),
160            pin_port_hold: None,
161            ports_followed: false,
162            lock: None,
163            scratch: Value::Null,
164            refused: 0,
165            refused_user: 0,
166        }
167    }
168}
169
170fn empty_request() -> Value {
171    serde_json::json!({ "expressions": "", "configurator": {}, "features": [] })
172}
173
174/// The trailing run of ASCII digits of `id` read as a number (`"P.CU12"` → 12,
175/// `"Box"` → 0). Trailing digits are ASCII (one byte each), so the slice boundary
176/// is always a valid char boundary; a missing/overflowing run yields 0.
177fn trailing_number(id: &str) -> u64 {
178    let digit_bytes = id
179        .bytes()
180        .rev()
181        .take_while(u8::is_ascii_digit)
182        .count();
183    id[id.len() - digit_bytes..].parse().unwrap_or(0)
184}
185
186impl History {
187    pub(crate) fn fold_plugin_persistent_data(&mut self, id: &str, data: Value) {
188        let Some(index) = self.index_of(id) else { return; };
189        if self.features()[index].get("persistentData") == Some(&data) { return; }
190        if let Some(features) = self.document_mut().get_mut("features").and_then(Value::as_array_mut) {
191            features[index]["persistentData"] = data;
192        }
193    }
194
195    /// Publish a successfully recomputed staged transaction as exactly one undo step.
196    /// The caller must check its captured revision before reaching this door.
197    pub(crate) fn commit_staged(&mut self, staged: &Self) -> Result<(), String> {
198        if self.refuse_user() {
199            return Err(format!("the document is read-only: {}", self.locked().unwrap_or("locked")));
200        }
201        self.checkpoint(None);
202        *self.document_mut() = staged.document.clone();
203        self.parts_library = staged.parts_library.clone();
204        self.feature_counter = staged.feature_counter;
205        self.rollback = staged.rollback;
206        self.pin_port_hold = None;
207        Ok(())
208    }
209
210    /// Load a whole history document (a saved part file parses as one). Rolls to
211    /// the last feature. Ensures a `features` array exists.
212    pub fn from_request_json(json: &str) -> Result<Self, String> {
213        let mut request: Value =
214            serde_json::from_str(json).map_err(|e| format!("history parse: {e}"))?;
215        if !request.get("features").map(Value::is_array).unwrap_or(false) {
216            if let Some(obj) = request.as_object_mut() {
217                obj.insert("features".into(), Value::Array(Vec::new()));
218            } else {
219                request = empty_request();
220            }
221        }
222        // Lift the persistent feature counter OUT of the document so it lives ONLY
223        // in the struct field: kept off `self.document`, the undo snapshots (which
224        // clone `request`) can never rewind it, and it can't be double-folded on
225        // re-serialize. A document with no stored counter (fresh or saved before
226        // this field existed) safe-inits below.
227        let stored = request
228            .as_object_mut()
229            .and_then(|obj| obj.remove("featureCounter"))
230            .and_then(|value| value.as_u64());
231        // Lift the parts-library block out of the document for the same reason
232        // (see the `parts_library` field): every per-edit copy of `request`
233        // would otherwise carry megabytes of embedded part payload.
234        let parts_library = request
235            .as_object_mut()
236            .and_then(|obj| obj.remove("partsLibrary"))
237            .unwrap_or(Value::Null);
238        let mut history = Self {
239            document: request,
240            rollback: 0,
241            parts_library: std::rc::Rc::new(parts_library),
242            ..Self::default()
243        };
244        history.rollback = history.len().saturating_sub(1);
245        // Safe init when unstored: start ABOVE the largest numeric suffix already
246        // present among feature ids so the next mint (`{shortName}{counter+1}`)
247        // cannot collide with an existing id. This holds because no shortName ends
248        // in a digit (verified — even `IMPORT3D` ends in `D`), so an id's trailing
249        // digits ARE its numeric suffix and `counter+1` strictly exceeds them all.
250        // A STORED counter is floored the same way: a document whose counter is
251        // behind its own ids (hand-written, or saved by an older build) would
252        // otherwise mint an id it already has, which the append now refuses.
253        history.feature_counter = stored.unwrap_or(0).max(history.max_id_suffix());
254        Ok(history)
255    }
256
257    /// The largest trailing-integer suffix among all existing feature ids (0 when
258    /// none carry one) — the floor for a safe counter init on a document with no
259    /// stored `featureCounter` (see [`Self::from_request_json`]).
260    fn max_id_suffix(&self) -> u64 {
261        self.features()
262            .iter()
263            .filter_map(|f| {
264                f.get("inputParams")
265                    .and_then(|p| p.get("id"))
266                    .and_then(Value::as_str)
267            })
268            .map(trailing_number)
269            .max()
270            .unwrap_or(0)
271    }
272
273    /// The whole document, read-only.
274    fn document(&self) -> &Value {
275        &self.document
276    }
277
278    /// The whole document for mutation. Bumps [`Self::revision`] — this is the
279    /// only way to reach `self.document` mutably.
280    ///
281    /// It is NOT the only door a cache keyed on the revision has to cover. The
282    /// `partsLibrary` block is held in [`Self::parts_library`], beside the
283    /// document rather than inside it, and [`Self::request_json`] folds it back
284    /// in — so it is part of the saved document and has its own two setters,
285    /// which bump the revision themselves. Anything else lifted out of
286    /// `self.document` into a field of its own owes the same.
287    ///
288    /// While the history is [locked](Self::lock), the reach lands on a SCRATCH
289    /// copy that the next reach replaces, and the refusal is counted: every
290    /// mutation that goes through this door — a user edit, and equally a
291    /// solver fold or a pin settle — leaves [`Self::request_json`] exactly as
292    /// it was. The revision does not move either, since nothing changed.
293    fn document_mut(&mut self) -> &mut Value {
294        if self.lock.is_some() {
295            self.refused += 1;
296            self.scratch = self.document.clone();
297            return &mut self.scratch;
298        }
299        self.revision = next_revision();
300        &mut self.document
301    }
302
303    /// Refuse every change to the saved document until [`Self::unlock`]:
304    /// [`Self::request_json`] stays byte for byte what it is now, whatever is
305    /// called. `reason` is the sentence a host shows ("released", "checked out
306    /// by grace", "not checked out"). Rolling ([`Self::set_rollback`]) is view
307    /// state and stays free, and so does reading.
308    ///
309    /// The three writes that do not pass `Self::document_mut` — the parts
310    /// library, the feature counter and the undo/redo stacks — check the lock
311    /// themselves; `a_locked_history_keeps_its_saved_bytes` calls every
312    /// mutator and pins the invariant.
313    pub fn lock(&mut self, reason: impl Into<String>) {
314        self.lock = Some(reason.into());
315        // A typing run from before the lock must not coalesce with the first
316        // edit after the next checkout into one undo entry.
317        self.break_coalescing();
318    }
319
320    /// Allow changes again (a checkout took the lock).
321    pub fn unlock(&mut self) {
322        self.lock = None;
323        self.scratch = Value::Null;
324    }
325
326    /// Why the document may not change, or `None` when it may.
327    pub fn locked(&self) -> Option<&str> {
328        self.lock.as_deref()
329    }
330
331    /// Mutations refused while locked, counted since this history was built.
332    /// A host compares it frame to frame to tell the user an edit was refused.
333    pub fn refused_edits(&self) -> u64 {
334        self.refused
335    }
336
337    /// Refused mutations that were a user's edit: every one that takes an undo
338    /// checkpoint, an undo, redo or retract, a feature id minted, a parts-library
339    /// edit. A kernel write-back (a solver fold, a pin settle, a parts-library
340    /// heal) is refused just the same but not counted here: it is nobody's edit,
341    /// so a host tells the user about THIS count, not [`Self::refused_edits`].
342    pub fn refused_user_edits(&self) -> u64 {
343        self.refused_user
344    }
345
346    /// Count a refused write that does not pass [`Self::document_mut`], and
347    /// say whether to refuse it.
348    fn refuse(&mut self) -> bool {
349        if self.lock.is_some() {
350            self.refused += 1;
351            return true;
352        }
353        false
354    }
355
356    /// [`Self::refuse`] for a write only a user makes.
357    fn refuse_user(&mut self) -> bool {
358        let refused = self.refuse();
359        if refused {
360            self.refused_user += 1;
361        }
362        refused
363    }
364
365    /// A monotonic counter of mutation opportunities on the document — the
366    /// cache key for anything derived from it that would otherwise be rebuilt
367    /// every frame (see the `__brepParams` blob in `BREP_app`).
368    pub fn revision(&self) -> u64 {
369        self.revision
370    }
371
372    /// Moves on every user edit of the model and on every undo and redo, and
373    /// on nothing else: not on a run's write-back, not on rolling. What a
374    /// transient view of the model (the family-row preview) keys on to know
375    /// that the user changed the model under it.
376    pub fn edit_serial(&self) -> u64 {
377        self.edit_serial
378    }
379
380    /// The features slice (empty if none).
381    pub fn features(&self) -> &[Value] {
382        self.document()
383            .get("features")
384            .and_then(Value::as_array)
385            .map(Vec::as_slice)
386            .unwrap_or(&[])
387    }
388
389    fn features_mut(&mut self) -> &mut Vec<Value> {
390        let obj = self
391            .document_mut()
392            .as_object_mut()
393            .expect("history request is a JSON object");
394        obj.entry("features")
395            .or_insert_with(|| Value::Array(Vec::new()));
396        obj.get_mut("features")
397            .and_then(Value::as_array_mut)
398            .expect("features is a JSON array")
399    }
400
401    pub fn len(&self) -> usize {
402        self.features().len()
403    }
404
405    pub fn is_empty(&self) -> bool {
406        self.len() == 0
407    }
408
409    /// The rolled-to index, clamped to a valid feature (0 when empty).
410    pub fn rollback(&self) -> usize {
411        self.rollback.min(self.len().saturating_sub(1))
412    }
413
414    pub fn set_rollback(&mut self, index: usize) {
415        self.rollback = if self.is_empty() {
416            0
417        } else {
418            index.min(self.len() - 1)
419        };
420        // Rolling to a step is a view move, not a model edit: it records NO undo
421        // snapshot, but it DOES break the edit-coalescing run so the next edit
422        // starts a fresh undo entry rather than merging with a pre-roll edit.
423        self.last_edit_key = None;
424    }
425
426    pub fn feature_type(&self, index: usize) -> Option<String> {
427        self.features()
428            .get(index)?
429            .get("type")
430            .and_then(Value::as_str)
431            .map(String::from)
432    }
433
434    pub fn feature_id(&self, index: usize) -> Option<String> {
435        self.features()
436            .get(index)?
437            .get("inputParams")
438            .and_then(|p| p.get("id"))
439            .and_then(Value::as_str)
440            .map(String::from)
441    }
442
443    pub fn index_of(&self, id: &str) -> Option<usize> {
444        self.features().iter().position(|f| {
445            f.get("inputParams")
446                .and_then(|p| p.get("id"))
447                .and_then(Value::as_str)
448                == Some(id)
449        })
450    }
451
452    /// The `inputParams` document of the feature at `index` (for the dialog).
453    pub fn feature_params(&self, index: usize) -> Option<Value> {
454        self.features().get(index)?.get("inputParams").cloned()
455    }
456
457    /// SOLVER write-back fold (the assembly pose-authority contract): set ONE
458    /// `inputParams` key of the feature whose `inputParams.id == id`, WITHOUT an
459    /// undo checkpoint and WITHOUT breaking edit coalescing — a solve write-back
460    /// is the kernel adopting its own result, not a user edit, so it must never
461    /// mint an undo entry (undoing a user action then re-running re-solves and
462    /// re-folds anyway). Returns whether a feature matched.
463    pub fn fold_param_no_undo(&mut self, id: &str, key: &str, value: Value) -> bool {
464        let Some(index) = self.index_of(id) else {
465            return false;
466        };
467        if let Some(feature) = self.features_mut().get_mut(index) {
468            if let Some(params) = feature
469                .get_mut("inputParams")
470                .and_then(Value::as_object_mut)
471            {
472                params.insert(key.to_string(), value);
473                return true;
474            }
475        }
476        false
477    }
478
479    pub fn set_feature_params(&mut self, index: usize, params: Value) {
480        if index >= self.len() {
481            return;
482        }
483        // Coalesce a slider drag (many consecutive edits of the SAME feature) into
484        // one undo entry, keyed by the feature index.
485        self.checkpoint(Some(&format!("param:{index}")));
486        if let Some(feat) = self.features_mut().get_mut(index) {
487            if let Some(obj) = feat.as_object_mut() {
488                obj.insert("inputParams".into(), params);
489            }
490        }
491    }
492
493    /// Replace the `inputParams` of MANY features as ONE mutation —
494    /// [`Self::set_feature_params`]'s batch sibling, and the reason it exists:
495    /// a packed BOM row rolls up N occurrences, so editing one cell writes N
496    /// features. A `set_feature_params` loop would mint N undo entries (each
497    /// keyed `param:{index}`, so none of them coalesce with each other), and
498    /// the user would have to press undo N times to take back ONE edit.
499    ///
500    /// Exactly ONE checkpoint covers the whole batch. The coalesce key names
501    /// the SET of indices, so a run of edits to the SAME set (typing into one
502    /// packed cell) still merges into a single entry, while switching to a
503    /// different set starts a new one — the `param:{index}` rule, lifted to a
504    /// group. Out-of-range indices are skipped; an empty batch is a no-op (no
505    /// checkpoint, so a fan-out that matched nothing leaves no empty entry).
506    pub fn set_many_feature_params(&mut self, edits: &[(usize, Value)]) {
507        let len = self.len();
508        let mut in_range: Vec<&(usize, Value)> =
509            edits.iter().filter(|(index, _)| *index < len).collect();
510        if in_range.is_empty() {
511            return;
512        }
513        // The key must not depend on the caller's ordering, or the same packed
514        // row could produce two different keys and stop coalescing.
515        in_range.sort_by_key(|(index, _)| *index);
516        let key: Vec<String> = in_range
517            .iter()
518            .map(|(index, _)| index.to_string())
519            .collect();
520        self.checkpoint(Some(&format!("params:{}", key.join(","))));
521        for (index, params) in in_range {
522            if let Some(feature) = self.features_mut().get_mut(*index) {
523                if let Some(object) = feature.as_object_mut() {
524                    object.insert("inputParams".into(), params.clone());
525                }
526            }
527        }
528    }
529
530    pub fn push_feature(&mut self, feature: Value) {
531        self.checkpoint(None);
532        self.features_mut().push(feature);
533    }
534
535    /// Append MANY features as ONE mutation — the batch an import lane needs
536    /// (`EngineState::add_features`). Distinct from a `push_feature` loop in the
537    /// two ways that matter: exactly ONE undo checkpoint (an N-instance assembly
538    /// import undoes in one step, not N), and the caller re-runs once instead of
539    /// once per feature. No-op for an empty batch (no checkpoint, so an import
540    /// that produced nothing leaves no empty undo entry).
541    pub fn push_features(&mut self, features: Vec<Value>) {
542        if features.is_empty() {
543            return;
544        }
545        self.checkpoint(None);
546        self.features_mut().extend(features);
547    }
548
549    pub fn remove_feature(&mut self, index: usize) {
550        if index >= self.len() {
551            return;
552        }
553        self.checkpoint(None);
554        self.features_mut().remove(index);
555    }
556
557    pub fn swap(&mut self, a: usize, b: usize) {
558        let len = self.len();
559        if a < len && b < len && a != b {
560            self.checkpoint(None);
561            self.features_mut().swap(a, b);
562        }
563    }
564
565    /// Mint the id for a NEW feature: `{base}{N}` where `base` is the feature's
566    /// shortName and `N` is this history's persistent GLOBAL counter, bumped by one
567    /// on every mint (`P.CU` → `P.CU7`, `S` → `S8`). GLOBAL across all feature
568    /// types, MONOTONIC, and NEVER reused — a delete does not free a number and
569    /// undo does not rewind the counter — and it persists across save/load, so two
570    /// features can never receive the same id over the document's whole lifetime.
571    pub fn next_feature_id(&mut self, base: &str) -> String {
572        // Locked, the counter is saved state and does not move; the id is
573        // still minted, for a feature whose push will be refused anyway.
574        if self.refuse_user() {
575            return format!("{base}{}", self.feature_counter + 1);
576        }
577        self.feature_counter += 1;
578        format!("{base}{}", self.feature_counter)
579    }
580
581    /// Commit IDs reserved on a transaction's cloned history. Document adoption
582    /// deliberately ignores serialized counters, so the transaction must carry
583    /// its allocation high-water mark separately. Explicit committed IDs also
584    /// set a floor; undo keeps this monotonic, while rollback restores a clone.
585    pub(crate) fn retain_feature_allocations(&mut self, staged: &Self) {
586        if self.refuse_user() {
587            return;
588        }
589        self.feature_counter = self
590            .feature_counter
591            .max(staged.feature_counter)
592            .max(self.max_id_suffix());
593    }
594
595    /// The `stopAtId`-truncated request that stops AFTER the rolled-to feature —
596    /// the roll-to-step request the pipeline runs. Empty history → empty request.
597    pub fn prefix_request(&self) -> Value {
598        let mut request = self.document().clone();
599        if let Some(id) = self.feature_id(self.rollback()) {
600            if let Some(obj) = request.as_object_mut() {
601                obj.insert("stopAtId".into(), Value::String(id));
602            }
603        }
604        request
605    }
606
607    /// The request that stops BEFORE feature `index` — the scene that feature
608    /// resolves its references against, wherever the model is rolled to. `None`
609    /// when there is no feature at `index`.
610    pub fn request_before(&self, index: usize) -> Option<Value> {
611        let id = self.feature_id(index)?;
612        let mut request = self.document().clone();
613        request.as_object_mut()?.insert("stopBeforeId".into(), Value::String(id));
614        Some(request)
615    }
616
617    /// The tree listing for the UI: `{ step, features: [{index, type, id}] }`.
618    pub fn listing_json(&self) -> String {
619        let features: Vec<Value> = self
620            .features()
621            .iter()
622            .enumerate()
623            .map(|(index, _)| {
624                serde_json::json!({
625                    "index": index,
626                    "type": self.feature_type(index).unwrap_or_else(|| "?".into()),
627                    "id": self.feature_id(index).unwrap_or_else(|| "(no id)".into()),
628                })
629            })
630            .collect();
631        serde_json::json!({ "step": self.rollback(), "features": features }).to_string()
632    }
633
634    /// The whole request document (for persistence / debugging), with the
635    /// persistent global feature counter folded back in under `"featureCounter"`
636    /// so it round-trips through save/load (the twin of [`Self::from_request_json`],
637    /// which lifts it back out). Written only when non-zero, so a document that has
638    /// never minted a feature persists byte-for-byte as before (mirrors the
639    /// metadata field's "un-annotated model persists unchanged" convention).
640    pub fn request_json(&self) -> String {
641        let has_library = self
642            .parts_library
643            .as_object()
644            .map(|map| !map.is_empty())
645            .unwrap_or(false);
646        if self.feature_counter == 0 && !has_library {
647            return self.document().to_string();
648        }
649        let mut document = self.document().clone();
650        if let Some(obj) = document.as_object_mut() {
651            if self.feature_counter != 0 {
652                obj.insert("featureCounter".into(), Value::from(self.feature_counter));
653            }
654            if has_library {
655                obj.insert("partsLibrary".into(), (*self.parts_library).clone());
656            }
657        }
658        document.to_string()
659    }
660
661    /// [`Self::request_json`] as a value, for a caller that edits it: the
662    /// same document, never printed and parsed back (an assembly's library
663    /// is megabytes).
664    pub fn request_value(&self) -> Value {
665        let mut document = self.document().clone();
666        if let Some(obj) = document.as_object_mut() {
667            if self.feature_counter != 0 {
668                obj.insert("featureCounter".into(), Value::from(self.feature_counter));
669            }
670            if self.parts_library.as_object().is_some_and(|map| !map.is_empty()) {
671                obj.insert("partsLibrary".into(), (*self.parts_library).clone());
672            }
673        }
674        document
675    }
676
677    /// The document WITHOUT the parts-library block — for the kernel round
678    /// trips that never read it. The assembly pose fold
679    /// (`assembly_apply_document_json`) only rewrites the `assembly` block and
680    /// per-feature `inputParams`, so handing it the library would serialize,
681    /// parse and re-serialize megabytes of part payload on every edit for
682    /// nothing. [`Self::request_json`] is the SAVE door and still carries it.
683    pub fn request_json_without_parts_library(&self) -> String {
684        if self.feature_counter == 0 {
685            return self.document().to_string();
686        }
687        let mut document = self.document().clone();
688        if let Some(obj) = document.as_object_mut() {
689            obj.insert("featureCounter".into(), Value::from(self.feature_counter));
690        }
691        document.to_string()
692    }
693
694    // --- Undo / redo over the model document ------------------------------
695
696    fn snapshot(&self) -> Snapshot {
697        Snapshot {
698            request: self.document().clone(),
699            rollback: self.rollback,
700            parts_library: self.parts_library.clone(),
701            serial: next_step(),
702        }
703    }
704
705    fn restore(&mut self, snap: Snapshot) {
706        self.edit_serial = next_revision();
707        *self.document_mut() = snap.request;
708        self.parts_library = snap.parts_library;
709        // A snapshot holds both sides as they stood together, so whatever the
710        // last pin or port edit held is no longer on screen.
711        self.pin_port_hold = None;
712        let last = self.len().saturating_sub(1);
713        self.rollback = snap.rollback.min(last);
714    }
715
716    /// Record a pre-mutation snapshot for undo. `coalesce_key` groups a run of
717    /// rapid same-target edits (one slider drag) into a SINGLE undo entry: while
718    /// the same non-empty key repeats, no new snapshot is pushed. A `None` key
719    /// never coalesces, so every structural add/delete/reorder is its own entry.
720    /// Any new snapshot clears the redo stack (a fresh edit forks the timeline)
721    /// and the oldest entry falls off once the stack passes [`MAX_UNDO`].
722    ///
723    /// A run has no time limit: only another key, an undo or
724    /// [`Self::break_coalescing`] ends it. The PMI label drag and the sheet
725    /// view, dimension and ordinate moves call that on release; nothing else
726    /// does, and a typing run has no release. So a param slider or
727    /// field (`param:{index}`) or an eCAD field (`component:{id}:text`) edited,
728    /// left, and edited again later with nothing in between is ONE undo step
729    /// across the pause, even across a Save. An eCAD host should call it when
730    /// focus leaves the editor's fields. Param runs are left as they are, a
731    /// decision not yet taken.
732    fn checkpoint(&mut self, coalesce_key: Option<&str>) {
733        // Locked, the edit this checkpoint precedes is refused at
734        // `document_mut`: no undo entry for an edit that never happened.
735        if self.lock.is_some() {
736            self.refused_user += 1;
737            return;
738        }
739        // Before the coalesce return: the second keystroke of a typing run is
740        // an edit too.
741        self.edit_serial = next_revision();
742        if coalesce_key.is_some() && coalesce_key == self.last_edit_key.as_deref() {
743            return;
744        }
745        self.undo_stack.push(self.snapshot());
746        if self.undo_stack.len() > MAX_UNDO {
747            self.undo_stack.remove(0);
748        }
749        self.redo_stack.clear();
750        self.last_edit_key = coalesce_key.map(str::to_string);
751    }
752
753    /// Whether an undo step is available (for enabling the toolbar button).
754    pub fn can_undo(&self) -> bool {
755        !self.undo_stack.is_empty()
756    }
757
758    /// Whether a redo step is available.
759    pub fn can_redo(&self) -> bool {
760        !self.redo_stack.is_empty()
761    }
762
763    /// Undo the last model mutation: push the current state onto the redo stack
764    /// and restore the previous document + rolled-to step. Returns whether it
765    /// changed anything (false when the undo stack is empty).
766    pub fn undo(&mut self) -> bool {
767        if self.refuse_user() {
768            return false;
769        }
770        let Some(prev) = self.undo_stack.pop() else {
771            return false;
772        };
773        self.redo_stack.push(self.snapshot());
774        self.restore(prev);
775        // A distinct undo breaks any coalescing run so the next edit is fresh.
776        self.last_edit_key = None;
777        true
778    }
779
780    /// Redo the last undone model mutation (symmetric with [`Self::undo`]).
781    pub fn redo(&mut self) -> bool {
782        if self.refuse_user() {
783            return false;
784        }
785        let Some(next) = self.redo_stack.pop() else {
786            return false;
787        };
788        self.undo_stack.push(self.snapshot());
789        self.restore(next);
790        self.last_edit_key = None;
791        true
792    }
793}
794
795// ============================================================================
796// Assembly document accessors (appended — the assemblies Wave-3 slice). A
797// SEPARATE `impl` block so concurrent edits to the primary block don't
798// conflict; purely additive over the existing history API.
799// ============================================================================
800impl History {
801    /// ADOPT a whole replacement document — the assembly FOLD's write-back lane
802    /// (`assembly_apply_document_json` returns the document with the solved
803    /// `assembly` block + poses/isFixed folded into the features; the engine
804    /// adopts it before persisting or re-running — the pose-authority contract).
805    ///
806    /// `checkpoint` chooses the undo semantics: a USER constraint mutation
807    /// records an undo snapshot (so constraint edits stay undoable like any
808    /// model edit); the silent post-run pose fold passes `false` (solver
809    /// write-back is not a user edit — undoing the user's LAST edit must not
810    /// strand an extra fold step in between).
811    ///
812    /// A stray `featureCounter` in the adopted document is stripped (the
813    /// in-memory counter stays authoritative — `request_json` re-folds it on
814    /// serialize, exactly like `from_request_json` lifts it on load). The
815    /// rollback index is preserved (the fold never changes the feature count).
816    pub fn adopt_document(&mut self, document_json: &str) -> Result<(), String> {
817        // Locked, say so: a caller that checks the result (an eCAD block sync
818        // recording what it wrote) must not believe a refused write landed.
819        if let Some(reason) = &self.lock {
820            let error = format!("the document is read-only: {reason}");
821            self.refused += 1;
822            return Err(error);
823        }
824        let document: Value = serde_json::from_str(document_json)
825            .map_err(|error| format!("adopt document parse: {error}"))?;
826        self.adopt_document_value(document)
827    }
828
829    /// [`Self::adopt_document`] for a caller that already holds the parsed
830    /// document: an assembly's parts library is megabytes, and printing it only
831    /// to parse it back was most of an Update Components commit at 500 parts.
832    pub fn adopt_document_value(&mut self, mut document: Value) -> Result<(), String> {
833        if let Some(reason) = &self.lock {
834            let error = format!("the document is read-only: {reason}");
835            self.refused += 1;
836            return Err(error);
837        }
838        if !document.is_object() {
839            return Err("adopt document: not a JSON object".into());
840        }
841        let mut adopted_library = None;
842        if let Some(obj) = document.as_object_mut() {
843            obj.remove("featureCounter");
844            // A block PRESENT in the adopted document replaces the field; one
845            // ABSENT leaves it alone (the fold lane is handed a library-free
846            // document by `request_json_without_parts_library` and must not
847            // silently drop the library on the way back).
848            adopted_library = obj.remove("partsLibrary");
849            if !obj.get("features").map(Value::is_array).unwrap_or(false) {
850                obj.insert("features".into(), Value::Array(Vec::new()));
851            }
852        }
853        *self.document_mut() = document;
854        // A replaced document is not the one a pin or port edit was held on.
855        self.pin_port_hold = None;
856        if let Some(library) = adopted_library {
857            self.set_parts_library(library);
858            }
859        Ok(())
860    }
861
862    /// Same adoption with an undo snapshot recorded FIRST (never coalesced) —
863    /// the user-mutation twin of [`Self::adopt_document`].
864    pub fn adopt_document_checkpointed(&mut self, document_json: &str) -> Result<(), String> {
865        // Validate BEFORE snapshotting so a parse failure never pushes a
866        // phantom undo entry.
867        let probe: Value = serde_json::from_str(document_json)
868            .map_err(|error| format!("adopt document parse: {error}"))?;
869        if !probe.is_object() {
870            return Err("adopt document: not a JSON object".into());
871        }
872        self.checkpoint(None);
873        self.adopt_document_value(probe)
874    }
875
876    /// [`Self::adopt_document_checkpointed`] for an already parsed document.
877    pub fn adopt_document_value_checkpointed(&mut self, document: Value) -> Result<(), String> {
878        if !document.is_object() {
879            return Err("adopt document: not a JSON object".into());
880        }
881        self.checkpoint(None);
882        self.adopt_document_value(document)
883    }
884
885    /// Replace the document's `partsLibrary` block (the assemblies parts
886    /// library) — the caller feeds `brep_kernel::parts_library_json()` here
887    /// (never an echo of a loaded block), so SAVE serializes the kernel
888    /// store with its heals and GC. An empty map clears the field, so a
889    /// non-assembly document serializes byte-identically to before.
890    ///
891    /// The block is held in the `parts_library` FIELD rather than on
892    /// `self.document`; [`Self::request_json`] folds it back into the saved
893    /// document. The on-disk shape is unchanged.
894    pub fn set_parts_library(&mut self, library: Value) {
895        if self.refuse() {
896            return;
897        }
898        let empty = library.as_object().map(|m| m.is_empty()).unwrap_or(true);
899        self.parts_library = std::rc::Rc::new(if empty { Value::Null } else { library });
900        // The block is part of the saved document, so this is a document
901        // mutation and owes the revision a tick — `document_mut` never sees it.
902        self.revision = next_revision();
903    }
904
905    /// Replace the `partsLibrary` block as a USER EDIT: one undo checkpoint,
906    /// and the mirror flag CLEARED so the next run ships the block to the
907    /// runner instead of assuming the kernel store already agrees.
908    ///
909    /// [`Self::set_parts_library`] is the other door and means the opposite —
910    /// "this block came OUT of the kernel store" — so it must not be reused
911    /// here: a part-attribute edit is authored on this side and the runner has
912    /// never seen it. `coalesce_key` follows the `param:{index}` rule so a run
913    /// of keystrokes into one attribute is one undo entry.
914    pub fn set_parts_library_edited(&mut self, library: Value, coalesce_key: Option<&str>) {
915        if self.refuse_user() {
916            return;
917        }
918        self.checkpoint(coalesce_key);
919        let empty = library.as_object().map(|m| m.is_empty()).unwrap_or(true);
920        self.parts_library = std::rc::Rc::new(if empty { Value::Null } else { library });
921        // As [`Self::set_parts_library`]: `checkpoint` records an undo entry
922        // and moves nothing a cache can see.
923        self.revision = next_revision();
924    }
925
926    /// The `partsLibrary` block (`Value::Null` when the document has none).
927    pub fn parts_library(&self) -> &Value {
928        &self.parts_library
929    }
930
931    /// Top-level block `key` of the document as it stood before the NEWEST
932    /// undoable edit (`None` with nothing to undo, or when that document had
933    /// no such block): what a caller reads to tell whether the newest step is
934    /// the one it means to take back.
935    pub fn undo_top_block(&self, key: &str) -> Option<&Value> {
936        self.undo_stack.last().and_then(|snapshot| snapshot.request.get(key))
937    }
938
939    /// The `partsLibrary` block as it stood before each undoable edit, newest
940    /// first: where a caller finds a library entry as it was when it appeared.
941    pub fn undo_parts_libraries(&self) -> impl Iterator<Item = &Value> + '_ {
942        self.undo_stack.iter().rev().map(|snapshot| snapshot.parts_library.as_ref())
943    }
944
945    /// The document's `assembly` block (`{constraints, idCounter}`), if any.
946    pub fn assembly_block(&self) -> Option<&Value> {
947        self.document().get("assembly")
948    }
949
950    /// Replace the `assembly` block (`{constraints, idCounter}`) — the STATE
951    /// half of the assembly write-back, which the engine folds from the run's
952    /// reply after every run and from the live session after a constraint
953    /// mutation. `checkpoint` = true makes it an undoable USER edit (a
954    /// mutation); false writes it silently, because a solver adopting its own
955    /// result is not a user edit and must never mint an undo entry — undoing a
956    /// user action then re-running re-solves and re-folds anyway.
957    pub fn set_assembly_block(&mut self, block: Value, checkpoint: bool) {
958        if checkpoint {
959            self.checkpoint(None);
960        }
961        if let Some(object) = self.document_mut().as_object_mut() {
962            object.insert("assembly".into(), block);
963        }
964    }
965
966    /// The document's `wireHarness` block (`{connections, idCounter,
967    /// buildBundles}`), if any.
968    pub fn wire_harness_block(&self) -> Option<&Value> {
969        self.document().get("wireHarness")
970    }
971
972    /// Replace (or, with `None`, remove) the `wireHarness` block. A USER edit:
973    /// snapshotted for undo, never coalesced — each add / edit / remove of a
974    /// connection is its own undo step, like a feature add or delete.
975    pub fn set_wire_harness_block(&mut self, block: Option<Value>) {
976        self.checkpoint(None);
977        if let Some(object) = self.document_mut().as_object_mut() {
978            match block {
979                Some(block) => {
980                    object.insert("wireHarness".into(), block);
981                }
982                None => {
983                    object.shift_remove("wireHarness");
984                }
985            }
986        }
987    }
988}
989
990// ============================================================================
991// PMI block accessors (the PMI workbench slice). A SEPARATE `impl` block —
992// purely additive over the history API.
993// ============================================================================
994impl History {
995    /// The document's `pmi` block (`{views, idCounter}`), if any.
996    pub fn pmi_block(&self) -> Option<&Value> {
997        self.document().get("pmi")
998    }
999
1000    /// Replace (or, with `None`, remove) the `pmi` block as a USER edit:
1001    /// snapshotted for undo. `coalesce_key` groups a run of rapid same-target
1002    /// edits (a label drag: `pmi:label:{id}`) into ONE undo entry; `None`
1003    /// makes the edit its own step (a view capture, an annotation add / edit
1004    /// / delete).
1005    pub fn set_pmi_block(&mut self, block: Option<Value>, coalesce_key: Option<&str>) {
1006        self.checkpoint(coalesce_key);
1007        self.put_pmi_block(block);
1008    }
1009
1010    /// Replace the `pmi` block WITHOUT an undo checkpoint — for a write that
1011    /// belongs to the checkpoint just taken (an import lifting a file's PMI
1012    /// beside the feature it added, so one undo removes both).
1013    pub fn set_pmi_block_no_undo(&mut self, block: Option<Value>) {
1014        self.put_pmi_block(block);
1015    }
1016
1017    fn put_pmi_block(&mut self, block: Option<Value>) {
1018        if let Some(object) = self.document_mut().as_object_mut() {
1019            match block {
1020                Some(block) => {
1021                    object.insert("pmi".into(), block);
1022                }
1023                None => {
1024                    object.shift_remove("pmi");
1025                }
1026            }
1027        }
1028    }
1029
1030    /// End a coalescing run (a label drag released): the next edit with the
1031    /// same key starts a fresh undo entry.
1032    pub fn break_coalescing(&mut self) {
1033        self.last_edit_key = None;
1034    }
1035}
1036
1037// ============================================================================
1038// CAM block accessors (the CAM workbench slice). A SEPARATE `impl` block,
1039// purely additive over the history API, on the PMI block's pattern.
1040//
1041// `cam` is a top-level document key like `pmi` and `sheets`: the user's
1042// setups, tools, operations and post settings. The kernel never reads it and
1043// `HistoryRequest` has no field for it — the document's unknown-key
1044// pass-through ([`Self::request_json`] serializes the raw document) is what
1045// round-trips it through save, load and undo. Derived data (toolpaths, mesh
1046// snapshots, stock states) is never written here: it is regenerated.
1047// ============================================================================
1048impl History {
1049    /// The document's `cam` block (`{setups, tools, operations, ncPrograms,
1050    /// idCounter}`), if any.
1051    pub fn cam_block(&self) -> Option<&Value> {
1052        self.document().get("cam")
1053    }
1054
1055    /// Replace (or, with `None`, remove) the `cam` block as a USER edit:
1056    /// snapshotted for undo. `coalesce_key` groups a run of rapid same-target
1057    /// edits (typing in one field of one operation: `cam:op:{id}:{field}`)
1058    /// into ONE undo entry; `None` makes the edit its own step (a setup, tool
1059    /// or operation add / delete, a pick committed).
1060    pub fn set_cam_block(&mut self, block: Option<Value>, coalesce_key: Option<&str>) {
1061        let coalesce_key = coalesce_key.map(|change| format!("cam:{change}"));
1062        self.checkpoint(coalesce_key.as_deref());
1063        self.put_cam_block(block);
1064    }
1065
1066    /// Replace the `cam` block WITHOUT an undo checkpoint — for a write that
1067    /// belongs to the checkpoint just taken (an import lifting a file's CAM
1068    /// data beside the feature it added, so one undo removes both).
1069    pub fn set_cam_block_no_undo(&mut self, block: Option<Value>) {
1070        self.put_cam_block(block);
1071    }
1072
1073    fn put_cam_block(&mut self, block: Option<Value>) {
1074        if let Some(object) = self.document_mut().as_object_mut() {
1075            match block {
1076                Some(block) => {
1077                    object.insert("cam".into(), block);
1078                }
1079                None => {
1080                    object.shift_remove("cam");
1081                }
1082            }
1083        }
1084    }
1085}
1086
1087// ============================================================================
1088// SHEETS block accessors (the drawing-sheet slice).
1089//
1090// `sheets` is a top-level document key like `pmi` and `partAttributes`: a
1091// sheet places saved PMI views and adds no geometry, so the kernel never needs
1092// it and `HistoryRequest` has no field for it — the document's unknown-key
1093// pass-through ([`Self::request_json`] serializes the raw document) is what
1094// round-trips it through save, load and undo. Absent on every document without
1095// a sheet, so those files re-serialize byte-identically.
1096// ============================================================================
1097impl History {
1098    /// The document's `sheets` block (`{sheets, idCounter}`), if any.
1099    pub fn sheets_block(&self) -> Option<&Value> {
1100        self.document().get(crate::sheets::SHEETS)
1101    }
1102
1103    /// Replace (or, with `None`, remove) the `sheets` block as a USER edit:
1104    /// snapshotted for undo. `coalesce_key` groups a run of rapid same-target
1105    /// edits (dragging a placed view: `sheet:view:{id}`) into ONE undo entry,
1106    /// exactly as the PMI label drag's does.
1107    pub fn set_sheets_block(&mut self, block: Option<Value>, coalesce_key: Option<&str>) {
1108        self.checkpoint(coalesce_key);
1109        if let Some(object) = self.document_mut().as_object_mut() {
1110            match block {
1111                Some(block) => {
1112                    object.insert(crate::sheets::SHEETS.into(), block);
1113                }
1114                None => {
1115                    object.shift_remove(crate::sheets::SHEETS);
1116                }
1117            }
1118        }
1119    }
1120}
1121
1122// ============================================================================
1123// PART ATTRIBUTES block accessors — the BOM attributes of the document ITSELF.
1124//
1125// `partAttributes` is a top-level document key (see
1126// `engine_state::bom`'s attributes block), so a document carries the BOM data
1127// of the PART it is. On a part in an assembly's library that record is written
1128// through the library entry's embedded document; on the document you have OPEN
1129// it is written here, by the toolbar's Properties dialog. Same key, same
1130// shape, two doors — which is what makes a part's Part Number the same value
1131// whether it is read from the assembly's BOM or from the part's own tab.
1132// ============================================================================
1133impl History {
1134    /// The document's own `partAttributes` record, if any.
1135    pub fn part_attributes_block(&self) -> Option<&Value> {
1136        self.document().get(crate::engine_state::PART_ATTRIBUTES)
1137    }
1138
1139    /// Replace (or, with `None`, remove) the document's own `partAttributes`
1140    /// record as a USER edit: snapshotted for undo. `coalesce_key` groups a run
1141    /// of edits to ONE field (a typing run in the Properties dialog) into a
1142    /// single undo entry, exactly as the PMI block's does.
1143    pub fn set_part_attributes_block(&mut self, block: Option<Value>, coalesce_key: Option<&str>) {
1144        self.checkpoint(coalesce_key);
1145        if let Some(object) = self.document_mut().as_object_mut() {
1146            match block {
1147                Some(block) => {
1148                    object.insert(crate::engine_state::PART_ATTRIBUTES.into(), block);
1149                }
1150                None => {
1151                    object.shift_remove(crate::engine_state::PART_ATTRIBUTES);
1152                }
1153            }
1154        }
1155    }
1156}
1157
1158// ============================================================================
1159// Expressions / configurator accessors (appended — the expressions/parameters
1160// panel slice). A SEPARATE `impl` block so concurrent edits to the primary block
1161// don't conflict; purely additive over the existing history API.
1162//
1163// The history document carries an `expressions` source string — the variable
1164// sheet feature params evaluate against (a numeric param may be the string
1165// `"boxW"`, evaluated by the pipeline's shared expression env). The panel edits
1166// this and re-runs; the `configurator` object (typed named inputs) is exposed
1167// read-only for display.
1168// ============================================================================
1169impl History {
1170    /// The history document's `expressions` source string (empty when absent or
1171    /// stored as `null`). The panel's editor binds to this.
1172    pub fn expressions(&self) -> String {
1173        self.document()
1174            .get("expressions")
1175            .and_then(Value::as_str)
1176            .unwrap_or("")
1177            .to_string()
1178    }
1179
1180    /// Replace the `expressions` source string. Snapshotted for undo, coalescing a
1181    /// run of keystroke edits into ONE undo entry (like a slider drag) via the
1182    /// shared `"expressions"` coalesce key, so a distinct add/edit/roll starts a
1183    /// fresh entry. A no-op re-set (same text) still records under the same key.
1184    pub fn set_expressions(&mut self, expressions: &str) {
1185        self.checkpoint(Some("expressions"));
1186        if let Some(obj) = self.document_mut().as_object_mut() {
1187            obj.insert(
1188                "expressions".into(),
1189                Value::String(expressions.to_string()),
1190            );
1191        }
1192    }
1193
1194    /// The `configurator` object (typed named inputs), or `{}` when absent —
1195    /// read-only for the panel's display (deeper configurator editing deferred).
1196    pub fn configurator(&self) -> Value {
1197        self.document()
1198            .get("configurator")
1199            .cloned()
1200            .unwrap_or_else(|| Value::Object(serde_json::Map::new()))
1201    }
1202}
1203
1204// ============================================================================
1205// Feature `persistentData` accessors (appended — the engine-native sketch mode
1206// slice). A SEPARATE `impl` block (like the expressions accessors above) so
1207// concurrent edits don't conflict; purely additive over the primary history API.
1208//
1209// A feature's `persistentData` is the kernel-persisted, non-input state (a
1210// SKETCH feature stores its solved `{points, geometries, constraints}` under the
1211// `sketch` key and the plane `basis` there). Sketch mode reads that state on
1212// enter and writes the edited doc back on commit — mirroring how the ref-select
1213// slice reads/writes `inputParams` through `feature_params` / `set_feature_params`.
1214// ============================================================================
1215impl History {
1216    /// The `persistentData` document of the feature at `index` (`None` if the
1217    /// feature or the field is absent) — the read twin of [`Self::feature_params`].
1218    pub fn feature_persistent_data(&self, index: usize) -> Option<Value> {
1219        self.features().get(index)?.get("persistentData").cloned()
1220    }
1221
1222    /// Set a single `key` inside the feature-at-`index`'s `persistentData` object,
1223    /// creating (or replacing a non-object) `persistentData` as needed. Snapshotted
1224    /// for undo (a structural edit — never coalesced), like an add/delete.
1225    pub fn set_feature_persistent_field(&mut self, index: usize, key: &str, value: Value) {
1226        self.set_feature_persistent_field_coalesced(index, key, value, None);
1227    }
1228
1229    /// [`Self::set_feature_persistent_field`] with an optional COALESCE key: a
1230    /// run of same-key writes (a gizmo drag moving a spline anchor, frame after
1231    /// frame) records ONE undo entry, exactly as a slider drag on a param does.
1232    pub fn set_feature_persistent_field_coalesced(
1233        &mut self,
1234        index: usize,
1235        key: &str,
1236        value: Value,
1237        coalesce_key: Option<&str>,
1238    ) {
1239        if index >= self.len() {
1240            return;
1241        }
1242        self.checkpoint(coalesce_key);
1243        if let Some(feat) = self.features_mut().get_mut(index).and_then(Value::as_object_mut) {
1244            let entry = feat
1245                .entry("persistentData")
1246                .or_insert_with(|| Value::Object(serde_json::Map::new()));
1247            if !entry.is_object() {
1248                *entry = Value::Object(serde_json::Map::new());
1249            }
1250            if let Some(obj) = entry.as_object_mut() {
1251                obj.insert(key.to_string(), value);
1252            }
1253        }
1254    }
1255
1256    /// Write several `persistentData` fields of the feature at `index` as ONE
1257    /// user edit: one undo checkpoint covers them all, and there is none when
1258    /// every field already holds its value, so an action that changed nothing
1259    /// leaves no undo step that does nothing. Returns whether anything changed.
1260    ///
1261    /// Leaving a sketch writes its geometry, its dimension label offsets and
1262    /// its external references this way. Written one field at a time they were
1263    /// three undo steps, and only the third Ctrl+Z took the drawing back.
1264    pub fn set_feature_persistent_fields(&mut self, index: usize, fields: Vec<(&str, Value)>) -> bool {
1265        let Some(feature) = self.features().get(index) else {
1266            return false;
1267        };
1268        let current = feature.get("persistentData");
1269        if fields.iter().all(|(key, value)| current.and_then(|data| data.get(*key)) == Some(value)) {
1270            return false;
1271        }
1272        self.checkpoint(None);
1273        if let Some(feat) = self.features_mut().get_mut(index).and_then(Value::as_object_mut) {
1274            let entry = feat
1275                .entry("persistentData")
1276                .or_insert_with(|| Value::Object(serde_json::Map::new()));
1277            if !entry.is_object() {
1278                *entry = Value::Object(serde_json::Map::new());
1279            }
1280            if let Some(obj) = entry.as_object_mut() {
1281                for (key, value) in fields {
1282                    obj.insert(key.to_string(), value);
1283                }
1284            }
1285        }
1286        true
1287    }
1288
1289    /// The undo step on top of the stack right now, as a token for
1290    /// [`Self::retract`] and [`Self::set_feature_persistent_fields_in_step`];
1291    /// `None` when there is nothing to undo.
1292    pub fn undo_top(&self) -> Option<u64> {
1293        self.undo_stack.last().map(|snapshot| snapshot.serial)
1294    }
1295
1296    /// Take back undo step `step` as though its edit had never been made:
1297    /// restore the state it holds and DROP it, leaving no redo entry. Only while
1298    /// `step` is still the newest step; otherwise nothing happens and this
1299    /// returns false, and the caller undoes the ordinary way.
1300    ///
1301    /// Cancelling a sketch created in the same session uses it, so the cancel
1302    /// leaves no undo step behind: two presses, one of which brought the
1303    /// cancelled sketch back, before. It cannot restore a redo stack the step's
1304    /// own edit cleared when it was taken. Neither could the cancel it replaces.
1305    pub fn retract(&mut self, step: u64) -> bool {
1306        if self.refuse_user() {
1307            return false;
1308        }
1309        if self.undo_top() != Some(step) {
1310            return false;
1311        }
1312        let Some(snapshot) = self.undo_stack.pop() else {
1313            return false;
1314        };
1315        self.restore(snapshot);
1316        self.last_edit_key = None;
1317        true
1318    }
1319
1320    /// [`Self::set_feature_persistent_fields`] as part of undo step `step`,
1321    /// with no checkpoint of its own, while `step` is still the newest step;
1322    /// otherwise an ordinary write with its own step. Finishing a sketch
1323    /// created in the same session writes this way, so creating, drawing and
1324    /// finishing it is one undo step.
1325    pub fn set_feature_persistent_fields_in_step(
1326        &mut self,
1327        index: usize,
1328        fields: Vec<(&str, Value)>,
1329        step: u64,
1330    ) -> bool {
1331        if self.undo_top() != Some(step) {
1332            return self.set_feature_persistent_fields(index, fields);
1333        }
1334        let Some(feat) = self.features_mut().get_mut(index).and_then(Value::as_object_mut) else {
1335            return false;
1336        };
1337        let entry = feat
1338            .entry("persistentData")
1339            .or_insert_with(|| Value::Object(serde_json::Map::new()));
1340        if !entry.is_object() {
1341            *entry = Value::Object(serde_json::Map::new());
1342        }
1343        if let Some(obj) = entry.as_object_mut() {
1344            for (key, value) in fields {
1345                obj.insert(key.to_string(), value);
1346            }
1347        }
1348        true
1349    }
1350}
1351
1352// ============================================================================
1353// eCAD block accessors (the eCAD documents slice).
1354//
1355// The four eCAD workbenches keep their documents as top-level keys: `diagram`
1356// (the wiring diagram) and `pcb` (schematic and board) are blocks of an
1357// ASSEMBLY document, since a board is an assembly; `symbol` and `pads` are
1358// blocks of a PART document, since a part carries its own symbol and pads.
1359// Like `sheets`, the kernel never reads them and `HistoryRequest` has no field
1360// for them: the raw document is what carries them through save, load, undo and
1361// the assembly fold. Absent on every document the eCAD editors never wrote, so
1362// those files re-serialize byte-identically.
1363//
1364// None of these setters runs the history. The tab's dirty dot and the autosave
1365// see the edit through [`History::revision`], which every write here moves. The
1366// two symbol setters also carry a pin edit to the part's PORT features (the
1367// pins-and-ports block below), and the engine's `pump` runs the history when
1368// that moved a port.
1369// ============================================================================
1370
1371/// The assembly document's wiring diagram (an eCAD `Document`).
1372pub const DIAGRAM: &str = "diagram";
1373/// The assembly document's schematic and board (an eCAD `Document`).
1374pub const PCB: &str = "pcb";
1375/// The part document's schematic symbol (an eCAD `Symbol`). Defined AS the
1376/// kernel's key: the pin/port binding (`brep_kernel` `part_pins`) reads the same
1377/// block, so the two can never name different keys.
1378pub const SYMBOL: &str = brep_kernel::SYMBOL_BLOCK;
1379/// The part document's footprint pads (an eCAD `Footprint`), defined AS the
1380/// kernel's key for the same reason.
1381pub const PADS: &str = brep_kernel::PADS_BLOCK;
1382/// The part's declared connection points (`ports.rs`) — data, not history,
1383/// and a sibling of `symbol` and `pads`.
1384pub const PORTS: &str = brep_kernel::PORTS_BLOCK;
1385
1386impl History {
1387    /// The document's `diagram` block, if any.
1388    pub fn diagram_block(&self) -> Option<&Value> {
1389        self.document().get(DIAGRAM)
1390    }
1391
1392    /// The document's `pcb` block, if any.
1393    pub fn pcb_block(&self) -> Option<&Value> {
1394        self.document().get(PCB)
1395    }
1396
1397    /// The document's `symbol` block, if any.
1398    pub fn symbol_block(&self) -> Option<&Value> {
1399        self.document().get(SYMBOL)
1400    }
1401
1402    /// The document's `pads` block, if any.
1403    pub fn pads_block(&self) -> Option<&Value> {
1404        self.document().get(PADS)
1405    }
1406
1407    /// The document's `ports` block (its declared connection points), if any.
1408    pub fn ports_block(&self) -> Option<&Value> {
1409        self.document().get(PORTS)
1410    }
1411
1412    /// The part's declared connection points, in block order.
1413    pub fn declared_points(&self) -> Vec<brep_kernel::DeclaredPoint> {
1414        brep_kernel::declared_points(self.document())
1415    }
1416
1417    /// The symbol's pin labels in pin order, or `None` without a symbol.
1418    pub fn pin_labels(&self) -> Option<Vec<String>> {
1419        brep_kernel::pin_labels(self.document())
1420    }
1421
1422    /// The symbol's pins with the UNIT each belongs to, in pin order, or `None`
1423    /// without a symbol. [`Self::pin_labels`] with the unit kept, for a caller
1424    /// that must say WHICH group a pin binds into.
1425    pub fn pins(&self) -> Option<Vec<brep_kernel::DeclaredPin>> {
1426        brep_kernel::pins(self.document())
1427    }
1428
1429    /// The port group the pins of symbol unit `unit` bind into, when one is
1430    /// mapped. The unit map's rule is the kernel's ([`brep_kernel::unit_groups`]).
1431    pub fn port_group_for_unit(&self, unit: u32) -> Option<String> {
1432        brep_kernel::port_group_for_unit(self.document(), unit)
1433    }
1434
1435    /// Replace (or, with `None`, remove) the `diagram` block as a USER edit.
1436    /// See `Self::set_ecad_block` for `coalesce_key`.
1437    pub fn set_diagram_block(&mut self, block: Option<Value>, coalesce_key: Option<&str>) {
1438        self.set_ecad_block(DIAGRAM, block, coalesce_key);
1439    }
1440
1441    /// Replace (or, with `None`, remove) the `pcb` block as a USER edit.
1442    /// See `Self::set_ecad_block` for `coalesce_key`.
1443    pub fn set_pcb_block(&mut self, block: Option<Value>, coalesce_key: Option<&str>) {
1444        self.set_ecad_block(PCB, block, coalesce_key);
1445    }
1446
1447    /// Replace (or, with `None`, remove) the `symbol` block as a USER edit,
1448    /// and follow it to the part's ports in the same undo step (see
1449    /// `Self::follow_pins`). See `Self::set_ecad_block` for `coalesce_key`.
1450    pub fn set_symbol_block(&mut self, block: Option<Value>, coalesce_key: Option<&str>) {
1451        self.set_ecad_block(SYMBOL, block, coalesce_key);
1452        self.follow_pins(None);
1453    }
1454
1455    /// Replace (or, with `None`, remove) the `pads` block as a USER edit.
1456    /// See `Self::set_ecad_block` for `coalesce_key`.
1457    pub fn set_pads_block(&mut self, block: Option<Value>, coalesce_key: Option<&str>) {
1458        self.set_ecad_block(PADS, block, coalesce_key);
1459    }
1460
1461    /// Write the `symbol` block WITHOUT an undo checkpoint, for a write that
1462    /// belongs to the checkpoint just taken: a KiCad import adds its Import 3D
1463    /// feature (one checkpoint) and writes the symbol and pads it read beside
1464    /// it, so one undo removes the whole import. The PMI import's
1465    /// [`Self::set_pmi_block_no_undo`] is the precedent.
1466    ///
1467    /// Skipping the checkpoint is deliberate, and costs the tab nothing: the
1468    /// write still goes through `document_mut`, which moves the revision the
1469    /// unsaved dot and the autosave key on.
1470    pub fn set_symbol_block_no_undo(&mut self, block: Option<Value>) {
1471        // No checkpoint marks where this write starts, so its base is the
1472        // document just before it.
1473        let base = self.document().clone();
1474        self.put_ecad_block(SYMBOL, block);
1475        self.follow_pins(Some(base));
1476    }
1477
1478    /// The `pads` twin of [`Self::set_symbol_block_no_undo`].
1479    pub fn set_pads_block_no_undo(&mut self, block: Option<Value>) {
1480        self.put_ecad_block(PADS, block);
1481    }
1482
1483    /// Replace (or, with `None`, remove) the `ports` block as a USER edit, and
1484    /// follow it to the part's pins in the same undo step (see
1485    /// `Self::follow_ports`).
1486    pub fn set_ports_block(&mut self, block: Option<Value>, coalesce_key: Option<&str>) {
1487        self.set_ecad_block(PORTS, block, coalesce_key);
1488        self.follow_ports();
1489        self.ports_followed = true;
1490    }
1491
1492    /// The `ports` twin of [`Self::set_symbol_block_no_undo`] — for a write
1493    /// that belongs to the checkpoint just taken (the KiCad import).
1494    pub fn set_ports_block_no_undo(&mut self, block: Option<Value>) {
1495        self.put_ecad_block(PORTS, block);
1496        self.ports_followed = true;
1497    }
1498
1499    /// Write block `key`, snapshotted for undo. `coalesce_key` is eCAD's own
1500    /// `Change::coalesce`, passed as the editor reported it: typing in one
1501    /// field of one object reports `component:{uuid}:text` on every frame, and
1502    /// a run of writes with that key is ONE undo entry. It is namespaced by the
1503    /// block here (`diagram:component:{uuid}:text`), so a run in one block never
1504    /// swallows an edit to another. `None` (a drag, a placement, the editor's
1505    /// own undo) is its own step.
1506    fn set_ecad_block(&mut self, key: &str, block: Option<Value>, coalesce_key: Option<&str>) {
1507        let coalesce_key = coalesce_key.map(|change| format!("{key}:{change}"));
1508        self.checkpoint(coalesce_key.as_deref());
1509        self.put_ecad_block(key, block);
1510    }
1511
1512    fn put_ecad_block(&mut self, key: &str, block: Option<Value>) {
1513        if let Some(object) = self.document_mut().as_object_mut() {
1514            match block {
1515                Some(block) => {
1516                    object.insert(key.into(), block);
1517                }
1518                None => {
1519                    object.shift_remove(key);
1520                }
1521            }
1522        }
1523    }
1524}
1525
1526// ============================================================================
1527// Pins and points: a part's symbol pins ARE its declared connection points,
1528// bound by NAME (eCAD plan decision 8; `brep_kernel` `part_pins` holds the
1529// rules).
1530//
1531// The binding lives HERE, inside the writes, so no caller can edit one side
1532// without the other: the symbol setters follow a pin edit to the points, and
1533// every mutator that can add, remove or rename a point follows it to the pins,
1534// inside the same undo step. Undo, redo and a replaced document need nothing,
1535// since a snapshot restores both sides together.
1536//
1537// Each follow diffs against the RUN BASE: the document the current coalesced
1538// run started from. Typing `10` over pin `2` is four writes under one eCAD key
1539// (`2`, ``, `1`, `10`). The two in the middle cannot bind (no label, and a
1540// label pin 1 already has), so they are HELD: the user's edit stands, the
1541// other side keeps what the last bound write gave it, and `pin_port_hold`
1542// names why. Replaying from the base makes the last keystroke decide
1543// whichever came before it.
1544// ============================================================================
1545
1546impl History {
1547    /// Why the latest pin or port edit was not followed to the other side, in
1548    /// words that name the label (`two pins are labelled '1'`), or `None`.
1549    pub fn pin_port_hold(&self) -> Option<&str> {
1550        self.pin_port_hold.as_deref()
1551    }
1552
1553    /// Everything about this part's pins and connection points that does not
1554    /// pair, by name; `None` for a document without a symbol.
1555    pub fn pin_point_report(&self) -> Option<brep_kernel::PinPointReport> {
1556        brep_kernel::pin_point_report(self.document())
1557    }
1558
1559    /// Whether a symbol edit has changed the declared points since the history
1560    /// last ran, clearing it. The engine re-runs when it was set.
1561    pub(crate) fn take_ports_followed(&mut self) -> bool {
1562        std::mem::take(&mut self.ports_followed)
1563    }
1564
1565    /// Whether the next [`crate::engine_state::EngineState::pump`] will re-run
1566    /// for a moved point, without clearing it — so the app can hand a document
1567    /// that is not in front its own parts library before that run is submitted.
1568    pub fn ports_follow_pending(&self) -> bool {
1569        self.ports_followed
1570    }
1571
1572    /// The document the current coalesced run started from. Right after a
1573    /// checkpointing write it is the top of the undo stack: [`Self::checkpoint`]
1574    /// pushes the pre-write state unless the write joins a run, and a run's
1575    /// first write pushed the state the run started from. Eviction takes the
1576    /// OLDEST entry, never the top. The stack is never empty after a
1577    /// checkpoint, so the fallback (the current document, which binds nothing)
1578    /// is never taken by a follow. Borrowed: a follow runs on every write of a
1579    /// drag, so the base is compared in place and never copied.
1580    fn run_base(&self) -> &Value {
1581        self.undo_stack
1582            .last()
1583            .map(|snapshot| &snapshot.request)
1584            .unwrap_or_else(|| self.document())
1585    }
1586
1587    /// Carry the symbol edit between the base (`base`, else the run base) and
1588    /// now to the connection points and the pads: a renamed pin renames its
1589    /// point in place (so the point keeps its placement), a removed pin removes
1590    /// its point, and a new pin gets a new point in the part's first port
1591    /// group.
1592    fn follow_pins(&mut self, base: Option<Value>) {
1593        let mut document = self.document().clone();
1594        let outcome = {
1595            let base = base.as_ref().unwrap_or_else(|| self.run_base());
1596            brep_kernel::follow_pin_edit(base, &mut document)
1597        };
1598        self.settle(outcome, document);
1599    }
1600
1601    /// Carry a connection-point edit between the run base and now to the pins
1602    /// and pads. Only an edit that changed the declared points is followed, so
1603    /// an unrelated edit neither binds nor clears a standing hold.
1604    fn follow_ports(&mut self) {
1605        if self.symbol_block().is_none() {
1606            return;
1607        }
1608        if brep_kernel::declared_points(self.run_base()) == brep_kernel::declared_points(self.document()) {
1609            return;
1610        }
1611        let mut document = self.document().clone();
1612        let outcome = brep_kernel::follow_point_edit(self.run_base(), &mut document);
1613        self.settle(outcome, document);
1614    }
1615
1616    /// Adopt a follow's result, or record why it was held. The write goes
1617    /// through `document_mut`, not a checkpoint: it belongs to the step the
1618    /// user's own write just took. The rolled-to step keeps its meaning: a
1619    /// model rolled to its last feature is still rolled to its last feature
1620    /// once a port is appended or removed, and one rolled back stays on the
1621    /// same feature.
1622    fn settle(&mut self, outcome: Result<Vec<brep_kernel::PinPointChange>, String>, document: Value) {
1623        let changes = match outcome {
1624            Ok(changes) => changes,
1625            Err(reason) => {
1626                self.pin_port_hold = Some(reason);
1627                return;
1628            }
1629        };
1630        self.pin_port_hold = None;
1631        if changes.is_empty() {
1632            return;
1633        }
1634        let moves_a_port = changes.iter().any(|change| {
1635            use brep_kernel::PinPointChange::*;
1636            // Exhaustive on purpose: a new kind of change must say whether
1637            // it touches the geometry a run publishes.
1638            match change {
1639                PointRenamed { .. } | PointAdded { .. } | PointRemoved { .. } => true,
1640                PinRelabelled { .. } | PinAdded { .. } | PinRemoved { .. }
1641                | PadsRenumbered { .. } | PadsAmbiguous { .. } => false,
1642            }
1643        });
1644        let at_end = self.rollback + 1 >= self.len();
1645        let rolled_to = self.feature_id(self.rollback());
1646        *self.document_mut() = document;
1647        let len = self.len();
1648        self.rollback = if len == 0 {
1649            0
1650        } else if at_end {
1651            len - 1
1652        } else {
1653            rolled_to
1654                .and_then(|id| self.index_of(&id))
1655                .unwrap_or_else(|| self.rollback.saturating_sub(1))
1656                .min(len - 1)
1657        };
1658        self.ports_followed |= moves_a_port;
1659    }
1660}
1661
1662
1663// ============================================================================
1664// Named top-level blocks the APP owns (appended — the document-classes slice).
1665// The history holds the document as a JSON value, so a key the kernel never
1666// reads (`documentClass`, a family's `familyTable`, a template's
1667// `templateInputs`, a member's `familySource`) rides save, open and every undo
1668// snapshot untouched. These are the doors that change one: the user-edit one
1669// records an undo step, the other is for a write that is bookkeeping rather
1670// than an edit (the class a load reads off the file's extension).
1671// ============================================================================
1672impl History {
1673    /// The document's top-level block `key`, if present.
1674    pub fn document_block(&self, key: &str) -> Option<&Value> {
1675        self.document().get(key)
1676    }
1677
1678    /// Replace (or, with `None`, remove) top-level block `key` as a USER edit:
1679    /// snapshotted for undo. `coalesce_key` groups a typing run into one step,
1680    /// namespaced by the block so a run in one never swallows another's.
1681    pub fn set_document_block(&mut self, key: &str, block: Option<Value>, coalesce_key: Option<&str>) {
1682        let coalesce_key = coalesce_key.map(|change| format!("block:{key}:{change}"));
1683        self.checkpoint(coalesce_key.as_deref());
1684        self.set_document_block_no_undo(key, block);
1685    }
1686
1687    /// [`Self::set_document_block`] with no undo step.
1688    pub fn set_document_block_no_undo(&mut self, key: &str, block: Option<Value>) {
1689        if let Some(object) = self.document_mut().as_object_mut() {
1690            match block {
1691                Some(block) => {
1692                    object.insert(key.to_string(), block);
1693                }
1694                None => {
1695                    // `shift_remove`: with `preserve_order`, `remove` moves the
1696                    // last key into the gap, and the reordered document then
1697                    // reads as an unsaved change.
1698                    object.shift_remove(key);
1699                }
1700            }
1701        }
1702    }
1703}
1704