1use super::*;
2
3pub(super) struct Chain {
38 pub(super) vertices: Vec<usize>,
39 pub(super) closed: bool,
40}
41
42#[derive(Clone)]
46pub(super) struct RingClaim {
47 pub(super) axis_point: Vec3,
48 pub(super) axis: Vec3,
49 pub(super) wall_forward: bool,
50}
51
52pub(super) struct ChainEdgeInfo {
55 pub(super) edge_id: u64,
56 pub(super) curve_along_chain: bool,
57 pub(super) ring: Option<RingClaim>,
58}
59
60pub(super) struct Traversal {
62 pub(super) chain: usize,
63 pub(super) forward_along_chain: bool,
64}
65
66pub(super) struct RegionBrepBuilder<'a> {
67 pub(super) data: &'a MeshData,
68 pub(super) tol: f64,
69 pub(super) chains: Vec<Chain>,
70 edge_chain: HashMap<(usize, usize), usize>,
71 pub(super) chain_edges: Vec<Option<ChainEdgeInfo>>,
72 pub(super) vertices: Vec<VertexRecord>,
73 vertex_ids: HashMap<usize, u64>,
74 edges: Vec<EdgeRecord>,
75 edge_index: HashMap<u64, usize>,
76 next_id: u64,
77}
78
79pub(super) fn wrap_to_pi(mut angle: f64) -> f64 {
80 while angle > std::f64::consts::PI {
81 angle -= std::f64::consts::TAU;
82 }
83 while angle < -std::f64::consts::PI {
84 angle += std::f64::consts::TAU;
85 }
86 angle
87}
88
89pub(super) fn max_deviation_from_segment(points: &[Vec3]) -> f64 {
90 if points.len() < 3 {
91 return 0.0;
92 }
93 let start = points[0];
94 let axis = points[points.len() - 1].sub(start);
95 let length_squared = axis.length_squared();
96 let mut worst = 0.0_f64;
97 for &p in &points[1..points.len() - 1] {
98 let d = p.sub(start);
99 let t = if length_squared > 0.0 {
100 (d.dot(axis) / length_squared).clamp(0.0, 1.0)
101 } else {
102 0.0
103 };
104 worst = worst.max(d.sub(axis.scale(t)).length());
105 }
106 worst
107}
108
109fn chord_parameters(points: &[Vec3]) -> Result<Vec<f64>, String> {
110 let mut cumulative = vec![0.0_f64];
111 for pair in points.windows(2) {
112 let step = pair[1].sub(pair[0]).length();
113 if !(step > 0.0) {
114 return Err("mesh_regions_to_brep: repeated point in boundary chain".into());
115 }
116 cumulative.push(cumulative.last().unwrap() + step);
117 }
118 let total = *cumulative.last().unwrap();
119 Ok(cumulative.into_iter().map(|value| value / total).collect())
120}
121
122pub(super) fn affine_pcurve(
125 curve: &NurbsCurve,
126 origin: Vec3,
127 x_axis: Vec3,
128 y_axis: Vec3,
129) -> Result<NurbsCurve, String> {
130 let points = curve
131 .control_points
132 .iter()
133 .map(|control| {
134 let delta = control.point()?.sub(origin);
135 Ok(Vec4 {
136 x: delta.dot(x_axis) * control.w,
137 y: delta.dot(y_axis) * control.w,
138 z: 0.0,
139 w: control.w,
140 })
141 })
142 .collect::<Result<Vec<_>, String>>()?;
143 NurbsCurve::new(curve.degree, curve.knots.clone(), points)
144}
145
146pub(super) fn parameter_segment(u0: f64, v0: f64, u1: f64, v1: f64) -> Result<NurbsCurve, String> {
147 make_line(Vec3::new(u0, v0, 0.0), Vec3::new(u1, v1, 0.0))
148}
149
150pub(super) fn translate_curve(curve: &NurbsCurve, delta: Vec3) -> Result<NurbsCurve, String> {
151 let points = curve
152 .control_points
153 .iter()
154 .map(|control| Vec4 {
155 x: control.x + control.w * delta.x,
156 y: control.y + control.w * delta.y,
157 z: control.z + control.w * delta.z,
158 w: control.w,
159 })
160 .collect();
161 NurbsCurve::new(curve.degree, curve.knots.clone(), points)
162}
163
164impl<'a> RegionBrepBuilder<'a> {
165 pub(super) fn allocate_id(&mut self) -> u64 {
166 let id = self.next_id;
167 self.next_id += 1;
168 id
169 }
170
171 pub(super) fn chain_points(&self, chain: &Chain) -> Vec<Vec3> {
172 chain.vertices.iter().map(|&v| self.data.verts[v]).collect()
173 }
174
175 pub(super) fn welded_vertex_id(&mut self, welded: usize) -> u64 {
176 if let Some(&id) = self.vertex_ids.get(&welded) {
177 return id;
178 }
179 let id = self.next_id;
180 self.next_id += 1;
181 self.vertices.push(VertexRecord {
182 id,
183 point: self.data.verts[welded],
184 });
185 self.vertex_ids.insert(welded, id);
186 id
187 }
188
189 pub(super) fn push_edge(&mut self, mut edge: EdgeRecord) -> u64 {
190 edge.id = self.next_id;
191 self.next_id += 1;
192 self.edge_index.insert(edge.id, self.edges.len());
193 let id = edge.id;
194 self.edges.push(edge);
195 id
196 }
197
198 pub(super) fn edge_curve(&self, edge_id: u64) -> &NurbsCurve {
199 &self.edges[self.edge_index[&edge_id]].curve
200 }
201
202 fn chain_step_direction(chain: &Chain, a: usize, b: usize) -> Option<bool> {
205 let n = chain.vertices.len();
206 let last = if chain.closed { n } else { n - 1 };
207 for index in 0..last {
208 let s = chain.vertices[index];
209 let e = chain.vertices[(index + 1) % n];
210 if s == a && e == b {
211 return Some(true);
212 }
213 if s == b && e == a {
214 return Some(false);
215 }
216 }
217 None
218 }
219
220 pub(super) fn cycle_traversals(&self, cycle: &[usize]) -> Result<Vec<Traversal>, String> {
222 let n = cycle.len();
223 if n < 2 {
224 return Err("mesh_regions_to_brep: degenerate boundary cycle".into());
225 }
226 let step_chain = (0..n)
227 .map(|i| {
228 self.edge_chain
229 .get(&edge_key(cycle[i], cycle[(i + 1) % n]))
230 .copied()
231 .ok_or_else(|| {
232 "mesh_regions_to_brep: boundary step outside every chain".to_string()
233 })
234 })
235 .collect::<Result<Vec<_>, _>>()?;
236 if step_chain.iter().all(|&c| c == step_chain[0]) {
237 let chain = &self.chains[step_chain[0]];
238 if !chain.closed {
239 return Err("mesh_regions_to_brep: cycle traverses an open chain only".into());
240 }
241 let forward = Self::chain_step_direction(chain, cycle[0], cycle[1])
242 .ok_or("mesh_regions_to_brep: cycle step not found in its ring chain")?;
243 return Ok(vec![Traversal {
244 chain: step_chain[0],
245 forward_along_chain: forward,
246 }]);
247 }
248 let start = (0..n)
249 .find(|&i| step_chain[i] != step_chain[(i + n - 1) % n])
250 .expect("mixed chains imply a boundary between them");
251 let mut traversals: Vec<Traversal> = Vec::new();
252 let mut index = 0;
253 while index < n {
254 let at = (start + index) % n;
255 let chain_id = step_chain[at];
256 let mut run = 0;
257 while index + run < n && step_chain[(start + index + run) % n] == chain_id {
258 run += 1;
259 }
260 let chain = &self.chains[chain_id];
261 if chain.closed || run != chain.vertices.len() - 1 {
262 return Err(
263 "mesh_regions_to_brep: partial chain traversal (pinched region boundary)"
264 .into(),
265 );
266 }
267 let a = cycle[at];
268 let b = cycle[(at + 1) % n];
269 let forward = Self::chain_step_direction(chain, a, b)
270 .ok_or("mesh_regions_to_brep: traversal step not found in its chain")?;
271 traversals.push(Traversal {
272 chain: chain_id,
273 forward_along_chain: forward,
274 });
275 index += run;
276 }
277 Ok(traversals)
278 }
279
280 pub(super) fn ensure_open_chain_edge(&mut self, chain_id: usize) -> Result<(), String> {
285 if self.chain_edges[chain_id].is_some() {
286 return Ok(());
287 }
288 let chain = &self.chains[chain_id];
289 if chain.closed {
290 return Err(
291 "mesh_regions_to_brep: closed boundary ring not adjacent to any revolve wall"
292 .into(),
293 );
294 }
295 let points = self.chain_points(chain);
296 let (first, last) = (chain.vertices[0], *chain.vertices.last().unwrap());
297 let curve = if max_deviation_from_segment(&points) <= self.tol {
298 make_line(points[0], *points.last().unwrap())?
299 } else {
300 let parameters = chord_parameters(&points)?;
301 interpolate_curve(&points, 3.min(points.len() - 1), ¶meters)?
302 };
303 let start_vertex_id = self.welded_vertex_id(first);
304 let end_vertex_id = self.welded_vertex_id(last);
305 let edge_id = self.push_edge(EdgeRecord {
306 id: 0,
307 curve,
308 t0: 0.0,
309 t1: 1.0,
310 start_vertex_id,
311 end_vertex_id,
312 degenerate: false,
313 name: None,
314 });
315 self.chain_edges[chain_id] = Some(ChainEdgeInfo {
316 edge_id,
317 curve_along_chain: true,
318 ring: None,
319 });
320 Ok(())
321 }
322
323 pub(super) fn cycle_circulation(
326 &self,
327 cycle: &[usize],
328 axis_point: Vec3,
329 axis: Vec3,
330 ) -> Result<f64, String> {
331 let x_axis = axis.perpendicular()?;
332 let y_axis = axis.cross(x_axis);
333 let angle_of = |p: Vec3| -> f64 {
334 let d = p.sub(axis_point);
335 let radial = d.sub(axis.scale(d.dot(axis)));
336 radial.dot(y_axis).atan2(radial.dot(x_axis))
337 };
338 let mut total = 0.0;
339 for index in 0..cycle.len() {
340 let a = angle_of(self.data.verts[cycle[index]]);
341 let b = angle_of(self.data.verts[cycle[(index + 1) % cycle.len()]]);
342 total += wrap_to_pi(b - a);
343 }
344 Ok(total)
345 }
346}
347
348pub fn mesh_regions_to_brep(
353 positions: &[f64],
354 indices: &[u32],
355 options: &SegmentOptions,
356) -> Result<BrepSolid, String> {
357 if !(options.deflection_angle_deg > 0.0) || !options.deflection_angle_deg.is_finite() {
358 return Err("mesh_regions_to_brep: deflection angle must be positive".into());
359 }
360 if !(options.fit_tolerance > 0.0) || !options.fit_tolerance.is_finite() {
361 return Err("mesh_regions_to_brep: fit tolerance must be positive".into());
362 }
363 let data = build_mesh_data(positions, indices, options)?;
364 let seg = segment_prepared(&data, options);
365
366 if seg
369 .regions
370 .iter()
371 .all(|region| matches!(region.carrier, RegionCarrier::Freeform))
372 {
373 let weld = if options.weld_tolerance > 0.0 {
374 options.weld_tolerance
375 } else {
376 -1.0
377 };
378 let index_arg = (!indices.is_empty()).then_some(indices);
379 return mesh_to_faceted_brep(positions, index_arg, weld);
380 }
381 if let Some(region) = seg
382 .regions
383 .iter()
384 .find(|region| matches!(region.carrier, RegionCarrier::Freeform))
385 {
386 return Err(format!(
387 "mesh_regions_to_brep: region {} is freeform; mixing freeform and analytic \
388 regions in one shell is not supported (v1) — import via mesh_to_faceted_brep",
389 region.id
390 ));
391 }
392 if let Some(region) = seg.regions.iter().find(|region| {
393 matches!(
394 region.carrier,
395 RegionCarrier::Sphere { .. } | RegionCarrier::Torus { .. }
396 )
397 }) {
398 return Err(format!(
399 "mesh_regions_to_brep: region {} is a {} — sphere/torus faces need pole/seam \
400 topology not supported in v1",
401 region.id,
402 region.carrier.kind()
403 ));
404 }
405 if seg
406 .triangle_region_ids
407 .iter()
408 .any(|&id| id == UNASSIGNED_REGION)
409 {
410 return Err("mesh_regions_to_brep: mesh has unassignable degenerate triangles".into());
411 }
412
413 let region_of = &seg.triangle_region_ids;
415 let mut boundary: HashMap<(usize, usize), (u32, u32)> = HashMap::default();
416 for (&key, incident) in &data.edges {
417 if incident.len() != 2 {
418 return Err(format!(
419 "mesh_regions_to_brep: mesh is not closed-manifold (edge with {} incident \
420 triangles)",
421 incident.len()
422 ));
423 }
424 let (ra, rb) = (
425 region_of[incident[0] as usize],
426 region_of[incident[1] as usize],
427 );
428 if ra != rb {
429 boundary.insert(key, (ra.min(rb), ra.max(rb)));
430 }
431 }
432 if boundary.is_empty() {
433 return Err(
434 "mesh_regions_to_brep: single boundary-less region cannot form a face loop".into(),
435 );
436 }
437
438 let mut vertex_boundary: HashMap<usize, Vec<(usize, usize)>> = HashMap::default();
439 for &key in boundary.keys() {
440 vertex_boundary.entry(key.0).or_default().push(key);
441 vertex_boundary.entry(key.1).or_default().push(key);
442 }
443 for list in vertex_boundary.values_mut() {
444 list.sort_unstable();
445 }
446 let is_corner = |vertex: usize| -> bool {
447 let list = &vertex_boundary[&vertex];
448 list.len() != 2 || boundary[&list[0]] != boundary[&list[1]]
449 };
450
451 let mut chains: Vec<Chain> = Vec::new();
454 let mut edge_chain: HashMap<(usize, usize), usize> = HashMap::default();
455 let other_end = |key: (usize, usize), vertex: usize| -> usize {
456 if key.0 == vertex {
457 key.1
458 } else {
459 key.0
460 }
461 };
462 let mut corner_vertices: Vec<usize> = vertex_boundary
463 .keys()
464 .copied()
465 .filter(|&v| is_corner(v))
466 .collect();
467 corner_vertices.sort_unstable();
468 for &corner in &corner_vertices {
469 let incident = vertex_boundary[&corner].clone();
470 for start_edge in incident {
471 if edge_chain.contains_key(&start_edge) {
472 continue;
473 }
474 let chain_id = chains.len();
475 let mut vertices = vec![corner];
476 let mut current_edge = start_edge;
477 let mut current = other_end(start_edge, corner);
478 edge_chain.insert(start_edge, chain_id);
479 while !is_corner(current) {
480 vertices.push(current);
481 let next_edge = vertex_boundary[¤t]
482 .iter()
483 .copied()
484 .find(|&e| e != current_edge)
485 .expect("valence-2 vertex has a continuation");
486 edge_chain.insert(next_edge, chain_id);
487 current_edge = next_edge;
488 current = other_end(next_edge, current);
489 }
490 vertices.push(current);
491 chains.push(Chain {
492 vertices,
493 closed: false,
494 });
495 }
496 }
497 let mut remaining: Vec<(usize, usize)> = boundary
498 .keys()
499 .copied()
500 .filter(|key| !edge_chain.contains_key(key))
501 .collect();
502 remaining.sort_unstable();
503 for start_edge in remaining {
504 if edge_chain.contains_key(&start_edge) {
505 continue;
506 }
507 let chain_id = chains.len();
508 let start = start_edge.0;
509 let mut vertices = vec![start];
510 let mut current_edge = start_edge;
511 let mut current = start_edge.1;
512 edge_chain.insert(start_edge, chain_id);
513 while current != start {
514 vertices.push(current);
515 let next_edge = vertex_boundary[¤t]
516 .iter()
517 .copied()
518 .find(|&e| e != current_edge)
519 .expect("ring vertex has a continuation");
520 edge_chain.insert(next_edge, chain_id);
521 current_edge = next_edge;
522 current = other_end(next_edge, current);
523 }
524 chains.push(Chain {
525 vertices,
526 closed: true,
527 });
528 }
529
530 let region_count = seg.regions.len();
533 let mut region_tris: Vec<Vec<u32>> = vec![Vec::new(); region_count];
534 for (t, &r) in region_of.iter().enumerate() {
535 region_tris[r as usize].push(t as u32);
536 }
537 let mut region_cycles: Vec<Vec<Vec<usize>>> = Vec::with_capacity(region_count);
538 for tris in ®ion_tris {
539 let mut out: HashMap<usize, usize> = HashMap::default();
540 for &t in tris {
541 let tri = &data.tris[t as usize];
542 for corner in 0..3 {
543 let a = tri.verts[corner];
544 let b = tri.verts[(corner + 1) % 3];
545 if a == b || !boundary.contains_key(&edge_key(a, b)) {
546 continue;
547 }
548 if let Some(previous) = out.insert(a, b) {
549 if previous != b {
550 return Err(
551 "mesh_regions_to_brep: region boundary pinches at a vertex (two \
552 outgoing boundary edges)"
553 .into(),
554 );
555 }
556 }
557 }
558 }
559 let mut starts: Vec<usize> = out.keys().copied().collect();
560 starts.sort_unstable();
561 let mut visited: rustc_hash::FxHashSet<usize> = rustc_hash::FxHashSet::default();
562 let mut cycles = Vec::new();
563 for &start in &starts {
564 if visited.contains(&start) {
565 continue;
566 }
567 let mut cycle = vec![start];
568 visited.insert(start);
569 let mut current = out[&start];
570 while current != start {
571 if !visited.insert(current) {
572 return Err("mesh_regions_to_brep: region boundary walk self-crosses".into());
573 }
574 cycle.push(current);
575 current = *out
576 .get(¤t)
577 .ok_or("mesh_regions_to_brep: region boundary walk breaks")?;
578 }
579 cycles.push(cycle);
580 }
581 region_cycles.push(cycles);
582 }
583
584 let mut builder = RegionBrepBuilder {
585 data: &data,
586 tol: (options.fit_tolerance * data.diag).max(1e-12),
587 chains,
588 edge_chain,
589 chain_edges: Vec::new(),
590 vertices: Vec::new(),
591 vertex_ids: HashMap::default(),
592 edges: Vec::new(),
593 edge_index: HashMap::default(),
594 next_id: 1,
595 };
596 builder.chain_edges = (0..builder.chains.len()).map(|_| None).collect();
597
598 let mut faces: Vec<FaceRecord> = Vec::new();
601 for (index, region) in seg.regions.iter().enumerate() {
602 match ®ion.carrier {
603 RegionCarrier::Cylinder { .. } | RegionCarrier::Cone { .. } => {
604 let face = build_revolved_region_face(&mut builder, region, ®ion_cycles[index])?;
605 faces.push(face);
606 }
607 _ => {}
608 }
609 }
610 for (index, region) in seg.regions.iter().enumerate() {
611 if let RegionCarrier::Plane { origin, normal } = region.carrier {
612 let face = build_planar_region_face(
613 &mut builder,
614 region.id,
615 origin,
616 normal,
617 ®ion_cycles[index],
618 )?;
619 faces.push(face);
620 }
621 }
622
623 let shell_id = builder.allocate_id();
624 let solid_id = builder.allocate_id();
625 let mut solid = BrepSolid {
626 id: solid_id,
627 vertices: builder.vertices,
628 edges: builder.edges,
629 shells: vec![ShellRecord {
630 id: shell_id,
631 faces,
632 }],
633 genus: 0,
634 };
635 let vertex_count = solid.vertices.len() as i64;
636 let edge_count = solid.edges.len() as i64;
637 let face_count = solid.shells[0].faces.len() as i64;
638 let hole_count: i64 = solid.shells[0]
639 .faces
640 .iter()
641 .map(|face| face.loops.len().saturating_sub(1) as i64)
642 .sum();
643 let euler = vertex_count - edge_count + face_count - hole_count;
644 let numerator = 2 - euler;
645 if numerator < 0 || numerator % 2 != 0 {
646 return Err(format!(
647 "mesh_regions_to_brep: rebuilt topology has non-manifold Euler count V-E+F-H={euler}"
648 ));
649 }
650 solid.genus = numerator / 2;
651 let issues = solid.validate();
652 if !issues.is_empty() {
653 return Err(format!(
654 "mesh_regions_to_brep: rebuilt solid failed validation: {issues:?}"
655 ));
656 }
657 let volume = solid_signed_volume(&solid)?;
658 if !(volume > 0.0) {
659 return Err(format!(
660 "mesh_regions_to_brep: rebuilt solid is inverted (signed volume {volume})"
661 ));
662 }
663 Ok(solid)
664}