1use crate::topology::{BrepSolid, EdgeRecord, FaceRecord};
42use crate::{AffineTransform, AnalyticSurface, MateAxis, MatePlane, NurbsCurve, Vec3};
43use serde::Serialize;
44
45const RESOLVE_TOLERANCE: f64 = 1e-9;
50const VERTEX_SNAP_TOLERANCE: f64 = 1e-6;
53const CIRCLE_VERIFY_SAMPLES: usize = 16;
55
56#[derive(Clone, Copy, Debug, Serialize)]
69#[serde(tag = "kind", rename_all = "snake_case")]
70pub enum SelectionGeometry {
71 Plane { origin: Vec3, normal: Vec3 },
76 Axis {
79 origin: Vec3,
80 direction: Vec3,
81 radius: Option<f64>,
82 },
83 Sphere { center: Vec3, radius: f64 },
85 Circle { center: Vec3, axis: Vec3, radius: f64 },
88 Line { origin: Vec3, direction: Vec3 },
92 Point { position: Vec3 },
94}
95
96#[derive(Clone, Debug, PartialEq, Serialize)]
100pub enum ResolveError {
101 NotFound { name: String },
103 Unsupported { name: String, detail: String },
106 Geometry { name: String, detail: String },
108}
109
110impl ResolveError {
111 pub fn status(&self) -> &'static str {
113 match self {
114 ResolveError::NotFound { .. } | ResolveError::Geometry { .. } => "invalid-selection",
115 ResolveError::Unsupported { .. } => "unsupported-selection",
116 }
117 }
118}
119
120impl std::fmt::Display for ResolveError {
121 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 match self {
123 ResolveError::NotFound { name } => write!(formatter, "selection '{name}' not found"),
124 ResolveError::Unsupported { name, detail } => {
125 write!(formatter, "selection '{name}' has no analytic frame: {detail}")
126 }
127 ResolveError::Geometry { name, detail } => {
128 write!(formatter, "selection '{name}' failed to resolve: {detail}")
129 }
130 }
131 }
132}
133
134impl SelectionGeometry {
135 pub fn mate_plane(&self) -> Option<MatePlane> {
137 match *self {
138 SelectionGeometry::Plane { origin, normal } => Some(MatePlane {
139 origin: triple(origin),
140 normal: triple(normal),
141 }),
142 _ => None,
143 }
144 }
145
146 pub fn mate_axis(&self) -> Option<MateAxis> {
150 let (origin, direction) = match *self {
151 SelectionGeometry::Axis {
152 origin, direction, ..
153 }
154 | SelectionGeometry::Line { origin, direction } => (origin, direction),
155 SelectionGeometry::Circle { center, axis, .. } => (center, axis),
156 _ => return None,
157 };
158 Some(MateAxis {
159 origin: triple(origin),
160 direction: triple(direction),
161 })
162 }
163
164 pub fn representative_point(&self) -> Vec3 {
167 match *self {
168 SelectionGeometry::Plane { origin, .. }
169 | SelectionGeometry::Axis { origin, .. }
170 | SelectionGeometry::Line { origin, .. } => origin,
171 SelectionGeometry::Sphere { center, .. }
172 | SelectionGeometry::Circle { center, .. } => center,
173 SelectionGeometry::Point { position } => position,
174 }
175 }
176
177 pub fn transformed(&self, transform: &AffineTransform) -> Result<Self, ResolveError> {
184 let scale = uniform_scale(transform).ok_or_else(|| ResolveError::Unsupported {
185 name: "transform".into(),
186 detail: "selection frames transform only by rigid/uniform-scale matrices".into(),
187 })?;
188 let direction = |vector: Vec3| {
189 linear(transform, vector)
190 .normalized()
191 .map_err(|error| ResolveError::Geometry {
192 name: "transform".into(),
193 detail: error,
194 })
195 };
196 Ok(match *self {
197 SelectionGeometry::Plane { origin, normal } => SelectionGeometry::Plane {
198 origin: transform.point(origin),
199 normal: direction(normal)?,
200 },
201 SelectionGeometry::Axis {
202 origin,
203 direction: axis,
204 radius,
205 } => SelectionGeometry::Axis {
206 origin: transform.point(origin),
207 direction: direction(axis)?,
208 radius: radius.map(|radius| radius * scale),
209 },
210 SelectionGeometry::Sphere { center, radius } => SelectionGeometry::Sphere {
211 center: transform.point(center),
212 radius: radius * scale,
213 },
214 SelectionGeometry::Circle {
215 center,
216 axis,
217 radius,
218 } => SelectionGeometry::Circle {
219 center: transform.point(center),
220 axis: direction(axis)?,
221 radius: radius * scale,
222 },
223 SelectionGeometry::Line {
224 origin,
225 direction: axis,
226 } => SelectionGeometry::Line {
227 origin: transform.point(origin),
228 direction: direction(axis)?,
229 },
230 SelectionGeometry::Point { position } => SelectionGeometry::Point {
231 position: transform.point(position),
232 },
233 })
234 }
235}
236
237fn triple(vector: Vec3) -> [f64; 3] {
238 [vector.x, vector.y, vector.z]
239}
240
241pub(crate) fn linear(transform: &AffineTransform, vector: Vec3) -> Vec3 {
243 let m = transform.elements;
244 Vec3::new(
245 m[0] * vector.x + m[1] * vector.y + m[2] * vector.z,
246 m[4] * vector.x + m[5] * vector.y + m[6] * vector.z,
247 m[8] * vector.x + m[9] * vector.y + m[10] * vector.z,
248 )
249}
250
251fn uniform_scale(transform: &AffineTransform) -> Option<f64> {
254 let m = transform.elements;
255 let columns = [
256 Vec3::new(m[0], m[4], m[8]),
257 Vec3::new(m[1], m[5], m[9]),
258 Vec3::new(m[2], m[6], m[10]),
259 ];
260 let lengths = [
261 columns[0].length(),
262 columns[1].length(),
263 columns[2].length(),
264 ];
265 let scale = (lengths[0] + lengths[1] + lengths[2]) / 3.0;
266 if !(scale > 0.0) {
267 return None;
268 }
269 if lengths
270 .iter()
271 .any(|length| (length - scale).abs() > 1e-9 * scale)
272 {
273 return None;
274 }
275 let orthogonality = 1e-9 * scale * scale;
276 if columns[0].dot(columns[1]).abs() > orthogonality
277 || columns[1].dot(columns[2]).abs() > orthogonality
278 || columns[0].dot(columns[2]).abs() > orthogonality
279 {
280 return None;
281 }
282 Some(scale)
283}
284
285pub fn split_component_namespace(name: &str) -> (Vec<&str>, &str) {
301 let mut chain = Vec::new();
302 let mut local = name;
303 while let Some((head, tail)) = local.split_once(':') {
304 if !is_component_reference(head) {
305 break;
306 }
307 chain.push(head);
308 local = tail;
309 }
310 (chain, local)
311}
312
313pub fn is_component_reference(name: &str) -> bool {
317 name.strip_prefix("ACOMP")
318 .map(|digits| !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()))
319 .unwrap_or(false)
320 || crate::feature_pipeline::features::assembly_pattern::parse_instance_id(name).is_some()
321}
322
323pub fn resolve_named_selection(
332 solid: &BrepSolid,
333 name: &str,
334) -> Result<SelectionGeometry, ResolveError> {
335 if let Some(record) = solid
336 .shells
337 .iter()
338 .flat_map(|shell| &shell.faces)
339 .find(|face| face.name.as_deref() == Some(name))
340 {
341 return resolve_face_record(solid, record);
342 }
343 if let Some(record) = solid
344 .edges
345 .iter()
346 .find(|edge| edge.name.as_deref() == Some(name))
347 {
348 return resolve_edge_record(record);
349 }
350 Err(ResolveError::NotFound { name: name.into() })
351}
352
353pub fn resolve_face_selection(
356 solid: &BrepSolid,
357 face_id: u64,
358) -> Result<SelectionGeometry, ResolveError> {
359 let record = solid
360 .shells
361 .iter()
362 .flat_map(|shell| &shell.faces)
363 .find(|face| face.id == face_id)
364 .ok_or_else(|| ResolveError::NotFound {
365 name: format!("face {face_id}"),
366 })?;
367 resolve_face_record(solid, record)
368}
369
370pub fn resolve_edge_selection(
372 solid: &BrepSolid,
373 edge_id: u64,
374) -> Result<SelectionGeometry, ResolveError> {
375 let record = solid
376 .edges
377 .iter()
378 .find(|edge| edge.id == edge_id)
379 .ok_or_else(|| ResolveError::NotFound {
380 name: format!("edge {edge_id}"),
381 })?;
382 resolve_edge_record(record)
383}
384
385pub fn resolve_vertex_selection(
391 solid: &BrepSolid,
392 position: Vec3,
393) -> Result<SelectionGeometry, ResolveError> {
394 let mut best: Option<(f64, Vec3)> = None;
395 for vertex in &solid.vertices {
396 let distance = vertex.point.sub(position).length();
397 if best
398 .map(|(best_distance, _)| distance < best_distance)
399 .unwrap_or(true)
400 {
401 best = Some((distance, vertex.point));
402 }
403 }
404 match best {
405 Some((distance, point))
406 if distance <= VERTEX_SNAP_TOLERANCE * crate::solid_scale(solid) =>
407 {
408 Ok(SelectionGeometry::Point { position: point })
409 }
410 _ => Err(ResolveError::NotFound {
411 name: format!(
412 "vertex near ({}, {}, {})",
413 position.x, position.y, position.z
414 ),
415 }),
416 }
417}
418
419pub fn resolve_component_point(solids: &[&BrepSolid]) -> Result<SelectionGeometry, ResolveError> {
426 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
427 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
428 let mut any = false;
429 let mut include = |point: Vec3| {
430 low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
431 high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
432 any = true;
433 };
434 for solid in solids {
435 for vertex in &solid.vertices {
436 include(vertex.point);
437 }
438 for shell in &solid.shells {
439 for face in &shell.faces {
440 for row in &face.surface.control_points {
441 for control in row {
442 let point = control.point().map_err(|error| ResolveError::Geometry {
443 name: "component".into(),
444 detail: error,
445 })?;
446 include(point);
447 }
448 }
449 }
450 }
451 }
452 if !any {
453 return Err(ResolveError::Geometry {
454 name: "component".into(),
455 detail: "component has no geometry to anchor".into(),
456 });
457 }
458 Ok(SelectionGeometry::Point {
459 position: low.add(high).scale(0.5),
460 })
461}
462
463fn face_label(record: &FaceRecord) -> String {
468 record
469 .name
470 .clone()
471 .unwrap_or_else(|| format!("face {}", record.id))
472}
473
474fn resolve_face_record(
475 solid: &BrepSolid,
476 record: &FaceRecord,
477) -> Result<SelectionGeometry, ResolveError> {
478 match record.surface.analytic() {
479 Some(AnalyticSurface::RuledRevolution {
480 frame, rho0, rho1, ..
481 }) => {
482 let span = rho0.abs().max(rho1.abs());
485 let radius = ((rho0 - rho1).abs() <= RESOLVE_TOLERANCE * (1.0 + span))
486 .then_some(0.5 * (rho0 + rho1));
487 Ok(SelectionGeometry::Axis {
488 origin: frame.origin,
489 direction: frame.axis,
490 radius,
491 })
492 }
493 Some(AnalyticSurface::Sphere { frame, radius }) => Ok(SelectionGeometry::Sphere {
494 center: frame.origin,
495 radius: *radius,
496 }),
497 Some(AnalyticSurface::Torus { frame, .. }) => Ok(SelectionGeometry::Axis {
498 origin: frame.origin,
499 direction: frame.axis,
500 radius: None,
501 }),
502 Some(AnalyticSurface::Revolution {
503 frame, generatrix, ..
504 }) => Ok(SelectionGeometry::Axis {
505 origin: frame.origin,
506 direction: frame.axis,
507 radius: revolution_cylinder_radius(frame, generatrix),
508 }),
509 Some(AnalyticSurface::Plane { .. }) | None => planar_face(solid, record),
512 }
513}
514
515fn revolution_cylinder_radius(
522 frame: &crate::RevolutionFrame,
523 generatrix: &NurbsCurve,
524) -> Option<f64> {
525 if !control_net_is_colinear(generatrix) {
526 return None;
527 }
528 let [t0, t1] = generatrix.domain().ok()?;
529 let radial = |t: f64| -> Option<f64> {
530 let point = generatrix.evaluate(t).ok()?;
531 let delta = point.sub(frame.origin);
532 Some(delta.sub(frame.axis.scale(delta.dot(frame.axis))).length())
533 };
534 let rho0 = radial(t0)?;
535 let rho_mid = radial(0.5 * (t0 + t1))?;
536 let rho1 = radial(t1)?;
537 let span = rho0.abs().max(rho1.abs());
538 let tolerance = RESOLVE_TOLERANCE * (1.0 + span);
539 ((rho0 - rho1).abs() <= tolerance && (rho_mid - rho0).abs() <= tolerance)
540 .then_some((rho0 + rho_mid + rho1) / 3.0)
541}
542
543fn planar_face(solid: &BrepSolid, record: &FaceRecord) -> Result<SelectionGeometry, ResolveError> {
562 let geometry = (|| -> Result<Option<SelectionGeometry>, String> {
563 let [u0, u1] = record.surface.domain_u()?;
564 let [v0, v1] = record.surface.domain_v()?;
565 let (um, vm) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
566 let center_normal = record.surface.normal(um, vm)?;
567 for u in [u0, um, u1] {
568 for v in [v0, vm, v1] {
569 if record.surface.normal(u, v)?.dot(center_normal) < 1.0 - 1e-6 {
570 return Ok(None);
571 }
572 }
573 }
574 let normal = if record.same_sense {
575 center_normal
576 } else {
577 center_normal.scale(-1.0)
578 }
579 .normalized()?;
580
581 let plane_point = record.surface.evaluate(um, vm)?;
593 let partials = record.surface.derivatives(um, vm, 1)?;
594 let in_plane = |partial: Vec3| -> Option<Vec3> {
595 let tangent = partial.sub(normal.scale(partial.dot(normal)));
596 let length = tangent.length();
597 (length.is_finite() && length > 1e-6 * partial.length() && length > 0.0)
598 .then(|| tangent.scale(1.0 / length))
599 };
600 let e1 = in_plane(partials[1][0])
601 .or_else(|| in_plane(partials[0][1]))
602 .ok_or_else(|| {
603 "planar face: both surface partials degenerate at the patch midpoint".to_string()
604 })?;
605 let e2 = normal.cross(e1).normalized()?;
606 let mut low = [f64::INFINITY; 2];
609 let mut high = [f64::NEG_INFINITY; 2];
610 let mut any = false;
611 for loop_record in &record.loops {
612 for coedge in &loop_record.coedges {
613 let Some(edge) = solid.edges.iter().find(|edge| edge.id == coedge.edge_id) else {
614 continue;
615 };
616 if edge.degenerate {
617 continue;
618 }
619 for step in 0..=4 {
620 let t = edge.t0 + (edge.t1 - edge.t0) * (step as f64 / 4.0);
621 let offset = edge.curve.evaluate(t)?.sub(plane_point);
622 let (a, b) = (offset.dot(e1), offset.dot(e2));
623 low = [low[0].min(a), low[1].min(b)];
624 high = [high[0].max(a), high[1].max(b)];
625 any = true;
626 }
627 }
628 }
629 let center = if any {
630 plane_point
631 .add(e1.scale((low[0] + high[0]) * 0.5))
632 .add(e2.scale((low[1] + high[1]) * 0.5))
633 } else {
634 plane_point
635 };
636 let signed = center.sub(plane_point).dot(normal);
639 let origin = center.sub(normal.scale(signed));
640 Ok(Some(SelectionGeometry::Plane { origin, normal }))
641 })();
642 match geometry {
643 Ok(Some(frame)) => Ok(frame),
644 Ok(None) => Err(ResolveError::Unsupported {
645 name: face_label(record),
646 detail: "face carries no analytic frame (freeform surface)".into(),
647 }),
648 Err(error) => Err(ResolveError::Geometry {
649 name: face_label(record),
650 detail: error,
651 }),
652 }
653}
654
655fn edge_label(record: &EdgeRecord) -> String {
660 record
661 .name
662 .clone()
663 .unwrap_or_else(|| format!("edge {}", record.id))
664}
665
666fn resolve_edge_record(record: &EdgeRecord) -> Result<SelectionGeometry, ResolveError> {
667 if record.degenerate {
668 return Err(ResolveError::Unsupported {
669 name: edge_label(record),
670 detail: "degenerate edge".into(),
671 });
672 }
673 match edge_geometry(record) {
674 Ok(Some(frame)) => Ok(frame),
675 Ok(None) => Err(ResolveError::Unsupported {
676 name: edge_label(record),
677 detail: "edge is neither straight nor circular".into(),
678 }),
679 Err(error) => Err(ResolveError::Geometry {
680 name: edge_label(record),
681 detail: error,
682 }),
683 }
684}
685
686fn edge_geometry(record: &EdgeRecord) -> Result<Option<SelectionGeometry>, String> {
687 if let Some(line) = straight_edge_line(record)? {
688 return Ok(Some(line));
689 }
690 circular_edge_circle(record)
691}
692
693fn control_net_extent(curve: &NurbsCurve) -> f64 {
695 let mut extent = 0.0_f64;
696 for control in &curve.control_points {
697 if let Ok(point) = control.point() {
698 extent = extent
699 .max(point.x.abs())
700 .max(point.y.abs())
701 .max(point.z.abs());
702 }
703 }
704 extent
705}
706
707fn control_net_is_colinear(curve: &NurbsCurve) -> bool {
711 let points: Vec<Vec3> = curve
712 .control_points
713 .iter()
714 .filter_map(|control| control.point().ok())
715 .collect();
716 let Some((&first, rest)) = points.split_first() else {
717 return false;
718 };
719 let tolerance = RESOLVE_TOLERANCE * (1.0 + control_net_extent(curve));
720 let Some(direction) = rest
721 .iter()
722 .map(|point| point.sub(first))
723 .max_by(|a, b| a.length().total_cmp(&b.length()))
724 .and_then(|chord| chord.normalized().ok())
725 else {
726 return false;
727 };
728 points.iter().all(|point| {
729 let offset = point.sub(first);
730 offset.sub(direction.scale(offset.dot(direction))).length() <= tolerance
731 })
732}
733
734fn straight_edge_line(record: &EdgeRecord) -> Result<Option<SelectionGeometry>, String> {
737 if !control_net_is_colinear(&record.curve) {
738 return Ok(None);
739 }
740 let start = record.curve.evaluate(record.t0)?;
741 let end = record.curve.evaluate(record.t1)?;
742 let chord = end.sub(start);
743 if chord.length() <= RESOLVE_TOLERANCE * (1.0 + control_net_extent(&record.curve)) {
744 return Ok(None);
746 }
747 Ok(Some(SelectionGeometry::Line {
748 origin: start.add(end).scale(0.5),
749 direction: chord.normalized()?,
750 }))
751}
752
753fn circular_edge_circle(record: &EdgeRecord) -> Result<Option<SelectionGeometry>, String> {
759 let span = record.t1 - record.t0;
760 if !(span > 0.0) {
761 return Ok(None);
762 }
763 let at = |fraction: f64| record.curve.evaluate(record.t0 + span * fraction);
764 let (a, b, c) = (at(1.0 / 6.0)?, at(0.5)?, at(5.0 / 6.0)?);
765 let Some(center) = crate::analytic_surface::circumcenter(a, b, c) else {
766 return Ok(None);
767 };
768 let Ok(axis) = b.sub(a).cross(c.sub(b)).normalized() else {
770 return Ok(None);
771 };
772 let radius = a.sub(center).length();
773 let tolerance = RESOLVE_TOLERANCE * (1.0 + control_net_extent(&record.curve));
774 for step in 0..=CIRCLE_VERIFY_SAMPLES {
775 let point = at(step as f64 / CIRCLE_VERIFY_SAMPLES as f64)?;
776 let delta = point.sub(center);
777 if (delta.length() - radius).abs() > tolerance || delta.dot(axis).abs() > tolerance {
778 return Ok(None);
779 }
780 }
781 Ok(Some(SelectionGeometry::Circle {
782 center,
783 axis,
784 radius,
785 }))
786}
787
788