1use ogeom_algo::{
12 Built, attach_pcurve, edge_vertices, find_plane, is_wire_closed, make_face, make_solid,
13 make_wire, sew,
14};
15use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
16use ogeom_geom::{PlaneSurface, SurfaceGeometry};
17use ogeom_math::Plane;
18use ogeom_topo::{Filter, Location, Model, NodeData, Shape, ShapeType, TShapeId, explore};
19
20use crate::sheet::make_loft_surface;
21
22pub fn make_loft_solid(
50 model: &mut Model,
51 sections: &[Shape],
52 guides: &[Shape],
53 ruled: bool,
54 tol: Tolerances,
55) -> OgeomResult<Built> {
56 for (k, section) in sections.iter().enumerate() {
57 if !is_closed_section(model, section, tol)? {
58 ogeom_bail!(
59 Construction,
60 "a solid loft runs through closed sections; section {k} is not a closed edge or \
61 wire"
62 );
63 }
64 }
65 let sheet = make_loft_surface(model, sections, false, guides, ruled, tol)?.shape;
66 let mut faces = explore(model, &sheet, Filter::OfType(ShapeType::Face))?;
67 let ends = free_loops(model, &faces)?;
68 if ends.len() != 2 {
69 ogeom_bail!(
70 Construction,
71 "the loft's walls are open along {} loops, not at its two ends",
72 ends.len()
73 );
74 }
75 for ring in &ends {
76 let wire = make_wire(model, ring, tol)?.shape;
77 let Some(plane) = find_plane(model, &wire, tol)? else {
78 ogeom_bail!(
79 Construction,
80 "a solid loft is capped by its end sections' planes; an end section is not planar"
81 );
82 };
83 faces.push(cap(model, plane, ring, wire, tol)?);
84 }
85 let sewn = sew(model, &faces, tol)?;
86 if sewn.shells.len() != 1 || !sewn.free_edges.is_empty() {
87 ogeom_bail!(
88 Construction,
89 "the loft's walls and caps did not close into one shell"
90 );
91 }
92 let mut built = make_solid(model, &sewn.shells)?;
93 for section in sections.iter().chain(guides) {
94 built.history.generate(section, built.shape.clone());
95 }
96 Ok(built)
97}
98
99fn cap(
102 model: &mut Model,
103 plane: Plane,
104 edges: &[Shape],
105 wire: Shape,
106 tol: Tolerances,
107) -> OgeomResult<Shape> {
108 let surface: SurfaceGeometry = PlaneSurface::new(plane).into();
109 let face = make_face(model, surface.clone(), &[wire], tol)?.shape;
110 let Some(NodeData::Face(data)) = model.node(&face).map(|n| n.data()) else {
111 ogeom_bail!(Construction, "the cap holds no face data");
112 };
113 let surface_id = data.surface;
114 for edge in edges {
115 let (curve, range) = crate::sweep::spine_curve_of(model, edge)?;
116 let Some(pcurve) = ogeom_intersect::exact_pcurve_over(&curve, range, &surface, tol) else {
117 ogeom_bail!(
118 Construction,
119 "an end section's edge has no exact trim on the section's plane"
120 );
121 };
122 attach_pcurve(model, edge, pcurve, surface_id, Location::identity(), range)?;
123 }
124 Ok(face)
125}
126
127fn is_closed_section(model: &Model, section: &Shape, tol: Tolerances) -> OgeomResult<bool> {
129 match model.kind_of(section)? {
130 ShapeType::Wire => is_wire_closed(model, section, tol),
131 ShapeType::Edge => match edge_vertices(model, section)? {
132 Some((start, end)) => {
133 Ok(start.is_same(&end) || model.same_position(&start, &end, tol)?)
134 }
135 None => Ok(false),
136 },
137 _ => Ok(false),
138 }
139}
140
141fn free_loops(model: &Model, faces: &[Shape]) -> OgeomResult<Vec<Vec<Shape>>> {
145 let mut uses: Vec<(TShapeId, usize, Shape)> = Vec::new();
146 for face in faces {
147 for wire in model.children_of(face)? {
148 for edge in model.children_of(&wire)? {
149 if model
150 .node(&edge)
151 .and_then(|n| n.data().as_edge())
152 .is_some_and(|d| d.degenerate)
153 {
154 continue;
155 }
156 match uses.iter_mut().find(|(id, _, _)| *id == edge.node()) {
157 Some(entry) => entry.1 += 1,
158 None => uses.push((edge.node(), 1, edge)),
159 }
160 }
161 }
162 }
163 let mut free: Vec<(Shape, TShapeId, TShapeId)> = Vec::new();
164 for (_, count, edge) in uses {
165 if count % 2 == 0 {
166 continue;
167 }
168 let Some((start, end)) = edge_vertices(model, &edge)? else {
169 ogeom_bail!(Construction, "a loft wall's border edge has no vertices");
170 };
171 free.push((edge, start.node(), end.node()));
172 }
173 let mut loops = Vec::new();
174 while let Some((edge, first, mut at)) = free.pop() {
175 let mut ring = vec![edge];
176 while at != first {
177 let Some(next) = free.iter().position(|(_, a, b)| *a == at || *b == at) else {
178 ogeom_bail!(Construction, "a loft wall's border does not close");
179 };
180 let (edge, a, b) = free.swap_remove(next);
181 at = if a == at { b } else { a };
182 ring.push(edge);
183 }
184 loops.push(ring);
185 }
186 Ok(loops)
187}