1use 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
32fn 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
44pub 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 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
87pub 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
105pub 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
141pub fn is_straight_edge(geometry: &SelectionGeometry) -> bool {
143 matches!(geometry, SelectionGeometry::Line { .. })
144}
145
146pub 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
157pub 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
187pub 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
200pub 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
212pub 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
240fn 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
247fn 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
254pub 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 return Ok((oa, foot_on_line(oa, ob, db)));
282 }
283 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
339pub 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}