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 },
75 Axis {
78 origin: Vec3,
79 direction: Vec3,
80 radius: Option<f64>,
81 },
82 Sphere { center: Vec3, radius: f64 },
84 Circle { center: Vec3, axis: Vec3, radius: f64 },
87 Line { origin: Vec3, direction: Vec3 },
91 Point { position: Vec3 },
93}
94
95#[derive(Clone, Debug, PartialEq, Serialize)]
99pub enum ResolveError {
100 NotFound { name: String },
102 Unsupported { name: String, detail: String },
105 Geometry { name: String, detail: String },
107}
108
109impl ResolveError {
110 pub fn status(&self) -> &'static str {
112 match self {
113 ResolveError::NotFound { .. } | ResolveError::Geometry { .. } => "invalid-selection",
114 ResolveError::Unsupported { .. } => "unsupported-selection",
115 }
116 }
117}
118
119impl std::fmt::Display for ResolveError {
120 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 match self {
122 ResolveError::NotFound { name } => write!(formatter, "selection '{name}' not found"),
123 ResolveError::Unsupported { name, detail } => {
124 write!(formatter, "selection '{name}' has no analytic frame: {detail}")
125 }
126 ResolveError::Geometry { name, detail } => {
127 write!(formatter, "selection '{name}' failed to resolve: {detail}")
128 }
129 }
130 }
131}
132
133impl SelectionGeometry {
134 pub fn mate_plane(&self) -> Option<MatePlane> {
136 match *self {
137 SelectionGeometry::Plane { origin, normal } => Some(MatePlane {
138 origin: triple(origin),
139 normal: triple(normal),
140 }),
141 _ => None,
142 }
143 }
144
145 pub fn mate_axis(&self) -> Option<MateAxis> {
149 let (origin, direction) = match *self {
150 SelectionGeometry::Axis {
151 origin, direction, ..
152 }
153 | SelectionGeometry::Line { origin, direction } => (origin, direction),
154 SelectionGeometry::Circle { center, axis, .. } => (center, axis),
155 _ => return None,
156 };
157 Some(MateAxis {
158 origin: triple(origin),
159 direction: triple(direction),
160 })
161 }
162
163 pub fn representative_point(&self) -> Vec3 {
166 match *self {
167 SelectionGeometry::Plane { origin, .. }
168 | SelectionGeometry::Axis { origin, .. }
169 | SelectionGeometry::Line { origin, .. } => origin,
170 SelectionGeometry::Sphere { center, .. }
171 | SelectionGeometry::Circle { center, .. } => center,
172 SelectionGeometry::Point { position } => position,
173 }
174 }
175
176 pub fn transformed(&self, transform: &AffineTransform) -> Result<Self, ResolveError> {
183 let scale = uniform_scale(transform).ok_or_else(|| ResolveError::Unsupported {
184 name: "transform".into(),
185 detail: "selection frames transform only by rigid/uniform-scale matrices".into(),
186 })?;
187 let direction = |vector: Vec3| {
188 linear(transform, vector)
189 .normalized()
190 .map_err(|error| ResolveError::Geometry {
191 name: "transform".into(),
192 detail: error,
193 })
194 };
195 Ok(match *self {
196 SelectionGeometry::Plane { origin, normal } => SelectionGeometry::Plane {
197 origin: transform.point(origin),
198 normal: direction(normal)?,
199 },
200 SelectionGeometry::Axis {
201 origin,
202 direction: axis,
203 radius,
204 } => SelectionGeometry::Axis {
205 origin: transform.point(origin),
206 direction: direction(axis)?,
207 radius: radius.map(|radius| radius * scale),
208 },
209 SelectionGeometry::Sphere { center, radius } => SelectionGeometry::Sphere {
210 center: transform.point(center),
211 radius: radius * scale,
212 },
213 SelectionGeometry::Circle {
214 center,
215 axis,
216 radius,
217 } => SelectionGeometry::Circle {
218 center: transform.point(center),
219 axis: direction(axis)?,
220 radius: radius * scale,
221 },
222 SelectionGeometry::Line {
223 origin,
224 direction: axis,
225 } => SelectionGeometry::Line {
226 origin: transform.point(origin),
227 direction: direction(axis)?,
228 },
229 SelectionGeometry::Point { position } => SelectionGeometry::Point {
230 position: transform.point(position),
231 },
232 })
233 }
234}
235
236fn triple(vector: Vec3) -> [f64; 3] {
237 [vector.x, vector.y, vector.z]
238}
239
240fn linear(transform: &AffineTransform, vector: Vec3) -> Vec3 {
242 let m = transform.elements;
243 Vec3::new(
244 m[0] * vector.x + m[1] * vector.y + m[2] * vector.z,
245 m[4] * vector.x + m[5] * vector.y + m[6] * vector.z,
246 m[8] * vector.x + m[9] * vector.y + m[10] * vector.z,
247 )
248}
249
250fn uniform_scale(transform: &AffineTransform) -> Option<f64> {
253 let m = transform.elements;
254 let columns = [
255 Vec3::new(m[0], m[4], m[8]),
256 Vec3::new(m[1], m[5], m[9]),
257 Vec3::new(m[2], m[6], m[10]),
258 ];
259 let lengths = [
260 columns[0].length(),
261 columns[1].length(),
262 columns[2].length(),
263 ];
264 let scale = (lengths[0] + lengths[1] + lengths[2]) / 3.0;
265 if !(scale > 0.0) {
266 return None;
267 }
268 if lengths
269 .iter()
270 .any(|length| (length - scale).abs() > 1e-9 * scale)
271 {
272 return None;
273 }
274 let orthogonality = 1e-9 * scale * scale;
275 if columns[0].dot(columns[1]).abs() > orthogonality
276 || columns[1].dot(columns[2]).abs() > orthogonality
277 || columns[0].dot(columns[2]).abs() > orthogonality
278 {
279 return None;
280 }
281 Some(scale)
282}
283
284pub fn split_component_namespace(name: &str) -> (Vec<&str>, &str) {
300 let mut chain = Vec::new();
301 let mut local = name;
302 while let Some((head, tail)) = local.split_once(':') {
303 if !is_component_reference(head) {
304 break;
305 }
306 chain.push(head);
307 local = tail;
308 }
309 (chain, local)
310}
311
312pub fn is_component_reference(name: &str) -> bool {
315 name.strip_prefix("ACOMP")
316 .map(|digits| !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()))
317 .unwrap_or(false)
318}
319
320pub fn resolve_named_selection(
329 solid: &BrepSolid,
330 name: &str,
331) -> Result<SelectionGeometry, ResolveError> {
332 if let Some(record) = solid
333 .shells
334 .iter()
335 .flat_map(|shell| &shell.faces)
336 .find(|face| face.name.as_deref() == Some(name))
337 {
338 return resolve_face_record(solid, record);
339 }
340 if let Some(record) = solid
341 .edges
342 .iter()
343 .find(|edge| edge.name.as_deref() == Some(name))
344 {
345 return resolve_edge_record(record);
346 }
347 Err(ResolveError::NotFound { name: name.into() })
348}
349
350pub fn resolve_face_selection(
353 solid: &BrepSolid,
354 face_id: u64,
355) -> Result<SelectionGeometry, ResolveError> {
356 let record = solid
357 .shells
358 .iter()
359 .flat_map(|shell| &shell.faces)
360 .find(|face| face.id == face_id)
361 .ok_or_else(|| ResolveError::NotFound {
362 name: format!("face {face_id}"),
363 })?;
364 resolve_face_record(solid, record)
365}
366
367pub fn resolve_edge_selection(
369 solid: &BrepSolid,
370 edge_id: u64,
371) -> Result<SelectionGeometry, ResolveError> {
372 let record = solid
373 .edges
374 .iter()
375 .find(|edge| edge.id == edge_id)
376 .ok_or_else(|| ResolveError::NotFound {
377 name: format!("edge {edge_id}"),
378 })?;
379 resolve_edge_record(record)
380}
381
382pub fn resolve_vertex_selection(
388 solid: &BrepSolid,
389 position: Vec3,
390) -> Result<SelectionGeometry, ResolveError> {
391 let mut best: Option<(f64, Vec3)> = None;
392 for vertex in &solid.vertices {
393 let distance = vertex.point.sub(position).length();
394 if best
395 .map(|(best_distance, _)| distance < best_distance)
396 .unwrap_or(true)
397 {
398 best = Some((distance, vertex.point));
399 }
400 }
401 match best {
402 Some((distance, point))
403 if distance <= VERTEX_SNAP_TOLERANCE * crate::solid_scale(solid) =>
404 {
405 Ok(SelectionGeometry::Point { position: point })
406 }
407 _ => Err(ResolveError::NotFound {
408 name: format!(
409 "vertex near ({}, {}, {})",
410 position.x, position.y, position.z
411 ),
412 }),
413 }
414}
415
416pub fn resolve_component_point(solids: &[&BrepSolid]) -> Result<SelectionGeometry, ResolveError> {
423 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
424 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
425 let mut any = false;
426 let mut include = |point: Vec3| {
427 low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
428 high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
429 any = true;
430 };
431 for solid in solids {
432 for vertex in &solid.vertices {
433 include(vertex.point);
434 }
435 for shell in &solid.shells {
436 for face in &shell.faces {
437 for row in &face.surface.control_points {
438 for control in row {
439 let point = control.point().map_err(|error| ResolveError::Geometry {
440 name: "component".into(),
441 detail: error,
442 })?;
443 include(point);
444 }
445 }
446 }
447 }
448 }
449 if !any {
450 return Err(ResolveError::Geometry {
451 name: "component".into(),
452 detail: "component has no geometry to anchor".into(),
453 });
454 }
455 Ok(SelectionGeometry::Point {
456 position: low.add(high).scale(0.5),
457 })
458}
459
460fn face_label(record: &FaceRecord) -> String {
465 record
466 .name
467 .clone()
468 .unwrap_or_else(|| format!("face {}", record.id))
469}
470
471fn resolve_face_record(
472 solid: &BrepSolid,
473 record: &FaceRecord,
474) -> Result<SelectionGeometry, ResolveError> {
475 match record.surface.analytic() {
476 Some(AnalyticSurface::RuledRevolution {
477 frame, rho0, rho1, ..
478 }) => {
479 let span = rho0.abs().max(rho1.abs());
482 let radius = ((rho0 - rho1).abs() <= RESOLVE_TOLERANCE * (1.0 + span))
483 .then_some(0.5 * (rho0 + rho1));
484 Ok(SelectionGeometry::Axis {
485 origin: frame.origin,
486 direction: frame.axis,
487 radius,
488 })
489 }
490 Some(AnalyticSurface::Sphere { frame, radius }) => Ok(SelectionGeometry::Sphere {
491 center: frame.origin,
492 radius: *radius,
493 }),
494 Some(AnalyticSurface::Torus { frame, .. }) => Ok(SelectionGeometry::Axis {
495 origin: frame.origin,
496 direction: frame.axis,
497 radius: None,
498 }),
499 Some(AnalyticSurface::Revolution {
500 frame, generatrix, ..
501 }) => Ok(SelectionGeometry::Axis {
502 origin: frame.origin,
503 direction: frame.axis,
504 radius: revolution_cylinder_radius(frame, generatrix),
505 }),
506 Some(AnalyticSurface::Plane { .. }) | None => planar_face(solid, record),
509 }
510}
511
512fn revolution_cylinder_radius(
519 frame: &crate::RevolutionFrame,
520 generatrix: &NurbsCurve,
521) -> Option<f64> {
522 if !control_net_is_colinear(generatrix) {
523 return None;
524 }
525 let [t0, t1] = generatrix.domain().ok()?;
526 let radial = |t: f64| -> Option<f64> {
527 let point = generatrix.evaluate(t).ok()?;
528 let delta = point.sub(frame.origin);
529 Some(delta.sub(frame.axis.scale(delta.dot(frame.axis))).length())
530 };
531 let rho0 = radial(t0)?;
532 let rho_mid = radial(0.5 * (t0 + t1))?;
533 let rho1 = radial(t1)?;
534 let span = rho0.abs().max(rho1.abs());
535 let tolerance = RESOLVE_TOLERANCE * (1.0 + span);
536 ((rho0 - rho1).abs() <= tolerance && (rho_mid - rho0).abs() <= tolerance)
537 .then_some((rho0 + rho_mid + rho1) / 3.0)
538}
539
540fn planar_face(solid: &BrepSolid, record: &FaceRecord) -> Result<SelectionGeometry, ResolveError> {
544 let geometry = (|| -> Result<Option<SelectionGeometry>, String> {
545 let [u0, u1] = record.surface.domain_u()?;
546 let [v0, v1] = record.surface.domain_v()?;
547 let (um, vm) = ((u0 + u1) * 0.5, (v0 + v1) * 0.5);
548 let center_normal = record.surface.normal(um, vm)?;
549 for u in [u0, um, u1] {
550 for v in [v0, vm, v1] {
551 if record.surface.normal(u, v)?.dot(center_normal) < 1.0 - 1e-6 {
552 return Ok(None);
553 }
554 }
555 }
556 let normal = if record.same_sense {
557 center_normal
558 } else {
559 center_normal.scale(-1.0)
560 }
561 .normalized()?;
562
563 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
566 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
567 let mut any = false;
568 for loop_record in &record.loops {
569 for coedge in &loop_record.coedges {
570 let Some(edge) = solid.edges.iter().find(|edge| edge.id == coedge.edge_id) else {
571 continue;
572 };
573 if edge.degenerate {
574 continue;
575 }
576 for step in 0..=4 {
577 let t = edge.t0 + (edge.t1 - edge.t0) * (step as f64 / 4.0);
578 let point = edge.curve.evaluate(t)?;
579 low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
580 high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
581 any = true;
582 }
583 }
584 }
585 let plane_point = record.surface.evaluate(um, vm)?;
586 let center = if any {
587 low.add(high).scale(0.5)
588 } else {
589 plane_point
590 };
591 let signed = center.sub(plane_point).dot(normal);
593 let origin = center.sub(normal.scale(signed));
594 Ok(Some(SelectionGeometry::Plane { origin, normal }))
595 })();
596 match geometry {
597 Ok(Some(frame)) => Ok(frame),
598 Ok(None) => Err(ResolveError::Unsupported {
599 name: face_label(record),
600 detail: "face carries no analytic frame (freeform surface)".into(),
601 }),
602 Err(error) => Err(ResolveError::Geometry {
603 name: face_label(record),
604 detail: error,
605 }),
606 }
607}
608
609fn edge_label(record: &EdgeRecord) -> String {
614 record
615 .name
616 .clone()
617 .unwrap_or_else(|| format!("edge {}", record.id))
618}
619
620fn resolve_edge_record(record: &EdgeRecord) -> Result<SelectionGeometry, ResolveError> {
621 if record.degenerate {
622 return Err(ResolveError::Unsupported {
623 name: edge_label(record),
624 detail: "degenerate edge".into(),
625 });
626 }
627 match edge_geometry(record) {
628 Ok(Some(frame)) => Ok(frame),
629 Ok(None) => Err(ResolveError::Unsupported {
630 name: edge_label(record),
631 detail: "edge is neither straight nor circular".into(),
632 }),
633 Err(error) => Err(ResolveError::Geometry {
634 name: edge_label(record),
635 detail: error,
636 }),
637 }
638}
639
640fn edge_geometry(record: &EdgeRecord) -> Result<Option<SelectionGeometry>, String> {
641 if let Some(line) = straight_edge_line(record)? {
642 return Ok(Some(line));
643 }
644 circular_edge_circle(record)
645}
646
647fn control_net_extent(curve: &NurbsCurve) -> f64 {
649 let mut extent = 0.0_f64;
650 for control in &curve.control_points {
651 if let Ok(point) = control.point() {
652 extent = extent
653 .max(point.x.abs())
654 .max(point.y.abs())
655 .max(point.z.abs());
656 }
657 }
658 extent
659}
660
661fn control_net_is_colinear(curve: &NurbsCurve) -> bool {
665 let points: Vec<Vec3> = curve
666 .control_points
667 .iter()
668 .filter_map(|control| control.point().ok())
669 .collect();
670 let Some((&first, rest)) = points.split_first() else {
671 return false;
672 };
673 let tolerance = RESOLVE_TOLERANCE * (1.0 + control_net_extent(curve));
674 let Some(direction) = rest
675 .iter()
676 .map(|point| point.sub(first))
677 .max_by(|a, b| a.length().total_cmp(&b.length()))
678 .and_then(|chord| chord.normalized().ok())
679 else {
680 return false;
681 };
682 points.iter().all(|point| {
683 let offset = point.sub(first);
684 offset.sub(direction.scale(offset.dot(direction))).length() <= tolerance
685 })
686}
687
688fn straight_edge_line(record: &EdgeRecord) -> Result<Option<SelectionGeometry>, String> {
691 if !control_net_is_colinear(&record.curve) {
692 return Ok(None);
693 }
694 let start = record.curve.evaluate(record.t0)?;
695 let end = record.curve.evaluate(record.t1)?;
696 let chord = end.sub(start);
697 if chord.length() <= RESOLVE_TOLERANCE * (1.0 + control_net_extent(&record.curve)) {
698 return Ok(None);
700 }
701 Ok(Some(SelectionGeometry::Line {
702 origin: start.add(end).scale(0.5),
703 direction: chord.normalized()?,
704 }))
705}
706
707fn circular_edge_circle(record: &EdgeRecord) -> Result<Option<SelectionGeometry>, String> {
713 let span = record.t1 - record.t0;
714 if !(span > 0.0) {
715 return Ok(None);
716 }
717 let at = |fraction: f64| record.curve.evaluate(record.t0 + span * fraction);
718 let (a, b, c) = (at(1.0 / 6.0)?, at(0.5)?, at(5.0 / 6.0)?);
719 let Some(center) = crate::analytic_surface::circumcenter(a, b, c) else {
720 return Ok(None);
721 };
722 let Ok(axis) = b.sub(a).cross(c.sub(b)).normalized() else {
724 return Ok(None);
725 };
726 let radius = a.sub(center).length();
727 let tolerance = RESOLVE_TOLERANCE * (1.0 + control_net_extent(&record.curve));
728 for step in 0..=CIRCLE_VERIFY_SAMPLES {
729 let point = at(step as f64 / CIRCLE_VERIFY_SAMPLES as f64)?;
730 let delta = point.sub(center);
731 if (delta.length() - radius).abs() > tolerance || delta.dot(axis).abs() > tolerance {
732 return Ok(None);
733 }
734 }
735 Ok(Some(SelectionGeometry::Circle {
736 center,
737 axis,
738 radius,
739 }))
740}
741
742