1use serde::{Deserialize, Serialize};
46
47use crate::arrangement::Vec2;
48use crate::classification::{parameter_point_in_face, PolygonClass};
49use crate::feature_pipeline::features::datum::rotate_euler_xyz;
50use crate::projection::{project_point_to_curve, project_point_to_surface};
51use crate::topology::{EdgeRecord, FaceRecord};
52use crate::{BrepSolid, KernelTolerances, NurbsCurve, Vec3};
53
54#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
57pub struct PmiPose {
58 pub center: [f64; 3],
59 pub translate: [f64; 3],
60 #[serde(rename = "rotateDeg")]
61 pub rotate_deg: [f64; 3],
62 pub scale: [f64; 3],
63}
64
65impl PmiPose {
66 fn radians(&self) -> [f64; 3] {
67 self.rotate_deg.map(f64::to_radians)
68 }
69
70 pub fn apply(&self, point: [f64; 3]) -> [f64; 3] {
72 let c = self.center;
73 let local = Vec3::new(
74 (point[0] - c[0]) * self.scale[0],
75 (point[1] - c[1]) * self.scale[1],
76 (point[2] - c[2]) * self.scale[2],
77 );
78 let rotated = rotate_euler_xyz(local, self.radians());
79 [
80 rotated.x + c[0] + self.translate[0],
81 rotated.y + c[1] + self.translate[1],
82 rotated.z + c[2] + self.translate[2],
83 ]
84 }
85
86 pub fn unapply(&self, point: [f64; 3]) -> [f64; 3] {
89 let c = self.center;
90 let moved = Vec3::new(
91 point[0] - c[0] - self.translate[0],
92 point[1] - c[1] - self.translate[1],
93 point[2] - c[2] - self.translate[2],
94 );
95 let e = self.radians();
96 let rx = rotate_euler_xyz(Vec3::new(1.0, 0.0, 0.0), e);
98 let ry = rotate_euler_xyz(Vec3::new(0.0, 1.0, 0.0), e);
99 let rz = rotate_euler_xyz(Vec3::new(0.0, 0.0, 1.0), e);
100 let local = Vec3::new(rx.dot(moved), ry.dot(moved), rz.dot(moved));
101 let unscale = |v: f64, s: f64| if s.abs() > 1e-300 { v / s } else { v };
102 [
103 unscale(local.x, self.scale[0]) + c[0],
104 unscale(local.y, self.scale[1]) + c[1],
105 unscale(local.z, self.scale[2]) + c[2],
106 ]
107 }
108
109 pub fn is_rigid(&self) -> bool {
112 self.scale.iter().all(|s| (s - 1.0).abs() < 1e-12)
113 }
114}
115
116pub fn pose_apply(stages: &[PmiPose], point: [f64; 3]) -> [f64; 3] {
119 stages.iter().fold(point, |p, stage| stage.apply(p))
120}
121
122pub fn pose_unapply(stages: &[PmiPose], point: [f64; 3]) -> [f64; 3] {
124 stages.iter().rev().fold(point, |p, stage| stage.unapply(p))
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129pub struct SolidPoses {
130 pub solid: String,
131 pub stages: Vec<PmiPose>,
132}
133
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
139pub struct HalfSpace {
140 pub point: [f64; 3],
141 pub normal: [f64; 3],
142 #[serde(default, skip_serializing_if = "Vec::is_empty")]
144 pub also: Vec<([f64; 3], [f64; 3])>,
145}
146
147impl HalfSpace {
148 pub fn of(planes: &[([f64; 3], [f64; 3])]) -> Option<Self> {
151 let (&(point, normal), rest) = planes.split_first()?;
152 Some(Self { point, normal, also: rest.to_vec() })
153 }
154
155 pub fn signed(&self, p: [f64; 3]) -> f64 {
159 let from = |point: [f64; 3], normal: [f64; 3]| (0..3).map(|i| (p[i] - point[i]) * normal[i]).sum::<f64>();
160 self.also.iter().fold(from(self.point, self.normal), |best, &(point, normal)| best.max(from(point, normal)))
161 }
162
163 pub fn keeps(&self, p: [f64; 3], tolerance: f64) -> bool {
165 self.signed(p) >= -tolerance
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171#[serde(tag = "mode", rename_all = "camelCase")]
172pub enum BalloonAnchorMode {
173 Surface,
175 Edge {
177 solid: String,
178 #[serde(rename = "edgeId")]
179 edge_id: u64,
180 },
181 Point,
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct BalloonAnchor {
188 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub camera: Option<super::PmiCamera>,
191 pub solids: Vec<String>,
194 #[serde(flatten)]
196 pub mode: BalloonAnchorMode,
197 #[serde(default, skip_serializing_if = "Vec::is_empty")]
199 pub poses: Vec<SolidPoses>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub section: Option<HalfSpace>,
203}
204
205impl BalloonAnchor {
206 pub fn stages(&self, solid: &str) -> &[PmiPose] {
208 self.poses
209 .iter()
210 .find(|poses| poses.solid == solid)
211 .map_or(&[], |poses| poses.stages.as_slice())
212 }
213
214 pub fn scope(&self, solid: &str) -> HeadScope<'_> {
216 HeadScope {
217 stages: self.stages(solid),
218 section: self.section.as_ref(),
219 }
220 }
221}
222
223#[derive(Debug, Clone, Copy)]
226pub struct HeadScope<'a> {
227 pub stages: &'a [PmiPose],
228 pub section: Option<&'a HalfSpace>,
229}
230
231impl HeadScope<'_> {
232 pub const PLAIN: HeadScope<'static> = HeadScope {
233 stages: &[],
234 section: None,
235 };
236
237 fn display(&self, modeling: Vec3) -> [f64; 3] {
238 pose_apply(self.stages, [modeling.x, modeling.y, modeling.z])
239 }
240
241 fn modeling(&self, display: [f64; 3]) -> Vec3 {
242 let p = pose_unapply(self.stages, display);
243 Vec3::new(p[0], p[1], p[2])
244 }
245
246 fn keeps(&self, display: [f64; 3], tolerance: f64) -> bool {
247 self.section.is_none_or(|section| section.keeps(display, tolerance))
248 }
249
250 fn is_rigid(&self) -> bool {
251 self.stages.iter().all(PmiPose::is_rigid)
252 }
253}
254
255struct Best {
258 display: [f64; 3],
259 d2: f64,
260}
261
262fn offer(best: &mut Option<Best>, scope: &HeadScope<'_>, bubble: [f64; 3], tolerance: f64, modeling: Vec3) {
263 let display = scope.display(modeling);
264 if !scope.keeps(display, tolerance) {
265 return;
266 }
267 let d2 = (0..3).map(|i| (display[i] - bubble[i]).powi(2)).sum::<f64>();
268 if best.as_ref().is_none_or(|b| d2 < b.d2) {
269 *best = Some(Best { display, d2 });
270 }
271}
272
273pub fn nearest_displayed_point(solid: &BrepSolid, scope: HeadScope<'_>, bubble: [f64; 3]) -> Result<Option<[f64; 3]>, String> {
281 let spatial = KernelTolerances::for_solid(solid, 1e-7).spatial();
282 let query = scope.modeling(bubble);
283 let mut best: Option<Best> = None;
284 for vertex in &solid.vertices {
285 offer(&mut best, &scope, bubble, spatial, vertex.point);
286 }
287 for edge in &solid.edges {
288 if edge.degenerate {
289 continue;
290 }
291 offer(&mut best, &scope, bubble, spatial, nearest_point_on_edge(edge, query)?);
292 if let Some(section) = scope.section {
293 for crossing in edge_plane_crossings(edge, &scope, section)? {
294 offer(&mut best, &scope, bubble, spatial, crossing);
295 }
296 }
297 }
298 let rigid = scope.is_rigid();
299 for shell in &solid.shells {
300 for face in &shell.faces {
301 if rigid {
306 if let (Some(best), Some(hull_d2)) = (best.as_ref(), hull_distance_squared(face, query)?) {
307 if hull_d2 > best.d2 {
308 continue;
309 }
310 }
311 }
312 let Ok(projection) = project_point_to_surface(&face.surface, query) else {
313 continue;
314 };
315 if rigid && best.as_ref().is_some_and(|b| projection.distance * projection.distance >= b.d2) {
316 continue;
317 }
318 let uv_band = match face.surface.derivatives(projection.u, projection.v, 1) {
319 Ok(derivatives) => crate::tolerance::surface_uv_tolerance(
320 spatial,
321 derivatives[1][0].length(),
322 derivatives[0][1].length(),
323 ),
324 Err(_) => spatial,
325 };
326 let Ok(class) = parameter_point_in_face(
327 face,
328 Vec2 {
329 x: projection.u,
330 y: projection.v,
331 },
332 uv_band,
333 ) else {
334 continue;
335 };
336 if matches!(class, PolygonClass::Inside | PolygonClass::Boundary) {
337 offer(&mut best, &scope, bubble, spatial, projection.point);
338 }
339 }
340 }
341 Ok(best.map(|b| b.display))
342}
343
344fn screen_control(camera: &super::PmiCamera, scope: HeadScope<'_>, p: crate::Vec4) -> crate::Vec4 {
347 let world = scope.display(Vec3::new(p.x / p.w, p.y / p.w, p.z / p.w));
348 let d = camera.view_direction();
349 let forward = Vec3::new(d[0], d[1], d[2]);
350 let up = Vec3::new(camera.up[0], camera.up[1], camera.up[2]);
351 let right = forward.cross(up);
352 let right = right.scale(1.0 / right.length().max(1e-15));
353 let up = right.cross(forward);
354 let v = Vec3::new(world[0] - camera.eye[0], world[1] - camera.eye[1], world[2] - camera.eye[2]);
355 let weight = match camera.projection {
356 super::PmiProjection::Orthographic { .. } => p.w,
357 super::PmiProjection::Perspective { .. } => p.w * v.dot(forward),
358 };
359 crate::Vec4 { x: v.dot(right) * p.w, y: v.dot(up) * p.w, z: 0.0, w: weight }
360}
361
362pub(super) fn head_distance(camera: Option<&super::PmiCamera>, a: [f64; 3], b: [f64; 3]) -> f64 {
363 let project = |p: [f64; 3]| {
364 if let Some(camera) = camera {
365 let q = screen_control(camera, HeadScope::PLAIN, crate::Vec4::from_point(Vec3::new(p[0], p[1], p[2]), 1.0));
366 [q.x / q.w, q.y / q.w, 0.0]
367 } else { p }
368 };
369 let a = project(a);
370 let b = project(b);
371 (0..3).map(|i| (a[i] - b[i]).powi(2)).sum()
372}
373
374fn planar_section_feet(face: &FaceRecord, scope: HeadScope<'_>, camera: &super::PmiCamera, query: Vec3, spatial: f64) -> Result<Vec<Vec3>, String> {
379 let Some(section) = scope.section else { return Ok(Vec::new()); };
380 let Some(crate::AnalyticSurface::Plane { .. }) = face.surface.analytic() else { return Ok(Vec::new()); };
381 let [u0, u1] = face.surface.domain_u()?;
382 let [v0, v1] = face.surface.domain_v()?;
383 let corners = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
384 let mut feet = Vec::new();
385 for (point, normal) in std::iter::once((section.point, section.normal)).chain(section.also.iter().copied()) {
386 let mut crossings: Vec<[f64; 2]> = Vec::new();
387 for k in 0..4 {
388 let a = corners[k];
389 let b = corners[(k + 1) % 4];
390 let signed = |uv: [f64; 2]| -> Result<f64, String> {
391 let p = scope.display(face.surface.evaluate(uv[0], uv[1])?);
392 Ok((0..3).map(|i| (p[i] - point[i]) * normal[i]).sum())
393 };
394 let da = signed(a)?;
395 let db = signed(b)?;
396 if da.abs() <= spatial { crossings.push(a); }
397 if da * db < 0.0 {
398 let t = da / (da - db);
399 crossings.push([a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])]);
400 }
401 }
402 let pair = crossings.iter().flat_map(|a| crossings.iter().map(move |b| (*a, *b)))
404 .max_by(|(a, b), (c, d)| {
405 let length = |a: &[f64; 2], b: &[f64; 2]| (a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2);
406 length(a, b).total_cmp(&length(c, d))
407 });
408 let Some((a, b)) = pair else { continue; };
409 if a == b { continue; }
410 let controls = [a, b].into_iter().map(|uv| {
411 face.surface.evaluate(uv[0], uv[1]).map(|p| screen_control(camera, scope, crate::Vec4::from_point(p, 1.0)))
412 }).collect::<Result<Vec<_>, _>>()?;
413 let Ok(line) = NurbsCurve::new(1, vec![0.0, 0.0, 1.0, 1.0], controls) else { continue; };
414 let Ok(foot) = project_point_to_curve(&line, query) else { continue; };
415 let uv = Vec2 { x: a[0] + foot.u * (b[0] - a[0]), y: a[1] + foot.u * (b[1] - a[1]) };
416 let derivatives = face.surface.derivatives(uv.x, uv.y, 1)?;
417 let band = crate::tolerance::surface_uv_tolerance(spatial, derivatives[1][0].length(), derivatives[0][1].length());
418 if matches!(parameter_point_in_face(face, uv, band), Ok(PolygonClass::Inside | PolygonClass::Boundary)) {
419 feet.push(face.surface.evaluate(uv.x, uv.y)?);
420 }
421 }
422 Ok(feet)
423}
424
425pub fn nearest_screen_point(
429 solid: &BrepSolid, scope: HeadScope<'_>, bubble: [f64; 3], camera: Option<&super::PmiCamera>,
430) -> Result<Option<[f64; 3]>, String> {
431 let Some(camera) = camera else { return nearest_displayed_point(solid, scope, bubble); };
432 let q = screen_control(camera, HeadScope::PLAIN, crate::Vec4::from_point(Vec3::new(bubble[0], bubble[1], bubble[2]), 1.0));
433 let query = Vec3::new(q.x / q.w, q.y / q.w, 0.0);
434 let spatial = KernelTolerances::for_solid(solid, 1e-7).spatial();
435 let mut best: Option<Best> = None;
436 let direction = camera.view_direction();
437 let depth = |p: [f64; 3]| (0..3).map(|i| (p[i] - camera.eye[i]) * direction[i]).sum::<f64>();
438 let mut offer = |point: Vec3| {
439 let display = scope.display(point);
440 if !scope.keeps(display, spatial) { return; }
441 if matches!(camera.projection, super::PmiProjection::Perspective { .. }) && depth(display) <= 0.0 { return; }
442 let d2 = head_distance(Some(camera), display, bubble);
443 if d2.is_finite() && best.as_ref().is_none_or(|b| d2 < b.d2 - 1e-12 || ((d2 - b.d2).abs() <= 1e-12 && depth(display) < depth(b.display))) {
444 best = Some(Best { display, d2 });
445 }
446 };
447 for vertex in &solid.vertices { offer(vertex.point); }
448 for edge in &solid.edges {
449 for t in [edge.t0, edge.t1] { offer(edge.curve.evaluate(t)?); }
450 if edge.degenerate { continue; }
451 let span = span_curve(&edge.curve, edge.t0.min(edge.t1), edge.t0.max(edge.t1))?;
452 let projected = NurbsCurve::new(span.degree, span.knots.clone(), span.control_points.iter().map(|p| screen_control(camera, scope, *p)).collect());
453 if let Ok(curve) = projected {
454 if let Ok(foot) = project_point_to_curve(&curve, query) { offer(edge.curve.evaluate(foot.u)?); }
455 }
456 if let Some(section) = scope.section {
457 for p in edge_plane_crossings(edge, &scope, section)? { offer(p); }
458 }
459 }
460 for face in solid.shells.iter().flat_map(|s| &s.faces) {
461 for foot in planar_section_feet(face, scope, camera, query, spatial)? { offer(foot); }
462 let s = &face.surface;
463 let projected = crate::NurbsSurface::new(s.degree_u, s.degree_v, s.knots_u.clone(), s.knots_v.clone(),
466 s.control_points.iter().map(|row| row.iter().map(|p| screen_control(camera, scope, *p)).collect()).collect());
467 let Ok(surface) = projected else { continue; };
468 let Ok(foot) = project_point_to_surface(&surface, query) else { continue; };
469 let derivatives = s.derivatives(foot.u, foot.v, 1)?;
470 let band = crate::tolerance::surface_uv_tolerance(spatial, derivatives[1][0].length(), derivatives[0][1].length());
471 if matches!(parameter_point_in_face(face, Vec2 { x: foot.u, y: foot.v }, band), Ok(PolygonClass::Inside | PolygonClass::Boundary)) {
472 offer(s.evaluate(foot.u, foot.v)?);
473 }
474 }
475 Ok(best.map(|b| b.display))
476}
477
478pub fn nearest_point_on_solid(solid: &BrepSolid, point: Vec3) -> Result<Vec3, String> {
481 nearest_displayed_point(solid, HeadScope::PLAIN, [point.x, point.y, point.z])?
482 .map(|p| Vec3::new(p[0], p[1], p[2]))
483 .ok_or_else(|| "solid has no geometry".to_string())
484}
485
486fn hull_distance_squared(face: &FaceRecord, point: Vec3) -> Result<Option<f64>, String> {
489 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
490 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
491 let mut any = false;
492 for row in &face.surface.control_points {
493 for control in row {
494 let p = control.point()?;
495 low = Vec3::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
496 high = Vec3::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
497 any = true;
498 }
499 }
500 if !any {
501 return Ok(None);
502 }
503 let axis = |v: f64, lo: f64, hi: f64| if v < lo { lo - v } else if v > hi { v - hi } else { 0.0 };
504 let dx = axis(point.x, low.x, high.x);
505 let dy = axis(point.y, low.y, high.y);
506 let dz = axis(point.z, low.z, high.z);
507 Ok(Some(dx * dx + dy * dy + dz * dz))
508}
509
510pub fn nearest_point_on_edge(edge: &EdgeRecord, point: Vec3) -> Result<Vec3, String> {
514 if edge.degenerate {
515 return edge.curve.evaluate(edge.t0);
516 }
517 let span = span_curve(&edge.curve, edge.t0.min(edge.t1), edge.t0.max(edge.t1))?;
518 let projection = project_point_to_curve(&span, point)?;
519 let mut best = (projection.distance, projection.point);
520 for parameter in [edge.t0, edge.t1] {
523 let end = edge.curve.evaluate(parameter)?;
524 let distance = end.sub(point).length();
525 if distance < best.0 {
526 best = (distance, end);
527 }
528 }
529 Ok(best.1)
530}
531
532pub fn nearest_displayed_point_on_edge(edge: &EdgeRecord, scope: HeadScope<'_>, bubble: [f64; 3]) -> Result<Option<[f64; 3]>, String> {
537 let tolerance = 1e-7;
538 let mut best: Option<Best> = None;
539 offer(&mut best, &scope, bubble, tolerance, nearest_point_on_edge(edge, scope.modeling(bubble))?);
540 if let Some(section) = scope.section {
541 if best.is_none() && !edge.degenerate {
542 for crossing in edge_plane_crossings(edge, &scope, section)? {
543 offer(&mut best, &scope, bubble, tolerance, crossing);
544 }
545 }
546 }
547 Ok(best.map(|b| b.display))
548}
549
550fn edge_plane_crossings(edge: &EdgeRecord, scope: &HeadScope<'_>, section: &HalfSpace) -> Result<Vec<Vec3>, String> {
554 const SAMPLES: usize = 64;
555 let (t0, t1) = (edge.t0.min(edge.t1), edge.t0.max(edge.t1));
556 if !(t1 > t0) {
557 return Ok(Vec::new());
558 }
559 let signed_at = |t: f64| -> Result<f64, String> { Ok(section.signed(scope.display(edge.curve.evaluate(t)?))) };
560 let mut out = Vec::new();
561 let mut previous = (t0, signed_at(t0)?);
562 for k in 1..=SAMPLES {
563 let t = t0 + (t1 - t0) * k as f64 / SAMPLES as f64;
564 let s = signed_at(t)?;
565 if previous.1 == 0.0 {
566 out.push(edge.curve.evaluate(previous.0)?);
567 } else if (previous.1 < 0.0) != (s < 0.0) && s != 0.0 {
568 let (mut a, mut sa, mut b) = (previous.0, previous.1, t);
569 for _ in 0..60 {
570 let m = (a + b) * 0.5;
571 let sm = signed_at(m)?;
572 if (sm < 0.0) == (sa < 0.0) {
573 a = m;
574 sa = sm;
575 } else {
576 b = m;
577 }
578 }
579 out.push(edge.curve.evaluate((a + b) * 0.5)?);
580 }
581 previous = (t, s);
582 }
583 if previous.1 == 0.0 {
584 out.push(edge.curve.evaluate(previous.0)?);
585 }
586 Ok(out)
587}
588
589fn span_curve(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<NurbsCurve, String> {
593 let [start, end] = curve.domain()?;
594 let eps = (end - start).abs() * 1e-9;
595 let mut span = curve.clone();
596 if t0 > start + eps && t0 < end - eps {
597 span = span.split(t0)?.1;
598 }
599 let [_, span_end] = span.domain()?;
600 if t1 > start + eps && t1 < span_end - eps {
601 span = span.split(t1)?.0;
602 }
603 Ok(span)
604}
605
606pub fn balloon_head<'a>(
613 anchor: &BalloonAnchor,
614 bubble: [f64; 3],
615 mut lookup: impl FnMut(&str) -> Option<&'a BrepSolid>,
616 missing: &mut Vec<String>,
617) -> Result<Option<[f64; 3]>, String> {
618 match &anchor.mode {
619 BalloonAnchorMode::Point => Ok(None),
620 BalloonAnchorMode::Edge { solid, edge_id } => {
621 let Some(brep) = lookup(solid) else {
622 missing.push(solid.clone());
623 return Ok(None);
624 };
625 let edge = brep
626 .edges
627 .iter()
628 .find(|edge| edge.id == *edge_id)
629 .ok_or_else(|| format!("anchor edge {edge_id} is not on '{solid}'"))?;
630 nearest_displayed_point_on_edge(edge, anchor.scope(solid), bubble)?
631 .map(Some)
632 .ok_or_else(|| format!("anchor edge {edge_id} of '{solid}' is sectioned away in this view"))
633 }
634 BalloonAnchorMode::Surface => {
635 let mut best: Option<Best> = None;
636 for name in &anchor.solids {
637 let Some(brep) = lookup(name) else {
638 missing.push(name.clone());
639 continue;
640 };
641 if let Some(display) = nearest_screen_point(brep, anchor.scope(name), bubble, anchor.camera.as_ref())? {
642 let d2 = head_distance(anchor.camera.as_ref(), display, bubble);
643 if best.as_ref().is_none_or(|b| d2 < b.d2) {
644 best = Some(Best { display, d2 });
645 }
646 }
647 }
648 if !missing.is_empty() {
649 return Ok(None);
650 }
651 best.map(|b| Some(b.display))
652 .ok_or_else(|| "the balloon's occurrence has no displayed geometry in this view".to_string())
653 }
654 }
655}