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 {
316 name.strip_prefix("ACOMP")
317 .map(|digits| !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()))
318 .unwrap_or(false)
319}
320
321pub fn resolve_named_selection(
330 solid: &BrepSolid,
331 name: &str,
332) -> Result<SelectionGeometry, ResolveError> {
333 if let Some(record) = solid
334 .shells
335 .iter()
336 .flat_map(|shell| &shell.faces)
337 .find(|face| face.name.as_deref() == Some(name))
338 {
339 return resolve_face_record(solid, record);
340 }
341 if let Some(record) = solid
342 .edges
343 .iter()
344 .find(|edge| edge.name.as_deref() == Some(name))
345 {
346 return resolve_edge_record(record);
347 }
348 Err(ResolveError::NotFound { name: name.into() })
349}
350
351pub fn resolve_face_selection(
354 solid: &BrepSolid,
355 face_id: u64,
356) -> Result<SelectionGeometry, ResolveError> {
357 let record = solid
358 .shells
359 .iter()
360 .flat_map(|shell| &shell.faces)
361 .find(|face| face.id == face_id)
362 .ok_or_else(|| ResolveError::NotFound {
363 name: format!("face {face_id}"),
364 })?;
365 resolve_face_record(solid, record)
366}
367
368pub fn resolve_edge_selection(
370 solid: &BrepSolid,
371 edge_id: u64,
372) -> Result<SelectionGeometry, ResolveError> {
373 let record = solid
374 .edges
375 .iter()
376 .find(|edge| edge.id == edge_id)
377 .ok_or_else(|| ResolveError::NotFound {
378 name: format!("edge {edge_id}"),
379 })?;
380 resolve_edge_record(record)
381}
382
383pub fn resolve_vertex_selection(
389 solid: &BrepSolid,
390 position: Vec3,
391) -> Result<SelectionGeometry, ResolveError> {
392 let mut best: Option<(f64, Vec3)> = None;
393 for vertex in &solid.vertices {
394 let distance = vertex.point.sub(position).length();
395 if best
396 .map(|(best_distance, _)| distance < best_distance)
397 .unwrap_or(true)
398 {
399 best = Some((distance, vertex.point));
400 }
401 }
402 match best {
403 Some((distance, point))
404 if distance <= VERTEX_SNAP_TOLERANCE * crate::solid_scale(solid) =>
405 {
406 Ok(SelectionGeometry::Point { position: point })
407 }
408 _ => Err(ResolveError::NotFound {
409 name: format!(
410 "vertex near ({}, {}, {})",
411 position.x, position.y, position.z
412 ),
413 }),
414 }
415}
416
417pub fn resolve_component_point(solids: &[&BrepSolid]) -> Result<SelectionGeometry, ResolveError> {
424 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
425 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
426 let mut any = false;
427 let mut include = |point: Vec3| {
428 low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
429 high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
430 any = true;
431 };
432 for solid in solids {
433 for vertex in &solid.vertices {
434 include(vertex.point);
435 }
436 for shell in &solid.shells {
437 for face in &shell.faces {
438 for row in &face.surface.control_points {
439 for control in row {
440 let point = control.point().map_err(|error| ResolveError::Geometry {
441 name: "component".into(),
442 detail: error,
443 })?;
444 include(point);
445 }
446 }
447 }
448 }
449 }
450 if !any {
451 return Err(ResolveError::Geometry {
452 name: "component".into(),
453 detail: "component has no geometry to anchor".into(),
454 });
455 }
456 Ok(SelectionGeometry::Point {
457 position: low.add(high).scale(0.5),
458 })
459}
460
461fn face_label(record: &FaceRecord) -> String {
466 record
467 .name
468 .clone()
469 .unwrap_or_else(|| format!("face {}", record.id))
470}
471
472fn resolve_face_record(
473 solid: &BrepSolid,
474 record: &FaceRecord,
475) -> Result<SelectionGeometry, ResolveError> {
476 match record.surface.analytic() {
477 Some(AnalyticSurface::RuledRevolution {
478 frame, rho0, rho1, ..
479 }) => {
480 let span = rho0.abs().max(rho1.abs());
483 let radius = ((rho0 - rho1).abs() <= RESOLVE_TOLERANCE * (1.0 + span))
484 .then_some(0.5 * (rho0 + rho1));
485 Ok(SelectionGeometry::Axis {
486 origin: frame.origin,
487 direction: frame.axis,
488 radius,
489 })
490 }
491 Some(AnalyticSurface::Sphere { frame, radius }) => Ok(SelectionGeometry::Sphere {
492 center: frame.origin,
493 radius: *radius,
494 }),
495 Some(AnalyticSurface::Torus { frame, .. }) => Ok(SelectionGeometry::Axis {
496 origin: frame.origin,
497 direction: frame.axis,
498 radius: None,
499 }),
500 Some(AnalyticSurface::Revolution {
501 frame, generatrix, ..
502 }) => Ok(SelectionGeometry::Axis {
503 origin: frame.origin,
504 direction: frame.axis,
505 radius: revolution_cylinder_radius(frame, generatrix),
506 }),
507 Some(AnalyticSurface::Plane { .. }) | None => planar_face(solid, record),
510 }
511}
512
513fn revolution_cylinder_radius(
520 frame: &crate::RevolutionFrame,
521 generatrix: &NurbsCurve,
522) -> Option<f64> {
523 if !control_net_is_colinear(generatrix) {
524 return None;
525 }
526 let [t0, t1] = generatrix.domain().ok()?;
527 let radial = |t: f64| -> Option<f64> {
528 let point = generatrix.evaluate(t).ok()?;
529 let delta = point.sub(frame.origin);
530 Some(delta.sub(frame.axis.scale(delta.dot(frame.axis))).length())
531 };
532 let rho0 = radial(t0)?;
533 let rho_mid = radial(0.5 * (t0 + t1))?;
534 let rho1 = radial(t1)?;
535 let span = rho0.abs().max(rho1.abs());
536 let tolerance = RESOLVE_TOLERANCE * (1.0 + span);
537 ((rho0 - rho1).abs() <= tolerance && (rho_mid - rho0).abs() <= tolerance)
538 .then_some((rho0 + rho_mid + rho1) / 3.0)
539}
540
541fn planar_face(solid: &BrepSolid, record: &FaceRecord) -> Result<SelectionGeometry, ResolveError> {
560 let geometry = (|| -> Result<Option<SelectionGeometry>, String> {
561 let [u0, u1] = record.surface.domain_u()?;
562 let [v0, v1] = record.surface.domain_v()?;
563 let (um, vm) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
564 let center_normal = record.surface.normal(um, vm)?;
565 for u in [u0, um, u1] {
566 for v in [v0, vm, v1] {
567 if record.surface.normal(u, v)?.dot(center_normal) < 1.0 - 1e-6 {
568 return Ok(None);
569 }
570 }
571 }
572 let normal = if record.same_sense {
573 center_normal
574 } else {
575 center_normal.scale(-1.0)
576 }
577 .normalized()?;
578
579 let plane_point = record.surface.evaluate(um, vm)?;
591 let partials = record.surface.derivatives(um, vm, 1)?;
592 let in_plane = |partial: Vec3| -> Option<Vec3> {
593 let tangent = partial.sub(normal.scale(partial.dot(normal)));
594 let length = tangent.length();
595 (length.is_finite() && length > 1e-6 * partial.length() && length > 0.0)
596 .then(|| tangent.scale(1.0 / length))
597 };
598 let e1 = in_plane(partials[1][0])
599 .or_else(|| in_plane(partials[0][1]))
600 .ok_or_else(|| {
601 "planar face: both surface partials degenerate at the patch midpoint".to_string()
602 })?;
603 let e2 = normal.cross(e1).normalized()?;
604 let mut low = [f64::INFINITY; 2];
607 let mut high = [f64::NEG_INFINITY; 2];
608 let mut any = false;
609 for loop_record in &record.loops {
610 for coedge in &loop_record.coedges {
611 let Some(edge) = solid.edges.iter().find(|edge| edge.id == coedge.edge_id) else {
612 continue;
613 };
614 if edge.degenerate {
615 continue;
616 }
617 for step in 0..=4 {
618 let t = edge.t0 + (edge.t1 - edge.t0) * (step as f64 / 4.0);
619 let offset = edge.curve.evaluate(t)?.sub(plane_point);
620 let (a, b) = (offset.dot(e1), offset.dot(e2));
621 low = [low[0].min(a), low[1].min(b)];
622 high = [high[0].max(a), high[1].max(b)];
623 any = true;
624 }
625 }
626 }
627 let center = if any {
628 plane_point
629 .add(e1.scale((low[0] + high[0]) * 0.5))
630 .add(e2.scale((low[1] + high[1]) * 0.5))
631 } else {
632 plane_point
633 };
634 let signed = center.sub(plane_point).dot(normal);
637 let origin = center.sub(normal.scale(signed));
638 Ok(Some(SelectionGeometry::Plane { origin, normal }))
639 })();
640 match geometry {
641 Ok(Some(frame)) => Ok(frame),
642 Ok(None) => Err(ResolveError::Unsupported {
643 name: face_label(record),
644 detail: "face carries no analytic frame (freeform surface)".into(),
645 }),
646 Err(error) => Err(ResolveError::Geometry {
647 name: face_label(record),
648 detail: error,
649 }),
650 }
651}
652
653fn edge_label(record: &EdgeRecord) -> String {
658 record
659 .name
660 .clone()
661 .unwrap_or_else(|| format!("edge {}", record.id))
662}
663
664fn resolve_edge_record(record: &EdgeRecord) -> Result<SelectionGeometry, ResolveError> {
665 if record.degenerate {
666 return Err(ResolveError::Unsupported {
667 name: edge_label(record),
668 detail: "degenerate edge".into(),
669 });
670 }
671 match edge_geometry(record) {
672 Ok(Some(frame)) => Ok(frame),
673 Ok(None) => Err(ResolveError::Unsupported {
674 name: edge_label(record),
675 detail: "edge is neither straight nor circular".into(),
676 }),
677 Err(error) => Err(ResolveError::Geometry {
678 name: edge_label(record),
679 detail: error,
680 }),
681 }
682}
683
684fn edge_geometry(record: &EdgeRecord) -> Result<Option<SelectionGeometry>, String> {
685 if let Some(line) = straight_edge_line(record)? {
686 return Ok(Some(line));
687 }
688 circular_edge_circle(record)
689}
690
691fn control_net_extent(curve: &NurbsCurve) -> f64 {
693 let mut extent = 0.0_f64;
694 for control in &curve.control_points {
695 if let Ok(point) = control.point() {
696 extent = extent
697 .max(point.x.abs())
698 .max(point.y.abs())
699 .max(point.z.abs());
700 }
701 }
702 extent
703}
704
705fn control_net_is_colinear(curve: &NurbsCurve) -> bool {
709 let points: Vec<Vec3> = curve
710 .control_points
711 .iter()
712 .filter_map(|control| control.point().ok())
713 .collect();
714 let Some((&first, rest)) = points.split_first() else {
715 return false;
716 };
717 let tolerance = RESOLVE_TOLERANCE * (1.0 + control_net_extent(curve));
718 let Some(direction) = rest
719 .iter()
720 .map(|point| point.sub(first))
721 .max_by(|a, b| a.length().total_cmp(&b.length()))
722 .and_then(|chord| chord.normalized().ok())
723 else {
724 return false;
725 };
726 points.iter().all(|point| {
727 let offset = point.sub(first);
728 offset.sub(direction.scale(offset.dot(direction))).length() <= tolerance
729 })
730}
731
732fn straight_edge_line(record: &EdgeRecord) -> Result<Option<SelectionGeometry>, String> {
735 if !control_net_is_colinear(&record.curve) {
736 return Ok(None);
737 }
738 let start = record.curve.evaluate(record.t0)?;
739 let end = record.curve.evaluate(record.t1)?;
740 let chord = end.sub(start);
741 if chord.length() <= RESOLVE_TOLERANCE * (1.0 + control_net_extent(&record.curve)) {
742 return Ok(None);
744 }
745 Ok(Some(SelectionGeometry::Line {
746 origin: start.add(end).scale(0.5),
747 direction: chord.normalized()?,
748 }))
749}
750
751fn circular_edge_circle(record: &EdgeRecord) -> Result<Option<SelectionGeometry>, String> {
757 let span = record.t1 - record.t0;
758 if !(span > 0.0) {
759 return Ok(None);
760 }
761 let at = |fraction: f64| record.curve.evaluate(record.t0 + span * fraction);
762 let (a, b, c) = (at(1.0 / 6.0)?, at(0.5)?, at(5.0 / 6.0)?);
763 let Some(center) = crate::analytic_surface::circumcenter(a, b, c) else {
764 return Ok(None);
765 };
766 let Ok(axis) = b.sub(a).cross(c.sub(b)).normalized() else {
768 return Ok(None);
769 };
770 let radius = a.sub(center).length();
771 let tolerance = RESOLVE_TOLERANCE * (1.0 + control_net_extent(&record.curve));
772 for step in 0..=CIRCLE_VERIFY_SAMPLES {
773 let point = at(step as f64 / CIRCLE_VERIFY_SAMPLES as f64)?;
774 let delta = point.sub(center);
775 if (delta.length() - radius).abs() > tolerance || delta.dot(axis).abs() > tolerance {
776 return Ok(None);
777 }
778 }
779 Ok(Some(SelectionGeometry::Circle {
780 center,
781 axis,
782 radius,
783 }))
784}
785
786