1use 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#[derive(Debug, Default)]
29pub struct Reshape {
30 requests: FastMap<TShapeId, (Location, Option<Shape>)>,
33 at: FastMap<SameKey, Option<Shape>>,
36 splits: FastMap<TShapeId, (Location, Vec<Shape>)>,
39}
40
41impl Reshape {
42 #[must_use]
44 pub fn new() -> Self {
45 Self::default()
46 }
47
48 pub fn replace(&mut self, old: &Shape, new: Shape) {
52 self.requests
53 .insert(old.node(), (old.location().clone(), Some(new)));
54 }
55
56 pub fn split(&mut self, old: &Shape, pieces: Vec<Shape>) {
61 self.splits
62 .insert(old.node(), (old.location().clone(), pieces));
63 }
64
65 pub fn remove(&mut self, old: &Shape) {
67 self.requests
68 .insert(old.node(), (old.location().clone(), None));
69 }
70
71 pub fn replace_at(&mut self, occurrence: &Shape, new: Shape) {
76 self.at.insert(SameKey(occurrence.clone()), Some(new));
77 }
78
79 pub fn remove_at(&mut self, occurrence: &Shape) {
81 self.at.insert(SameKey(occurrence.clone()), None);
82 }
83
84 #[must_use]
86 pub fn len(&self) -> usize {
87 self.requests.len() + self.splits.len() + self.at.len()
88 }
89
90 #[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 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 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 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 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 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#[derive(Default)]
294struct Memo {
295 done: FastMap<SameKey, Option<Shape>>,
296 made: FastMap<(TShapeId, Vec<Shape>), Shape>,
297}
298
299fn 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 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 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 assert!(!rebuilt.history.modified(&solid).is_empty());
357 }
358
359 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 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 #[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 #[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}