brepkit_operations/
projection.rs1use brepkit_math::vec::{Point2, Point3, Vec3};
10use brepkit_topology::Topology;
11use brepkit_topology::solid::SolidId;
12
13use crate::classify::{PointClassification, classify_point};
14
15#[derive(Debug, Clone, Default)]
18pub struct ProjectedEdges {
19 pub visible: Vec<Vec<Point2>>,
21 pub hidden: Vec<Vec<Point2>>,
23}
24
25pub fn project_edges(
40 topo: &Topology,
41 solid: SolidId,
42 origin: Point3,
43 direction: Vec3,
44 x_axis: Vec3,
45 hidden_lines: bool,
46 deflection: f64,
47) -> Result<ProjectedEdges, crate::OperationsError> {
48 let view = direction
49 .normalize()
50 .map_err(|_| crate::OperationsError::InvalidInput {
51 reason: "projection direction must be non-zero".into(),
52 })?;
53 let x = (x_axis - view * view.dot(x_axis))
55 .normalize()
56 .map_err(|_| crate::OperationsError::InvalidInput {
57 reason: "projection x_axis is parallel to the direction".into(),
58 })?;
59 let y = x.cross(view);
60
61 let project = |p: Point3| -> Point2 {
62 let v = p - origin;
63 Point2::new(x.dot(v), y.dot(v))
64 };
65
66 let bbox = crate::measure::solid_bounding_box(topo, solid)?;
71 let diag = (bbox.max - bbox.min).length();
72 let eps = (diag * 1e-4).clamp(1e-6, 1e-2);
73
74 let is_hidden = |p: Point3| -> Result<bool, crate::OperationsError> {
79 Ok(
80 classify_point(topo, solid, p - view * eps, deflection, 1e-7)?
81 == PointClassification::Inside,
82 )
83 };
84
85 let lines = crate::tessellate::sample_solid_edges(topo, solid, deflection)?;
86 let n_edges = lines.offsets.len();
87 let mut result = ProjectedEdges::default();
88
89 for i in 0..n_edges {
90 let start = lines.offsets[i];
91 let end = if i + 1 < n_edges {
92 lines.offsets[i + 1]
93 } else {
94 lines.positions.len()
95 };
96 let pts = &lines.positions[start..end];
97 if pts.len() < 2 {
98 continue;
99 }
100
101 let seg_hidden: Vec<bool> = (0..pts.len() - 1)
105 .map(|j| {
106 let mid = Point3::new(
107 0.5 * (pts[j].x() + pts[j + 1].x()),
108 0.5 * (pts[j].y() + pts[j + 1].y()),
109 0.5 * (pts[j].z() + pts[j + 1].z()),
110 );
111 is_hidden(mid)
112 })
113 .collect::<Result<Vec<bool>, _>>()?;
114
115 let mut j = 0;
116 while j < seg_hidden.len() {
117 let hidden = seg_hidden[j];
118 let run_start = j;
119 while j < seg_hidden.len() && seg_hidden[j] == hidden {
120 j += 1;
121 }
122 if hidden && !hidden_lines {
124 continue;
125 }
126 let poly: Vec<Point2> = (run_start..=j).map(|k| project(pts[k])).collect();
127 if hidden {
128 result.hidden.push(poly);
129 } else {
130 result.visible.push(poly);
131 }
132 }
133 }
134
135 Ok(result)
136}
137
138#[cfg(test)]
139mod tests {
140 #![allow(clippy::unwrap_used)]
141
142 use super::*;
143
144 fn oblique() -> (Point3, Vec3, Vec3) {
147 (
148 Point3::new(-100.0, -100.0, -100.0),
149 Vec3::new(1.0, 1.0, 1.0),
150 Vec3::new(1.0, -1.0, 0.0),
151 )
152 }
153
154 #[test]
155 fn project_box_oblique_view_has_visible_and_hidden_edges() {
156 let mut topo = Topology::new();
157 let solid = crate::primitives::make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
158 let (o, d, x) = oblique();
159 let result = project_edges(&topo, solid, o, d, x, true, 0.1).unwrap();
160 assert!(
161 !result.visible.is_empty(),
162 "oblique view must have visible edges"
163 );
164 assert!(
165 !result.hidden.is_empty(),
166 "the far corner's edges must be hidden behind the box"
167 );
168 }
169
170 #[test]
171 fn project_box_without_hidden_lines_drops_hidden() {
172 let mut topo = Topology::new();
173 let solid = crate::primitives::make_box(&mut topo, 10.0, 10.0, 10.0).unwrap();
174 let (o, d, x) = oblique();
175 let result = project_edges(&topo, solid, o, d, x, false, 0.1).unwrap();
176 assert!(!result.visible.is_empty());
177 assert!(
178 result.hidden.is_empty(),
179 "hidden lines disabled → no hidden polylines"
180 );
181 }
182}