Skip to main content

ogeom_heal/
reshape.rs

1//! The reshape framework: record substitutions, apply them over a shape in
2//! one pass.
3//!
4//! Healing operations want to say "this edge becomes that one, this face
5//! goes away" and have the change ripple upward (every wire holding the
6//! edge rebuilt, every face holding the wire, up to the solid) without
7//! each fix reimplementing the traversal. A [`Reshape`] collects the
8//! requests; [`Reshape::apply`] rebuilds bottom-up, sharing rebuilt nodes
9//! so a substituted edge is one new node however many faces reach it, and
10//! reports what became of every input through the ordinary history.
11//!
12//! A node can stand in a shape at more than one placement (an assembly
13//! placing one part twice). A request on a node ([`Reshape::replace`]) is
14//! stated in the frame of the occurrence it names, and every occurrence of
15//! the node takes it, carried by the placement between the two: each copy
16//! changes alike. A request on one occurrence ([`Reshape::replace_at`])
17//! changes that copy alone; every other keeps its nodes. A node rebuilt
18//! around changed children is made in its own frame and placed where its
19//! occurrence stands, and occurrences rebuilt alike share the new node.
20
21use ogeom_core::FastMap;
22
23use ogeom_algo::{Built, History};
24use ogeom_core::{OgeomResult, ogeom_bail};
25use ogeom_topo::{Location, Model, NodeData, Orientation, SameKey, Shape, ShapeType, TShapeId};
26
27/// A batch of substitutions, applied in one rebuild.
28#[derive(Debug, Default)]
29pub struct Reshape {
30    /// Per node, the occurrence named and what it becomes: `None` removes
31    /// it, `Some` replaces it, stated in the named occurrence's frame.
32    requests: FastMap<TShapeId, (Location, Option<Shape>)>,
33    /// Per occurrence alone (node and placement), what it becomes, stated
34    /// in the frame of the shape the batch is applied to.
35    at: FastMap<SameKey, Option<Shape>>,
36    /// Nodes cut into pieces, in their forward order, with the placement
37    /// of the occurrence named.
38    splits: FastMap<TShapeId, (Location, Vec<Shape>)>,
39}
40
41impl Reshape {
42    /// An empty batch.
43    #[must_use]
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Replace every occurrence of `old`'s node with `new`, stated in the
49    /// frame `old` is given in: an occurrence placed elsewhere takes `new`
50    /// carried by the placement taking `old` onto it.
51    pub fn replace(&mut self, old: &Shape, new: Shape) {
52        self.requests
53            .insert(old.node(), (old.location().clone(), Some(new)));
54    }
55
56    /// Replace every occurrence of `old`'s node with `pieces`, in order: an
57    /// edge cut in a wire, a face cut in a shell. A reversed occurrence
58    /// takes the pieces reversed, in reverse order. The pieces are stated
59    /// in `old`'s frame, as in [`Reshape::replace`].
60    pub fn split(&mut self, old: &Shape, pieces: Vec<Shape>) {
61        self.splits
62            .insert(old.node(), (old.location().clone(), pieces));
63    }
64
65    /// Remove every occurrence of `old`'s node.
66    pub fn remove(&mut self, old: &Shape) {
67        self.requests
68            .insert(old.node(), (old.location().clone(), None));
69    }
70
71    /// Replace `occurrence` alone, at the placement it has in the shape
72    /// the batch is applied to, with `new`, stated in that shape's frame.
73    /// Other occurrences of the node keep it. Takes precedence over a
74    /// request on the node.
75    pub fn replace_at(&mut self, occurrence: &Shape, new: Shape) {
76        self.at.insert(SameKey(occurrence.clone()), Some(new));
77    }
78
79    /// Remove `occurrence` alone, as [`Reshape::replace_at`] names it.
80    pub fn remove_at(&mut self, occurrence: &Shape) {
81        self.at.insert(SameKey(occurrence.clone()), None);
82    }
83
84    /// How many replacements are staged.
85    #[must_use]
86    pub fn len(&self) -> usize {
87        self.requests.len() + self.splits.len() + self.at.len()
88    }
89
90    /// Whether anything is requested.
91    #[must_use]
92    pub fn is_empty(&self) -> bool {
93        self.requests.is_empty() && self.splits.is_empty() && self.at.is_empty()
94    }
95
96    /// Apply the batch over `shape`, rebuilding what the substitutions
97    /// touch and sharing everything they do not.
98    ///
99    /// # Errors
100    ///
101    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if
102    /// a rebuilt container ends up empty where the model forbids it, or a
103    /// substitution's kind does not fit its slot.
104    pub fn apply(&self, model: &mut Model, shape: &Shape) -> OgeomResult<Built> {
105        let mut memo = Memo::default();
106        let mut history = History::new();
107        let Some(result) = self.rebuilt(model, shape, &mut memo, &mut history)? else {
108            ogeom_bail!(Construction, "the reshape removed the shape itself");
109        };
110        for (old, (from, request)) in &self.requests {
111            let stand_in = Shape::new(*old, from.clone(), Orientation::Forward);
112            match request {
113                Some(new) => history.modify(&stand_in, new.clone()),
114                None => history.delete(&stand_in),
115            }
116        }
117        for (occurrence, request) in &self.at {
118            match request {
119                Some(new) => history.modify(&occurrence.0, new.clone()),
120                None => history.delete(&occurrence.0),
121            }
122        }
123        for (old, (from, pieces)) in &self.splits {
124            let stand_in = Shape::new(*old, from.clone(), Orientation::Forward);
125            for piece in pieces {
126                history.modify(&stand_in, piece.clone());
127            }
128        }
129        Ok(Built::new(result, history))
130    }
131
132    /// The rebuilt occurrence of `shape`, `None` if it is removed.
133    fn rebuilt(
134        &self,
135        model: &mut Model,
136        shape: &Shape,
137        memo: &mut Memo,
138        history: &mut History,
139    ) -> OgeomResult<Option<Shape>> {
140        let oriented = |new: &Shape| {
141            if shape.orientation() == Orientation::Reversed {
142                new.reversed()
143            } else {
144                new.clone()
145            }
146        };
147        // A direct request wins, orientation carried from the occurrence.
148        let key = SameKey(shape.clone());
149        if let Some(request) = self.at.get(&key) {
150            return Ok(request.as_ref().map(oriented));
151        }
152        if let Some((from, request)) = self.requests.get(&shape.node()) {
153            return Ok(request
154                .as_ref()
155                .map(|new| oriented(&carried(new, from, shape.location()))));
156        }
157        if let Some(held) = memo.done.get(&key) {
158            return Ok(held.as_ref().map(oriented));
159        }
160
161        // Rebuild children; if none changed, the node itself is shared.
162        // Read through the forward occurrence: `children_of` composes the
163        // occurrence's orientation into every child, and the rebuilt node is
164        // oriented as the occurrence once, below. Read through a reversed
165        // occurrence, a reversed edge rebuilt around a substituted vertex
166        // comes out with its start and end swapped and is then reversed
167        // again, so the wire it sits in walks it against its neighbours.
168        let forward = if shape.orientation() == Orientation::Reversed {
169            shape.reversed()
170        } else {
171            shape.clone()
172        };
173        let children = model.children_of(&forward)?;
174        let mut rebuilt_children = Vec::with_capacity(children.len());
175        let mut changed = false;
176        for child in &children {
177            if let Some((from, pieces)) = self.splits.get(&child.node()) {
178                changed = true;
179                let pieces = pieces
180                    .iter()
181                    .map(|piece| carried(piece, from, child.location()));
182                if child.orientation() == Orientation::Reversed {
183                    let pieces: Vec<Shape> = pieces.collect();
184                    rebuilt_children.extend(pieces.iter().rev().map(Shape::reversed));
185                } else {
186                    rebuilt_children.extend(pieces);
187                }
188                continue;
189            }
190            match self.rebuilt(model, child, memo, history)? {
191                Some(new) => {
192                    if new.node() != child.node() {
193                        changed = true;
194                    }
195                    rebuilt_children.push(new);
196                }
197                None => changed = true,
198            }
199        }
200        if !changed {
201            memo.done.insert(key, Some(forward));
202            return Ok(Some(shape.clone()));
203        }
204
205        // The node is made in its own frame, its children read relative to
206        // the occurrence, and placed where the occurrence stands.
207        let own = forward.location().clone();
208        if !own.is_identity() {
209            let back = own.inverted();
210            for child in &mut rebuilt_children {
211                *child = Shape::new(
212                    child.node(),
213                    back.then(child.location()),
214                    child.orientation(),
215                );
216            }
217        }
218        let alike = (shape.node(), rebuilt_children);
219        if let Some(fresh) = memo.made.get(&alike) {
220            let placed = fresh.located(own);
221            history.modify(shape, placed.clone());
222            memo.done.insert(key, Some(placed.clone()));
223            return Ok(Some(oriented(&placed)));
224        }
225        let rebuilt_children = &alike.1;
226
227        let kind = model.kind_of(shape)?;
228        let data = {
229            let Some(node) = model.node(shape) else {
230                ogeom_bail!(Construction, "shape is not in this model");
231            };
232            node.data().clone()
233        };
234        let fresh = match (kind, data) {
235            (_, NodeData::Face(face)) => {
236                if rebuilt_children.is_empty() {
237                    memo.done.insert(key, None);
238                    return Ok(None);
239                }
240                model.add_face(*face, rebuilt_children)?
241            }
242            (_, NodeData::Edge(edge)) => {
243                if rebuilt_children.is_empty() {
244                    memo.done.insert(key, None);
245                    return Ok(None);
246                }
247                model.add_edge(*edge, rebuilt_children)?
248            }
249            (ShapeType::Wire, NodeData::Container) => {
250                if rebuilt_children.is_empty() {
251                    memo.done.insert(key, None);
252                    return Ok(None);
253                }
254                model.add_wire(rebuilt_children)?
255            }
256            (ShapeType::Shell, NodeData::Container) => {
257                if rebuilt_children.is_empty() {
258                    memo.done.insert(key, None);
259                    return Ok(None);
260                }
261                model.add_shell(rebuilt_children)?
262            }
263            (ShapeType::Solid, NodeData::Container) => {
264                if rebuilt_children.is_empty() {
265                    memo.done.insert(key, None);
266                    return Ok(None);
267                }
268                model.add_solid(rebuilt_children)?
269            }
270            (ShapeType::Compound, NodeData::Container) => model.add_compound(rebuilt_children)?,
271            (other, _) => {
272                ogeom_bail!(
273                    Construction,
274                    "a {other:?} cannot be rebuilt around substituted children"
275                );
276            }
277        };
278        memo.made.insert(alike, fresh.clone());
279        let placed = if own.is_identity() {
280            fresh
281        } else {
282            fresh.located(own)
283        };
284        history.modify(shape, placed.clone());
285        memo.done.insert(key, Some(placed.clone()));
286        Ok(Some(oriented(&placed)))
287    }
288}
289
290/// What [`Reshape::rebuilt`] has answered: per occurrence, its rebuilt
291/// form; per node and its children as rebuilt (relative to it), the node
292/// made, so occurrences rebuilt alike share it.
293#[derive(Default)]
294struct Memo {
295    done: FastMap<SameKey, Option<Shape>>,
296    made: FastMap<(TShapeId, Vec<Shape>), Shape>,
297}
298
299/// `new`, stated in the frame of an occurrence at `from`, as it stands for
300/// an occurrence at `to`.
301fn carried(new: &Shape, from: &Location, to: &Location) -> Shape {
302    let by = to.then(&from.inverted());
303    if by.is_identity() {
304        return new.clone();
305    }
306    Shape::new(new.node(), by.then(new.location()), new.orientation())
307}
308
309#[cfg(test)]
310#[allow(clippy::unwrap_used, clippy::expect_used)]
311mod tests {
312    use super::*;
313    use ogeom_core::Tolerances;
314    use ogeom_geom::{PlaneSurface, SurfaceGeometry};
315    use ogeom_math::{Frame, Plane, Point, Transform, Vector};
316    use ogeom_topo::explore_unique;
317
318    const T: Tolerances = Tolerances::millimetres();
319
320    #[test]
321    fn a_substituted_vertex_ripples_to_the_top_and_shares_the_rest() {
322        let mut model = Model::new();
323        let solid = ogeom_algo::make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T)
324            .unwrap()
325            .shape;
326        let vertices = explore_unique(&model, &solid, ShapeType::Vertex).unwrap();
327        let corner = vertices
328            .iter()
329            .find(|v| {
330                model
331                    .node(v)
332                    .and_then(|n| n.data().as_vertex())
333                    .is_some_and(|data| data.point.distance(Point::ORIGIN) < 1e-9)
334            })
335            .expect("the origin corner")
336            .clone();
337        let moved = ogeom_algo::make_vertex(&mut model, Point::new(0.1, 0.0, 0.0)).shape;
338
339        let mut reshape = Reshape::new();
340        reshape.replace(&corner, moved.clone());
341        let rebuilt = reshape.apply(&mut model, &solid).unwrap();
342
343        // The new solid holds the new vertex and none of the old one.
344        let after = explore_unique(&model, &rebuilt.shape, ShapeType::Vertex).unwrap();
345        assert!(after.iter().any(|v| v.node() == moved.node()));
346        assert!(after.iter().all(|v| v.node() != corner.node()));
347        // Only the three faces at that corner rebuilt; the rest shared.
348        let before_faces = explore_unique(&model, &solid, ShapeType::Face).unwrap();
349        let after_faces = explore_unique(&model, &rebuilt.shape, ShapeType::Face).unwrap();
350        let shared = after_faces
351            .iter()
352            .filter(|f| before_faces.iter().any(|b| b.node() == f.node()))
353            .count();
354        assert_eq!(shared, 3, "the untouched half of the box is the same nodes");
355        // History names the substitution and the ripple.
356        assert!(!rebuilt.history.modified(&solid).is_empty());
357    }
358
359    /// A 2 mm square face and a copy of it placed 10 above, in one
360    /// compound; the copy, and the square's corner at the origin.
361    fn square_and_copy(model: &mut Model) -> (Shape, Shape, Shape) {
362        let corners =
363            [(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0)].map(|(x, y)| Point::new(x, y, 0.0));
364        let ring = ogeom_algo::make_polygon(model, &corners, true, T)
365            .unwrap()
366            .shape;
367        let plane = SurfaceGeometry::Plane(PlaneSurface::new(Plane::new(Frame::WORLD)));
368        let face = ogeom_algo::make_face(model, plane, &[ring], T)
369            .unwrap()
370            .shape;
371        let up = model.placed(&face, Transform::translation(Vector::new(0.0, 0.0, 10.0)));
372        let corner = explore_unique(model, &face, ShapeType::Vertex)
373            .unwrap()
374            .into_iter()
375            .find(|v| placed_point(model, v).distance(Point::ORIGIN) < 1e-12)
376            .unwrap();
377        let root = model.add_compound(&[face, up.clone()]).unwrap();
378        (root, up, corner)
379    }
380
381    fn placed_point(model: &Model, vertex: &Shape) -> Point {
382        let point = model
383            .node(vertex)
384            .unwrap()
385            .data()
386            .as_vertex()
387            .unwrap()
388            .point;
389        vertex.transform(model.datums()).unwrap().apply(point)
390    }
391
392    /// Every vertex of `shape` where it stands, by height then x.
393    fn points(model: &Model, shape: &Shape) -> Vec<(f64, f64)> {
394        let mut out: Vec<(f64, f64)> = explore_unique(model, shape, ShapeType::Vertex)
395            .unwrap()
396            .iter()
397            .map(|v| placed_point(model, v))
398            .map(|p| (p.z, p.x + 10.0 * p.y))
399            .collect();
400        out.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.total_cmp(&b.1)));
401        out
402    }
403
404    /// A vertex replaced by node reaches both copies, each where it stands,
405    /// and the copies share the face rebuilt around it.
406    #[test]
407    fn a_node_replaced_changes_every_placed_copy_where_it_stands() {
408        let mut model = Model::new();
409        let (root, _, corner) = square_and_copy(&mut model);
410        let moved = ogeom_algo::make_vertex(&mut model, Point::new(0.5, 0.0, 0.0)).shape;
411        let mut reshape = Reshape::new();
412        reshape.replace(&corner, moved);
413        let built = reshape.apply(&mut model, &root).unwrap().shape;
414        let want = [0.5, 2.0, 20.0, 22.0];
415        let got = points(&model, &built);
416        assert_eq!(got.len(), 8);
417        for (k, (z, at)) in got.iter().enumerate() {
418            let (wz, wat) = (if k < 4 { 0.0 } else { 10.0 }, want[k % 4]);
419            assert!(
420                (z - wz).abs() < 1e-12 && (at - wat).abs() < 1e-12,
421                "{got:?}"
422            );
423        }
424        let faces = explore_unique(&model, &built, ShapeType::Face).unwrap();
425        assert_eq!(faces.len(), 2);
426        assert_eq!(faces[0].node(), faces[1].node());
427    }
428
429    /// A vertex replaced at one copy alone changes that copy, where it
430    /// stands; the other keeps its nodes and its corner.
431    #[test]
432    fn an_occurrence_replaced_changes_that_copy_alone() {
433        let mut model = Model::new();
434        let (root, up, corner) = square_and_copy(&mut model);
435        let at_copy = corner.moved(up.location());
436        let moved = ogeom_algo::make_vertex(&mut model, Point::new(0.5, 0.0, 0.0)).shape;
437        let mut reshape = Reshape::new();
438        reshape.replace_at(&at_copy, moved.located(up.location().clone()));
439        let built = reshape.apply(&mut model, &root).unwrap().shape;
440        let got = points(&model, &built);
441        let want = [(0.0, 0.0), (0.0, 2.0), (0.0, 20.0), (0.0, 22.0)]
442            .into_iter()
443            .chain([0.5, 2.0, 20.0, 22.0].map(|x| (10.0, x)));
444        for ((z, at), (wz, wat)) in got.iter().zip(want) {
445            assert!(
446                (z - wz).abs() < 1e-12 && (at - wat).abs() < 1e-12,
447                "{got:?}"
448            );
449        }
450        let faces = explore_unique(&model, &built, ShapeType::Face).unwrap();
451        let below = faces.iter().find(|f| f.location().is_identity()).unwrap();
452        let original = &model.children_of(&root).unwrap()[0];
453        assert_eq!(
454            below.node(),
455            original.node(),
456            "the copy below keeps its nodes"
457        );
458        assert_ne!(faces[0].node(), faces[1].node());
459    }
460}