Skip to main content

brep_kernel/feature_pipeline/pmi/
resolve.rs

1//! PMI reference resolution and the measurement helpers every annotation
2//! resolver shares.
3//!
4//! A reference is a kernel entity NAME; both plain and component-owned
5//! geometry resolve (the assembly constraints' component fence does not apply
6//! — PMI annotates whatever is on screen). Conventions:
7//!
8//! - a face / edge name → its analytic frame through
9//!   `crate::assembly_resolve` (plane, axis, sphere, circle, line);
10//! - `{solid}@x,y,z` → the vertex of `solid` nearest that WORLD position
11//!   (PMI resolves against the world-posed resident solids, the same solids
12//!   the STEP exporter writes — unlike an assembly constraint's
13//!   component-local vertex ref);
14//! - a D / P frame name → the plane (origin + z axis);
15//! - a solid name → its representative point (the aggregate bbox center).
16
17use crate::feature_pipeline::SceneMap;
18use crate::{
19    resolve_edge_selection, resolve_face_selection, resolve_vertex_selection, SelectionGeometry,
20    Vec3,
21};
22
23
24pub fn v3(a: [f64; 3]) -> Vec3 {
25    Vec3::new(a[0], a[1], a[2])
26}
27
28pub fn a3(v: Vec3) -> [f64; 3] {
29    [v.x, v.y, v.z]
30}
31
32/// Parse `x,y,z`.
33fn parse_triple(text: &str) -> Option<Vec3> {
34    let mut parts = text.split(',').map(|part| part.trim().parse::<f64>());
35    let x = parts.next()?.ok()?;
36    let y = parts.next()?.ok()?;
37    let z = parts.next()?.ok()?;
38    if parts.next().is_some() {
39        return None;
40    }
41    Some(Vec3::new(x, y, z))
42}
43
44/// Resolve one reference name to its analytic frame (module docs).
45pub fn resolve_reference(scene: &SceneMap, name: &str) -> Result<SelectionGeometry, String> {
46    let name = name.trim();
47    if name.is_empty() {
48        return Err("empty reference".into());
49    }
50    // Vertex ref: `{solid}@x,y,z` in world coordinates.
51    if let Some((solid_name, coords)) = name.split_once('@') {
52        let handle = scene
53            .resolve_solid(solid_name)
54            .ok_or_else(|| format!("vertex ref '{name}': unknown solid '{solid_name}'"))?;
55        let position = parse_triple(coords)
56            .ok_or_else(|| format!("vertex ref '{name}': position must be 'x,y,z' numbers"))?;
57        return crate::with_registered_solid_str(handle, |solid| {
58            Ok(resolve_vertex_selection(solid, position))
59        })?
60        .map_err(|error| format!("vertex ref '{name}': {error}"));
61    }
62    if let Some(face) = scene.resolve_face(name) {
63        return crate::with_registered_solid_str(face.handle, |solid| {
64            Ok(resolve_face_selection(solid, face.face_id))
65        })?
66        .map_err(|error| format!("face '{name}': {error}"));
67    }
68    if let Some(edge) = scene.resolve_edge(name) {
69        return crate::with_registered_solid_str(edge.handle, |solid| {
70            Ok(resolve_edge_selection(solid, edge.edge_id))
71        })?
72        .map_err(|error| format!("edge '{name}': {error}"));
73    }
74    if let Some(frame) = scene.resolve_frame(name) {
75        return Ok(SelectionGeometry::Plane {
76            origin: frame.origin,
77            normal: frame.z_axis,
78        });
79    }
80    if let Some(handle) = scene.resolve_solid(name) {
81        let center = solid_bbox_center(handle)?;
82        return Ok(SelectionGeometry::Point { position: center });
83    }
84    Err(format!("'{name}' is not in the model"))
85}
86
87/// The world endpoints of a named edge (its curve evaluated at the trimmed
88/// span ends), for a single-edge length dimension.
89pub fn edge_endpoints(scene: &SceneMap, name: &str) -> Result<(Vec3, Vec3), String> {
90    let edge = scene
91        .resolve_edge(name)
92        .ok_or_else(|| format!("'{name}' is not an edge"))?;
93    crate::with_registered_solid_str(edge.handle, |solid| {
94        let record = solid
95            .edges
96            .iter()
97            .find(|record| record.id == edge.edge_id)
98            .ok_or_else(|| format!("edge '{name}' has no record"))?;
99        let start = record.curve.evaluate(record.t0)?;
100        let end = record.curve.evaluate(record.t1)?;
101        Ok((start, end))
102    })
103}
104
105/// The span `(low, high)` a named face covers along the line `(origin,
106/// direction)` (unit direction): its boundary edges sampled and projected.
107/// What a centerline drawn along a cylinder's or cone's axis spans, since an
108/// axis frame carries a point on the axis and no extent.
109pub fn face_axial_span(scene: &SceneMap, name: &str, origin: Vec3, direction: Vec3) -> Result<(f64, f64), String> {
110    let face = scene
111        .resolve_face(name)
112        .ok_or_else(|| format!("'{name}' is not a face"))?;
113    crate::with_registered_solid_str(face.handle, |solid| {
114        let record = solid
115            .shells
116            .iter()
117            .flat_map(|shell| shell.faces.iter())
118            .find(|record| record.id == face.face_id)
119            .ok_or_else(|| format!("face '{name}' has no record"))?;
120        let mut low = f64::INFINITY;
121        let mut high = f64::NEG_INFINITY;
122        for coedge in record.loops.iter().flat_map(|lp| lp.coedges.iter()) {
123            let Some(edge) = solid.edges.iter().find(|edge| edge.id == coedge.edge_id) else {
124                continue;
125            };
126            const SAMPLES: usize = 8;
127            for k in 0..=SAMPLES {
128                let t = edge.t0 + (edge.t1 - edge.t0) * k as f64 / SAMPLES as f64;
129                let station = edge.curve.evaluate(t)?.sub(origin).dot(direction);
130                low = low.min(station);
131                high = high.max(station);
132            }
133        }
134        if !(high >= low) {
135            return Err(format!("face '{name}' has no boundary to measure along its axis"));
136        }
137        Ok((low, high))
138    })
139}
140
141/// Whether `name` resolves to a straight edge.
142pub fn is_straight_edge(geometry: &SelectionGeometry) -> bool {
143    matches!(geometry, SelectionGeometry::Line { .. })
144}
145
146/// The center of the aggregate AABB of a resident solid's topology vertices
147/// and surface control hulls (bounds the exact surfaces without tessellation).
148pub fn solid_bbox_center(handle: u32) -> Result<Vec3, String> {
149    let (low, high) = solid_bbox(handle)?;
150    Ok(Vec3::new(
151        (low.x + high.x) * 0.5,
152        (low.y + high.y) * 0.5,
153        (low.z + high.z) * 0.5,
154    ))
155}
156
157/// The aggregate AABB (topology vertices ∪ control-point hulls) of a resident solid.
158pub fn solid_bbox(handle: u32) -> Result<(Vec3, Vec3), String> {
159    crate::with_registered_solid_str(handle, |solid| {
160        let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
161        let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
162        let mut any = false;
163        let mut include = |point: Vec3| {
164            low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
165            high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
166            any = true;
167        };
168        for vertex in &solid.vertices {
169            include(vertex.point);
170        }
171        for shell in &solid.shells {
172            for face in &shell.faces {
173                for row in &face.surface.control_points {
174                    for control in row {
175                        include(control.point()?);
176                    }
177                }
178            }
179        }
180        if !any {
181            return Err("solid has no geometry".into());
182        }
183        Ok((low, high))
184    })
185}
186
187/// The direction an element carries: a plane's normal, a line's / axis's
188/// direction, a circle's axis. Points and spheres carry none.
189pub fn direction_of(geometry: &SelectionGeometry) -> Option<Vec3> {
190    match geometry {
191        SelectionGeometry::Plane { normal, .. } => Some(*normal),
192        SelectionGeometry::Line { direction, .. } | SelectionGeometry::Axis { direction, .. } => {
193            Some(*direction)
194        }
195        SelectionGeometry::Circle { axis, .. } => Some(*axis),
196        SelectionGeometry::Sphere { .. } | SelectionGeometry::Point { .. } => None,
197    }
198}
199
200/// A lowercase geometry class word for messages.
201pub fn class_of(geometry: &SelectionGeometry) -> &'static str {
202    match geometry {
203        SelectionGeometry::Plane { .. } => "plane",
204        SelectionGeometry::Axis { .. } => "axis",
205        SelectionGeometry::Sphere { .. } => "sphere",
206        SelectionGeometry::Circle { .. } => "circle",
207        SelectionGeometry::Line { .. } => "line",
208        SelectionGeometry::Point { .. } => "point",
209    }
210}
211
212/// The measurement classification of an element for a linear dimension:
213/// planes stay planes; straight edges and axis-bearing faces measure on their
214/// INFINITE carrier lines; circles and spheres from their centers; vertices
215/// from their points.
216pub enum LinearSide {
217    Plane { origin: Vec3, normal: Vec3 },
218    Line { origin: Vec3, direction: Vec3 },
219    Point(Vec3),
220}
221
222pub fn classify_linear(geometry: &SelectionGeometry) -> LinearSide {
223    match *geometry {
224        SelectionGeometry::Plane { origin, normal } => LinearSide::Plane { origin, normal },
225        SelectionGeometry::Line { origin, direction }
226        | SelectionGeometry::Axis {
227            origin, direction, ..
228        } => LinearSide::Line { origin, direction },
229        SelectionGeometry::Circle { center, .. } | SelectionGeometry::Sphere { center, .. } => {
230            LinearSide::Point(center)
231        }
232        SelectionGeometry::Point { position } => LinearSide::Point(position),
233    }
234}
235
236fn unit(v: Vec3) -> Result<Vec3, String> {
237    v.normalized().map_err(|_| "degenerate direction".to_string())
238}
239
240/// Foot of the perpendicular from `point` onto the line `(origin, direction)`.
241fn foot_on_line(point: Vec3, origin: Vec3, direction: Vec3) -> Vec3 {
242    let d = direction;
243    let t = point.sub(origin).dot(d) / d.dot(d).max(1e-300);
244    origin.add(d.scale(t))
245}
246
247/// Foot of the perpendicular from `point` onto the plane `(origin, normal)`.
248fn foot_on_plane(point: Vec3, origin: Vec3, normal: Vec3) -> Vec3 {
249    let n = normal;
250    let h = point.sub(origin).dot(n) / n.dot(n).max(1e-300);
251    point.sub(n.scale(h))
252}
253
254/// The two points a linear dimension measures between (`a` on the first
255/// element, `b` on the second), per the module's classification. Errors name
256/// the unsupported pairing (a non-parallel plane pair, a plane against a
257/// crossing line).
258pub fn linear_between(first: &SelectionGeometry, second: &SelectionGeometry) -> Result<(Vec3, Vec3), String> {
259    use LinearSide::*;
260    match (classify_linear(first), classify_linear(second)) {
261        (Point(p), Point(q)) => Ok((p, q)),
262        (Point(p), Line { origin, direction }) => Ok((p, foot_on_line(p, origin, direction))),
263        (Line { origin, direction }, Point(q)) => Ok((foot_on_line(q, origin, direction), q)),
264        (Point(p), Plane { origin, normal }) => Ok((p, foot_on_plane(p, origin, normal))),
265        (Plane { origin, normal }, Point(q)) => Ok((foot_on_plane(q, origin, normal), q)),
266        (
267            Line {
268                origin: oa,
269                direction: da,
270            },
271            Line {
272                origin: ob,
273                direction: db,
274            },
275        ) => {
276            let da = unit(da)?;
277            let db = unit(db)?;
278            let cross = da.cross(db);
279            if cross.length() < 1e-7 {
280                // Parallel carriers: the spacing, measured from the first origin.
281                return Ok((oa, foot_on_line(oa, ob, db)));
282            }
283            // Skew / crossing: the closest points on the two infinite lines.
284            let w = oa.sub(ob);
285            let a = da.dot(da);
286            let b = da.dot(db);
287            let c = db.dot(db);
288            let d = da.dot(w);
289            let e = db.dot(w);
290            let denom = a * c - b * b;
291            let s = (b * e - c * d) / denom;
292            let t = (a * e - b * d) / denom;
293            Ok((oa.add(da.scale(s)), ob.add(db.scale(t))))
294        }
295        (
296            Plane {
297                origin: oa,
298                normal: na,
299            },
300            Plane {
301                origin: ob,
302                normal: nb,
303            },
304        ) => {
305            let na = unit(na)?;
306            let nb = unit(nb)?;
307            if na.cross(nb).length() > 1e-6 {
308                return Err("the two planes are not parallel — a linear dimension needs parallel planes (use an angle dimension)".into());
309            }
310            Ok((oa, foot_on_plane(oa, ob, nb)))
311        }
312        (
313            Plane {
314                origin: po,
315                normal,
316            },
317            Line { origin, direction },
318        ) => {
319            if unit(normal)?.dot(unit(direction)?).abs() > 1e-6 {
320                return Err("the line is not parallel to the plane".into());
321            }
322            Ok((foot_on_plane(origin, po, normal), origin))
323        }
324        (
325            Line { origin, direction },
326            Plane {
327                origin: po,
328                normal,
329            },
330        ) => {
331            if unit(normal)?.dot(unit(direction)?).abs() > 1e-6 {
332                return Err("the line is not parallel to the plane".into());
333            }
334            Ok((origin, foot_on_plane(origin, po, normal)))
335        }
336    }
337}
338
339/// A unit vector perpendicular to `axis` — `preferred` projected into the
340/// plane when it has a component there, else an arbitrary perpendicular.
341pub fn perpendicular_in_plane(axis: Vec3, preferred: Vec3) -> Vec3 {
342    let axis = axis.normalized().unwrap_or(Vec3::new(0.0, 0.0, 1.0));
343    let planar = preferred.sub(axis.scale(preferred.dot(axis)));
344    match planar.normalized() {
345        Ok(v) if planar.length() > 1e-9 => v,
346        _ => axis
347            .perpendicular()
348            .unwrap_or(Vec3::new(1.0, 0.0, 0.0)),
349    }
350}