1use ogeom_algo::{Built, History};
17use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
18use ogeom_geom::{
19 Curve, Curve3d as _, PlanarCurve, Surface as _, SurfaceGeometry, Transformable as _,
20};
21use ogeom_math::{Point, Point2};
22use ogeom_topo::{EdgeRepr, Model, NodeData, Shape, ShapeType, SurfaceId, explore_unique};
23
24#[derive(Debug, Clone)]
26pub struct Projected {
27 pub edge: Shape,
29 pub face: Shape,
31 pub tolerance: f64,
33}
34
35pub fn normal_projection(
47 model: &mut Model,
48 target: &Shape,
49 wire: &Shape,
50 stations: usize,
51 tolerance: f64,
52 tol: Tolerances,
53) -> OgeomResult<(Vec<Projected>, Built)> {
54 if stations < 4 {
55 ogeom_bail!(
56 Construction,
57 "a projection sampled at {stations} stations is a guess"
58 );
59 }
60 if !tolerance.is_finite() || tolerance <= 0.0 {
61 ogeom_bail!(Construction, "a tolerance of {tolerance} is not a distance");
62 }
63
64 let mut seats: Vec<Seat> = Vec::new();
69 let deflection = ogeom_mesh::Deflection::default();
70 for face in explore_unique(model, target, ShapeType::Face)? {
71 let Some(NodeData::Face(data)) = model.node(&face).map(|n| n.data().clone()) else {
72 continue;
73 };
74 let Some(surface) = model.geometry().surface(data.surface).cloned() else {
75 continue;
76 };
77 let placement = face.transform(model.datums())?;
78 if (placement.scale_factor().abs() - 1.0).abs() > 1e-9 {
79 ogeom_bail!(
80 Construction,
81 "a scaled placement changes a surface's parameterization out \
82 from under its pcurves; bake the scale before projecting"
83 );
84 }
85 let rings = ogeom_mesh::face_boundary(model, &face, deflection, tol)?;
86 seats.push(Seat {
87 face,
88 surface_id: data.surface,
89 surface: surface.transformed(&placement, tol)?,
90 rings,
91 });
92 }
93 if seats.is_empty() {
94 ogeom_bail!(Construction, "a shape with no faces catches nothing");
95 }
96
97 let mut out = Vec::new();
98 let mut history = History::new();
99 for edge in explore_unique(model, wire, ShapeType::Edge)? {
100 let (curve, range) = {
101 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
102 continue;
103 };
104 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
105 continue;
106 };
107 let Some(geometry) = model.geometry().curve(*curve).cloned() else {
108 continue;
109 };
110 (geometry, *range)
111 };
112 let mut run: Vec<(usize, Point, Point2)> = Vec::new();
116 let mut runs: Vec<(usize, Vec<(Point, Point2)>)> = Vec::new();
117 let mut flush = |run: &mut Vec<(usize, Point, Point2)>| {
118 if run.len() >= 4 {
119 let seat = run[0].0;
120 runs.push((seat, run.iter().map(|(_, p, uv)| (*p, *uv)).collect()));
121 }
122 run.clear();
123 };
124 for k in 0..=stations {
125 #[expect(
126 clippy::cast_precision_loss,
127 reason = "a station index, far below the mantissa"
128 )]
129 let t = (range.1 - range.0).mul_add(k as f64 / stations as f64, range.0);
130 let at = curve.point_at(t, tol)?;
131 let landed = nearest_seat(&seats, at, tol)?;
132 match landed {
133 Some((seat, point, uv)) => {
134 if run.first().is_some_and(|(held, _, _)| *held != seat) {
135 flush(&mut run);
136 }
137 run.push((seat, point, uv));
138 }
139 None => flush(&mut run),
140 }
141 }
142 flush(&mut run);
143
144 for (seat, samples) in runs {
145 let points: Vec<Point> = samples.iter().map(|(p, _)| *p).collect();
146 let mut chart: Vec<Point2> = samples.iter().map(|(_, uv)| *uv).collect();
147 unwrap(&mut chart, &seats[seat].surface);
152 let (fitted, on_face, _) =
155 ogeom_geom::fit::fit_points_joint(&points, &chart, &chart, 3, tolerance, tol)?;
156 let curve: Curve = fitted.curve.into();
157 let built = ogeom_algo::make_edge(model, curve, (0.0, 1.0), tol)?.shape;
158 let pcurve: PlanarCurve = on_face.into();
159 ogeom_algo::attach_pcurve(
160 model,
161 &built,
162 pcurve,
163 seats[seat].surface_id,
164 ogeom_topo::Location::identity(),
165 (0.0, 1.0),
166 )?;
167 history.generate(&edge, built.clone());
168 out.push(Projected {
169 edge: built,
170 face: seats[seat].face.clone(),
171 tolerance: fitted.error,
172 });
173 }
174 }
175
176 let edges: Vec<Shape> = out.iter().map(|p| p.edge.clone()).collect();
177 let result = model.add_compound(&edges)?;
178 history.modify(wire, result.clone());
179 Ok((out, Built::new(result, history)))
180}
181
182struct Seat {
184 face: Shape,
185 surface_id: SurfaceId,
186 surface: SurfaceGeometry,
188 rings: Vec<Vec<Point2>>,
190}
191
192fn nearest_seat(
195 seats: &[Seat],
196 at: Point,
197 tol: Tolerances,
198) -> OgeomResult<Option<(usize, Point, Point2)>> {
199 let mut best: Option<(usize, Point, Point2, f64)> = None;
200 for (i, seat) in seats.iter().enumerate() {
201 let projection = ogeom_algo::project_on_surface(&seat.surface, at, 24, tol)?;
202 let (u, v) = projection.parameters;
203 let uv = Point2::new(u, v);
204 if !inside_rings(&seat.rings, uv) {
205 continue;
206 }
207 let foot = seat.surface.point_at(u, v, tol)?;
208 let distance = foot.distance(at);
209 if best.as_ref().is_none_or(|(.., held)| distance < *held) {
210 best = Some((i, foot, uv, distance));
211 }
212 }
213 Ok(best.map(|(i, foot, uv, _)| (i, foot, uv)))
214}
215
216fn unwrap(chart: &mut [Point2], surface: &SurfaceGeometry) {
219 let ((u0, u1), (v0, v1)) = surface.domain();
220 let periods = [
221 if surface.is_periodic_u() {
222 u1 - u0
223 } else {
224 0.0
225 },
226 if surface.is_periodic_v() {
227 v1 - v0
228 } else {
229 0.0
230 },
231 ];
232 for k in 1..chart.len() {
233 let previous = chart[k - 1];
234 let mut here = chart[k];
235 for (axis, period) in periods.iter().enumerate() {
236 if *period <= 0.0 {
237 continue;
238 }
239 let (was, is) = if axis == 0 {
240 (previous.x, here.x)
241 } else {
242 (previous.y, here.y)
243 };
244 let shifted = (is - was) / period;
245 let turns = shifted.round();
246 if turns.abs() >= 1.0 {
247 if axis == 0 {
248 here.x = turns.mul_add(-period, is);
249 } else {
250 here.y = turns.mul_add(-period, is);
251 }
252 }
253 }
254 chart[k] = here;
255 }
256}
257
258fn inside_rings(rings: &[Vec<Point2>], p: Point2) -> bool {
261 let mut inside = false;
262 for ring in rings {
263 for i in 0..ring.len() {
264 let (a, b) = (ring[i], ring[(i + 1) % ring.len()]);
265 if (a.y > p.y) != (b.y > p.y) {
266 let x = (b.x - a.x).mul_add((p.y - a.y) / (b.y - a.y), a.x);
267 if x > p.x {
268 inside = !inside;
269 }
270 }
271 }
272 }
273 inside
274}