1use std::collections::HashMap;
13
14use ogeom_algo::{Built, History};
15use ogeom_core::{OgeomResult, ogeom_bail};
16use ogeom_topo::{Model, NodeData, Orientation, Shape, ShapeType, TShapeId};
17
18#[derive(Debug, Default)]
20pub struct Reshape {
21 requests: HashMap<TShapeId, Option<Shape>>,
23 splits: HashMap<TShapeId, Vec<Shape>>,
25}
26
27impl Reshape {
28 #[must_use]
30 pub fn new() -> Self {
31 Self::default()
32 }
33
34 pub fn replace(&mut self, old: &Shape, new: Shape) {
36 self.requests.insert(old.node(), Some(new));
37 }
38
39 pub fn split(&mut self, old: &Shape, pieces: Vec<Shape>) {
43 self.splits.insert(old.node(), pieces);
44 }
45
46 pub fn remove(&mut self, old: &Shape) {
48 self.requests.insert(old.node(), None);
49 }
50
51 #[must_use]
53 pub fn len(&self) -> usize {
54 self.requests.len() + self.splits.len()
55 }
56
57 #[must_use]
59 pub fn is_empty(&self) -> bool {
60 self.requests.is_empty() && self.splits.is_empty()
61 }
62
63 pub fn apply(&self, model: &mut Model, shape: &Shape) -> OgeomResult<Built> {
72 let mut memo: HashMap<TShapeId, Option<Shape>> = HashMap::new();
73 let mut history = History::new();
74 let Some(result) = self.rebuilt(model, shape, &mut memo, &mut history)? else {
75 ogeom_bail!(Construction, "the reshape removed the shape itself");
76 };
77 for (old, request) in &self.requests {
78 let stand_in = Shape::of(*old);
79 match request {
80 Some(new) => history.modify(&stand_in, new.clone()),
81 None => history.delete(&stand_in),
82 }
83 }
84 for (old, pieces) in &self.splits {
85 let stand_in = Shape::of(*old);
86 for piece in pieces {
87 history.modify(&stand_in, piece.clone());
88 }
89 }
90 Ok(Built::new(result, history))
91 }
92
93 fn rebuilt(
95 &self,
96 model: &mut Model,
97 shape: &Shape,
98 memo: &mut HashMap<TShapeId, Option<Shape>>,
99 history: &mut History,
100 ) -> OgeomResult<Option<Shape>> {
101 if let Some(request) = self.requests.get(&shape.node()) {
103 return Ok(request.as_ref().map(|new| {
104 if shape.orientation() == Orientation::Reversed {
105 new.reversed()
106 } else {
107 new.clone()
108 }
109 }));
110 }
111 if let Some(held) = memo.get(&shape.node()) {
112 return Ok(held.as_ref().map(|new| {
113 if shape.orientation() == Orientation::Reversed {
114 new.reversed()
115 } else {
116 new.clone()
117 }
118 }));
119 }
120
121 let forward = if shape.orientation() == Orientation::Reversed {
129 shape.reversed()
130 } else {
131 shape.clone()
132 };
133 let children = model.children_of(&forward)?;
134 let mut rebuilt_children = Vec::with_capacity(children.len());
135 let mut changed = false;
136 for child in &children {
137 if let Some(pieces) = self.splits.get(&child.node()) {
138 changed = true;
139 if child.orientation() == Orientation::Reversed {
140 rebuilt_children.extend(pieces.iter().rev().map(Shape::reversed));
141 } else {
142 rebuilt_children.extend(pieces.iter().cloned());
143 }
144 continue;
145 }
146 match self.rebuilt(model, child, memo, history)? {
147 Some(new) => {
148 if new.node() != child.node() {
149 changed = true;
150 }
151 rebuilt_children.push(new);
152 }
153 None => changed = true,
154 }
155 }
156 if !changed {
157 memo.insert(shape.node(), Some(forward));
158 return Ok(Some(shape.clone()));
159 }
160
161 let kind = model.kind_of(shape)?;
162 let data = {
163 let Some(node) = model.node(shape) else {
164 ogeom_bail!(Construction, "shape is not in this model");
165 };
166 node.data().clone()
167 };
168 let fresh = match (kind, data) {
169 (_, NodeData::Face(face)) => {
170 if rebuilt_children.is_empty() {
171 memo.insert(shape.node(), None);
172 return Ok(None);
173 }
174 model.add_face(*face, &rebuilt_children)?
175 }
176 (_, NodeData::Edge(edge)) => {
177 if rebuilt_children.is_empty() {
178 memo.insert(shape.node(), None);
179 return Ok(None);
180 }
181 model.add_edge(*edge, &rebuilt_children)?
182 }
183 (ShapeType::Wire, NodeData::Container) => {
184 if rebuilt_children.is_empty() {
185 memo.insert(shape.node(), None);
186 return Ok(None);
187 }
188 model.add_wire(&rebuilt_children)?
189 }
190 (ShapeType::Shell, NodeData::Container) => {
191 if rebuilt_children.is_empty() {
192 memo.insert(shape.node(), None);
193 return Ok(None);
194 }
195 model.add_shell(&rebuilt_children)?
196 }
197 (ShapeType::Solid, NodeData::Container) => {
198 if rebuilt_children.is_empty() {
199 memo.insert(shape.node(), None);
200 return Ok(None);
201 }
202 model.add_solid(&rebuilt_children)?
203 }
204 (ShapeType::Compound, NodeData::Container) => model.add_compound(&rebuilt_children)?,
205 (other, _) => {
206 ogeom_bail!(
207 Construction,
208 "a {other:?} cannot be rebuilt around substituted children"
209 );
210 }
211 };
212 let oriented = if shape.orientation() == Orientation::Reversed {
213 fresh.reversed()
214 } else {
215 fresh.clone()
216 };
217 history.modify(shape, fresh.clone());
218 memo.insert(shape.node(), Some(fresh));
219 Ok(Some(oriented))
220 }
221}
222
223#[cfg(test)]
224#[allow(clippy::unwrap_used, clippy::expect_used)]
225mod tests {
226 use super::*;
227 use ogeom_core::Tolerances;
228 use ogeom_math::{Frame, Point};
229 use ogeom_topo::explore_unique;
230
231 const T: Tolerances = Tolerances::millimetres();
232
233 #[test]
234 fn a_substituted_vertex_ripples_to_the_top_and_shares_the_rest() {
235 let mut model = Model::new();
236 let solid = ogeom_algo::make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T)
237 .unwrap()
238 .shape;
239 let vertices = explore_unique(&model, &solid, ShapeType::Vertex).unwrap();
240 let corner = vertices
241 .iter()
242 .find(|v| {
243 model
244 .node(v)
245 .and_then(|n| n.data().as_vertex())
246 .is_some_and(|data| data.point.distance(Point::ORIGIN) < 1e-9)
247 })
248 .expect("the origin corner")
249 .clone();
250 let moved = ogeom_algo::make_vertex(&mut model, Point::new(0.1, 0.0, 0.0)).shape;
251
252 let mut reshape = Reshape::new();
253 reshape.replace(&corner, moved.clone());
254 let rebuilt = reshape.apply(&mut model, &solid).unwrap();
255
256 let after = explore_unique(&model, &rebuilt.shape, ShapeType::Vertex).unwrap();
258 assert!(after.iter().any(|v| v.node() == moved.node()));
259 assert!(after.iter().all(|v| v.node() != corner.node()));
260 let before_faces = explore_unique(&model, &solid, ShapeType::Face).unwrap();
262 let after_faces = explore_unique(&model, &rebuilt.shape, ShapeType::Face).unwrap();
263 let shared = after_faces
264 .iter()
265 .filter(|f| before_faces.iter().any(|b| b.node() == f.node()))
266 .count();
267 assert_eq!(shared, 3, "the untouched half of the box is the same nodes");
268 assert!(!rebuilt.history.modified(&solid).is_empty());
270 }
271}