1use crate::topology::{CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord};
23use crate::{
24 build_pcurve_on_surface, make_line, sew_solid, BodyKind, BrepSolid, KernelRefusal, KernelStage, NurbsCurve,
25 NurbsSurface, OrRefuse, Vec3,
26};
27
28pub const SURFACE_FACE_REFUSAL: &str = "surfaceFace";
30
31fn input(what: &'static str, message: String) -> KernelRefusal {
32 KernelRefusal::input(KernelStage::Collect, what, format!("{SURFACE_FACE_REFUSAL}: {message}"))
33}
34
35fn refuse(stage: KernelStage, what: &'static str, message: String) -> KernelRefusal {
36 KernelRefusal::unsupported(stage, what, format!("{SURFACE_FACE_REFUSAL}: {message}"))
37}
38
39struct Records {
41 vertices: Vec<VertexRecord>,
42 edges: Vec<EdgeRecord>,
43 next: u64,
44 tolerance: f64,
45}
46
47impl Records {
48 fn new(tolerance: f64) -> Self {
49 Self { vertices: Vec::new(), edges: Vec::new(), next: 0, tolerance }
50 }
51
52 fn id(&mut self) -> u64 {
53 self.next += 1;
54 self.next
55 }
56
57 fn vertex(&mut self, point: Vec3) -> u64 {
59 if let Some(found) = self.vertices.iter().find(|vertex| vertex.point.sub(point).length() <= self.tolerance) {
60 return found.id;
61 }
62 let id = self.id();
63 self.vertices.push(VertexRecord { id, point });
64 id
65 }
66
67 fn edge(&mut self, curve: NurbsCurve, start: u64, end: u64, degenerate: bool) -> Result<u64, KernelRefusal> {
68 let [t0, t1] = curve.domain().or_refuse(KernelStage::Collect, "surfaceFace.edge_domain")?;
69 let id = self.id();
70 self.edges.push(EdgeRecord { id, curve, t0, t1, start_vertex_id: start, end_vertex_id: end, degenerate, name: None });
71 Ok(id)
72 }
73
74 fn sheet(mut self, surface: &NurbsSurface, loops: Vec<Vec<(u64, bool, NurbsCurve)>>) -> BrepSolid {
75 let mut records = Vec::with_capacity(loops.len());
76 for coedges in loops {
77 let coedges = coedges
78 .into_iter()
79 .map(|(edge_id, forward, pcurve)| CoedgeRecord { id: self.id(), edge_id, forward, pcurve })
80 .collect();
81 records.push(LoopRecord { id: self.id(), coedges });
82 }
83 let face = FaceRecord { id: self.id(), surface: surface.clone(), same_sense: true, loops: records, name: None };
84 let shell = ShellRecord { id: self.id(), faces: vec![face] };
85 BrepSolid {
86 mass_properties_cache: Default::default(),
87 id: self.id(),
88 vertices: self.vertices,
89 edges: self.edges,
90 shells: vec![shell],
91 genus: 0,
92 kind: BodyKind::Sheet,
93 cells: Default::default(),
94 }
95 }
96}
97
98fn extent(surface: &NurbsSurface) -> f64 {
100 let (mut low, mut high) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
101 for pole in surface.control_points.iter().flatten() {
102 let point = [pole.x / pole.w, pole.y / pole.w, pole.z / pole.w];
103 for axis in 0..3 {
104 low[axis] = low[axis].min(point[axis]);
105 high[axis] = high[axis].max(point[axis]);
106 }
107 }
108 Vec3::new(high[0] - low[0], high[1] - low[1], high[2] - low[2]).length()
109}
110
111fn collapsed(curve: &NurbsCurve, tolerance: f64) -> Option<Vec3> {
113 let point = |index: usize| {
114 let pole = curve.control_points[index];
115 Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w)
116 };
117 let first = point(0);
118 (1..curve.control_points.len()).all(|index| point(index).sub(first).length() <= tolerance).then_some(first)
119}
120
121fn coincide(a: &NurbsCurve, b: &NurbsCurve, tolerance: f64) -> Result<bool, KernelRefusal> {
124 const SAMPLES: usize = 33;
125 let [t0, t1] = a.domain().or_refuse(KernelStage::Collect, "surfaceFace.side_domain")?;
126 for index in 0..=SAMPLES {
127 let t = t0 + (t1 - t0) * index as f64 / SAMPLES as f64;
128 let gap = a.evaluate(t).or_refuse(KernelStage::Collect, "surfaceFace.side")?
129 .sub(b.evaluate(t).or_refuse(KernelStage::Collect, "surfaceFace.side")?);
130 if gap.length() > tolerance {
131 return Ok(false);
132 }
133 }
134 Ok(true)
135}
136
137fn uv_line(from: [f64; 2], to: [f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
138 make_line(Vec3::new(from[0], from[1], 0.0), Vec3::new(to[0], to[1], 0.0)).or_refuse(KernelStage::Collect, "surfaceFace.pcurve")
139}
140
141fn accept(sheet: BrepSolid, what: &'static str) -> Result<BrepSolid, KernelRefusal> {
142 let issues = sheet.validate();
143 if let Some(issue) = issues.first() {
144 return Err(refuse(KernelStage::Validate, what, format!("the face is not valid: {issue:?}")));
145 }
146 Ok(sheet)
147}
148
149pub fn face_from_surface(surface: &NurbsSurface) -> Result<BrepSolid, KernelRefusal> {
162 let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Collect, "surfaceFace.domain")?;
163 let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Collect, "surfaceFace.domain")?;
164 let size = extent(surface);
165 if !(size.is_finite() && size > 0.0) {
166 return Err(input("surface_extent", "the surface has no extent".into()));
167 }
168 let tolerance = 1e-9 * size;
169 let side = |along_u: bool, at: f64| -> Result<NurbsCurve, KernelRefusal> {
170 if along_u { surface.iso_curve_v(at) } else { surface.iso_curve_u(at) }.or_refuse(KernelStage::Collect, "surfaceFace.side")
171 };
172 let (bottom, top) = (side(true, v0)?, side(true, v1)?);
174 let (left, right) = (side(false, u0)?, side(false, u1)?);
175 let poles = [collapsed(&bottom, tolerance), collapsed(&right, tolerance), collapsed(&top, tolerance), collapsed(&left, tolerance)];
176 if (0..4).any(|index| poles[index].is_some() && poles[(index + 1) % 4].is_some()) {
177 return Err(input("surface_collapsed", "two adjacent sides of the surface are single points".into()));
178 }
179 let seam_u = poles[1].is_none() && poles[3].is_none() && coincide(&left, &right, tolerance)?;
180 let seam_v = poles[0].is_none() && poles[2].is_none() && coincide(&bottom, &top, tolerance)?;
181
182 let mut records = Records::new(tolerance);
183 let corner = |u: f64, v: f64| surface.evaluate(u, v).or_refuse(KernelStage::Collect, "surfaceFace.corner");
184 let c00 = records.vertex(corner(u0, v0)?);
185 let c10 = records.vertex(corner(u1, v0)?);
186 let c11 = records.vertex(corner(u1, v1)?);
187 let c01 = records.vertex(corner(u0, v1)?);
188 let edge = |records: &mut Records, curve: NurbsCurve, pole: Option<Vec3>, start: u64, end: u64| -> Result<u64, KernelRefusal> {
189 match pole {
190 Some(point) => {
191 let line = make_line(point, point).or_refuse(KernelStage::Collect, "surfaceFace.pole")?;
192 records.edge(line, start, start, true)
193 }
194 None => records.edge(curve, start, end, false),
195 }
196 };
197 let bottom_edge = edge(&mut records, bottom, poles[0], c00, c10)?;
198 let left_edge = edge(&mut records, left, poles[3], c00, c01)?;
199 let top_edge = if seam_v { bottom_edge } else { edge(&mut records, top, poles[2], c01, c11)? };
200 let right_edge = if seam_u { left_edge } else { edge(&mut records, right, poles[1], c10, c11)? };
201 let coedges = vec![
204 (bottom_edge, true, uv_line([u0, v0], [u1, v0])?),
205 (right_edge, true, uv_line([u1, v0], [u1, v1])?),
206 (top_edge, false, uv_line([u1, v1], [u0, v1])?),
207 (left_edge, false, uv_line([u0, v1], [u0, v0])?),
208 ];
209 accept(records.sheet(surface, vec![coedges]), "face")
210}
211
212fn signed_area(pcurves: &[NurbsCurve]) -> Result<f64, KernelRefusal> {
215 const SAMPLES: usize = 48;
216 let mut points = Vec::with_capacity(pcurves.len() * SAMPLES);
217 for pcurve in pcurves {
218 let [t0, t1] = pcurve.domain().or_refuse(KernelStage::Collect, "surfaceFace.pcurve_domain")?;
219 for index in 0..SAMPLES {
220 let at = pcurve.evaluate(t0 + (t1 - t0) * index as f64 / SAMPLES as f64).or_refuse(KernelStage::Collect, "surfaceFace.pcurve")?;
221 points.push([at.x, at.y]);
222 }
223 }
224 let mut twice = 0.0;
225 for index in 0..points.len() {
226 let (a, b) = (points[index], points[(index + 1) % points.len()]);
227 twice += a[0] * b[1] - b[0] * a[1];
228 }
229 Ok(0.5 * twice)
230}
231
232pub fn face_from_surface_trimmed(surface: &NurbsSurface, loops: &[Vec<NurbsCurve>]) -> Result<BrepSolid, KernelRefusal> {
246 if loops.is_empty() || loops.iter().any(Vec::is_empty) {
247 return Err(input("trim_no_loop", "at least one loop, each of at least one curve, is required".into()));
248 }
249 let size = extent(surface);
250 if !(size.is_finite() && size > 0.0) {
251 return Err(input("surface_extent", "the surface has no extent".into()));
252 }
253 let tolerance = 1e-9 * size;
254 let ends = |curve: &NurbsCurve| -> Result<[Vec3; 2], KernelRefusal> {
255 let [t0, t1] = curve.domain().or_refuse(KernelStage::Collect, "surfaceFace.curve_domain")?;
256 Ok([curve.evaluate(t0).or_refuse(KernelStage::Collect, "surfaceFace.curve")?, curve.evaluate(t1).or_refuse(KernelStage::Collect, "surfaceFace.curve")?])
257 };
258 let mut records = Records::new(tolerance);
259 let mut face_loops = Vec::with_capacity(loops.len());
260 for (loop_index, curves) in loops.iter().enumerate() {
261 let mut coedges = Vec::with_capacity(curves.len());
262 for (index, curve) in curves.iter().enumerate() {
263 let [start, end] = ends(curve)?;
264 let [next, _] = ends(&curves[(index + 1) % curves.len()])?;
265 if end.sub(next).length() > tolerance {
266 return Err(input("trim_open_loop", format!("loop {loop_index} does not close after curve {index} (gap {:.3e})", end.sub(next).length())));
267 }
268 let (from, to) = (records.vertex(start), records.vertex(end));
269 let pcurve = build_pcurve_on_surface(surface, curve).map_err(|error| {
270 refuse(KernelStage::Fragment, "trim_off_surface", format!("curve {index} of loop {loop_index} has no image on the surface: {error}"))
271 })?;
272 coedges.push((records.edge(curve.clone(), from, to, false)?, true, pcurve));
273 }
274 let pcurves: Vec<NurbsCurve> = coedges.iter().map(|(_, _, pcurve)| pcurve.clone()).collect();
275 let area = signed_area(&pcurves)?;
276 if area == 0.0 {
277 return Err(input("trim_no_area", format!("loop {loop_index} encloses no area on the surface")));
278 }
279 if (area > 0.0) != (loop_index == 0) {
281 let mut turned = Vec::with_capacity(coedges.len());
282 for (edge, _, pcurve) in coedges.into_iter().rev() {
283 turned.push((edge, false, pcurve.reversed().or_refuse(KernelStage::Collect, "surfaceFace.pcurve_reverse")?));
284 }
285 coedges = turned;
286 }
287 face_loops.push(coedges);
288 }
289 accept(records.sheet(surface, face_loops), "trimmed_face")
290}
291
292pub fn solid_from_sheets(sheets: &[BrepSolid], tolerance: f64) -> Result<BrepSolid, KernelRefusal> {
301 if sheets.is_empty() || sheets.iter().any(|sheet| sheet.shells.iter().all(|shell| shell.faces.is_empty())) {
302 return Err(input("sheets_empty", "at least one sheet, each of at least one face, is required".into()));
303 }
304 if !(tolerance.is_finite() && tolerance > 0.0) {
305 return Err(input("sheets_tolerance", "the sewing tolerance must be positive".into()));
306 }
307 let mut next = 0u64;
309 let mut id = || {
310 next += 1;
311 next
312 };
313 let (mut vertices, mut edges, mut shells) = (Vec::new(), Vec::new(), Vec::new());
314 for sheet in sheets {
315 let vertex_ids: std::collections::HashMap<u64, u64> = sheet.vertices.iter().map(|vertex| (vertex.id, id())).collect();
316 let edge_ids: std::collections::HashMap<u64, u64> = sheet.edges.iter().map(|edge| (edge.id, id())).collect();
317 let renamed = |ids: &std::collections::HashMap<u64, u64>, old: u64| {
318 ids.get(&old).copied().ok_or_else(|| input("sheets_reference", format!("a sheet refers to the missing id {old}")))
319 };
320 for vertex in &sheet.vertices {
321 vertices.push(VertexRecord { id: vertex_ids[&vertex.id], point: vertex.point });
322 }
323 for edge in &sheet.edges {
324 let mut record = edge.clone();
325 record.id = edge_ids[&edge.id];
326 record.start_vertex_id = renamed(&vertex_ids, edge.start_vertex_id)?;
327 record.end_vertex_id = renamed(&vertex_ids, edge.end_vertex_id)?;
328 edges.push(record);
329 }
330 for shell in &sheet.shells {
331 let mut faces = Vec::with_capacity(shell.faces.len());
332 for face in &shell.faces {
333 let mut record = face.clone();
334 record.id = id();
335 for loop_record in &mut record.loops {
336 loop_record.id = id();
337 for coedge in &mut loop_record.coedges {
338 coedge.id = id();
339 coedge.edge_id = renamed(&edge_ids, coedge.edge_id)?;
340 }
341 }
342 faces.push(record);
343 }
344 shells.push(ShellRecord { id: id(), faces });
345 }
346 }
347 let joined = BrepSolid {
348 mass_properties_cache: Default::default(),
349 id: id(),
350 vertices,
351 edges,
352 shells,
353 genus: 0,
354 kind: BodyKind::Sheet,
355 cells: Default::default(),
356 };
357 let (mut solid, report) = sew_solid(&joined, tolerance)?;
358 let mut uses = std::collections::HashMap::<u64, usize>::new();
360 for coedge in solid.shells.iter().flat_map(|shell| &shell.faces).flat_map(|face| &face.loops).flat_map(|record| &record.coedges) {
361 *uses.entry(coedge.edge_id).or_default() += 1;
362 }
363 let open = solid.edges.iter().filter(|edge| !edge.degenerate && uses.get(&edge.id).copied().unwrap_or(0) < 2).count();
364 if open > 0 {
365 return Err(refuse(
366 KernelStage::Sew,
367 "sheets_open",
368 format!("the sheets do not close: {open} edge(s) bound one face only after sewing ({} sewn)", report.edges_sewn),
369 ));
370 }
371 solid.kind = BodyKind::Solid;
372 let live = solid.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
374 let faces = solid.shells.iter().map(|shell| shell.faces.len()).sum::<usize>() as i64;
375 let rings = solid.shells.iter().flat_map(|shell| &shell.faces).map(|face| face.loops.len().saturating_sub(1)).sum::<usize>() as i64;
376 let twice = 2 * solid.shells.len() as i64 - (solid.vertices.len() as i64 - live + faces - rings);
377 if twice < 0 || twice % 2 != 0 {
378 return Err(refuse(KernelStage::Validate, "sheets_euler", format!("the joined faces' counts describe no closed surface (2g = {twice})")));
379 }
380 solid.genus = twice / 2;
381 let volume = crate::solid_signed_volume(&solid).or_refuse(KernelStage::Validate, "surfaceFace.volume")?;
382 if volume < 0.0 {
383 crate::offset_shell::flip_all_faces(&mut solid)?;
384 }
385 let issues = solid.validate();
386 if let Some(issue) = issues.first() {
387 return Err(refuse(KernelStage::Validate, "sheets_invalid", format!("the joined faces are not a valid solid: {issue:?}")));
388 }
389 Ok(solid)
390}
391