1use super::*;
2
3pub(super) struct RimPiece {
5 pub(super) face_id: u64,
6 pub(super) loop_index: usize,
7 pub(super) window: [f64; 2],
9 pub(super) edge_id: u64,
10}
11
12pub fn blend_smooth_chain(
18 solid: &BrepSolid,
19 seed_edge_id: u64,
20 radius: f64,
21 chamfer: bool,
22 name: Option<&str>,
23) -> Result<BrepSolid, String> {
24 if !(radius > 0.0) || !radius.is_finite() {
25 return Err("blend: radius must be positive".into());
26 }
27 let chain = collect_smooth_chain(solid, seed_edge_id)?;
28 if chain.segments.len() < 2 {
29 return Err("blend: chain collapsed to a single segment".into());
30 }
31 if chain.closed {
32 blend_closed_smooth_chain(solid, &chain.segments, radius, chamfer, name)
33 } else {
34 blend_open_smooth_chain(solid, &chain, radius, chamfer, name)
35 }
36}
37
38pub fn blend_smooth_chain_if_closed(
45 solid: &BrepSolid,
46 seed_edge_id: u64,
47 radius: f64,
48 chamfer: bool,
49 name: Option<&str>,
50) -> Result<BrepSolid, String> {
51 if !(radius > 0.0) || !radius.is_finite() {
52 return Err("blend: radius must be positive".into());
53 }
54 let chain = collect_smooth_chain(solid, seed_edge_id)?;
55 if chain.segments.len() < 2 {
56 return Err("blend: chain collapsed to a single segment".into());
57 }
58 if !chain.closed {
59 return Err(
60 "blend: the edge is one arc of an OPEN tangent chain; the unselected \
61 continuation is capped, not blended"
62 .into(),
63 );
64 }
65 blend_closed_smooth_chain(solid, &chain.segments, radius, chamfer, name)
66}
67
68fn blend_closed_smooth_chain(
71 solid: &BrepSolid,
72 segments: &[ChainSegment<'_>],
73 radius: f64,
74 chamfer: bool,
75 name: Option<&str>,
76) -> Result<BrepSolid, String> {
77 let samples = march_chain(segments, radius, false)?;
78
79 let mut global: Vec<(f64, &ChainSample)> = samples
81 .iter()
82 .filter(|sample| (0..CHAIN_PER_SEGMENT as isize).contains(&sample.position))
83 .map(|sample| (sample.parameter.rem_euclid(1.0), sample))
84 .collect();
85 global.sort_by(|a, b| a.0.total_cmp(&b.0));
86 let count = global.len();
87 let degree = FIT_DEGREE;
88 let mut extended_params = Vec::new();
89 let mut samples_cr = Vec::new();
90 let mut samples_mid = Vec::new();
91 let mut samples_cs = Vec::new();
92 let mut push = |station: &Station, parameter: f64| {
93 extended_params.push(parameter);
94 samples_cr.push(Vec4::from_point(station.p1, 1.0));
95 samples_cs.push(Vec4::from_point(station.p2, 1.0));
96 samples_mid.push(Vec4 {
97 x: station.apex.x * station.weight,
98 y: station.apex.y * station.weight,
99 z: station.apex.z * station.weight,
100 w: station.weight,
101 });
102 };
103 for offset in (1..=degree).rev() {
104 let (parameter, sample) = &global[count - offset];
105 push(&sample.station, parameter - 1.0);
106 }
107 for (parameter, sample) in &global {
108 push(&sample.station, *parameter);
109 }
110 push(&global[0].1.station, global[0].0 + 1.0);
113 for offset in 1..=degree {
114 let (parameter, sample) = &global[offset];
115 push(&sample.station, parameter + 1.0);
116 }
117 let low = extended_params[0];
118 let high = *extended_params.last().unwrap();
119 let range = high - low;
120 let normalized: Vec<f64> = extended_params
121 .iter()
122 .map(|parameter| (parameter - low) / range)
123 .collect();
124 let seam_low = (0.0 - low) / range;
125 let seam_high = (1.0 - low) / range;
126 let fit_row = |row: &[Vec4]| -> Result<NurbsCurve, String> {
127 let curve = fit::interpolate_homogeneous(row, degree, &normalized)?;
128 let (_, tail) = curve.split(seam_low)?;
129 let (middle, _) = tail.split(seam_high)?;
130 Ok(middle)
131 };
132 let cr = fit_row(&samples_cr)?;
133 let cs = fit_row(&samples_cs)?;
134 let mid = if chamfer {
135 None
136 } else {
137 Some(fit_row(&samples_mid)?)
138 };
139 let u_domain = cr.domain()?;
140 let surface = crate::blend::rows::surface_from_rows(degree, &cr, &cs, mid.as_ref(), true)?;
141
142 let single_face = |side: usize| -> bool {
146 let first_id = segments[0].mate(side).face.id;
147 segments
148 .iter()
149 .all(|segment| segment.mate(side).face.id == first_id)
150 };
151 let mut whole_pcurves: [Option<NurbsCurve>; 2] = [None, None];
152 for side in 0..2 {
153 if !single_face(side) {
154 continue;
155 }
156 let mut row = Vec::new();
157 let select = |station: &Station| -> [f64; 2] {
158 if side == 0 {
159 station.uv1
160 } else {
161 station.uv2
162 }
163 };
164 let mut push_uv = |station: &Station| {
165 let uv = select(station);
166 row.push(Vec4::from_point(Vec3::new(uv[0], uv[1], 0.0), 1.0));
167 };
168 for offset in (1..=degree).rev() {
169 push_uv(&global[count - offset].1.station);
170 }
171 for (_, sample) in &global {
172 push_uv(&sample.station);
173 }
174 push_uv(&global[0].1.station);
175 for offset in 1..=degree {
176 push_uv(&global[offset].1.station);
177 }
178 whole_pcurves[side] = Some(fit_row(&row)?);
179 }
180
181 let mut pcurves1 = Vec::with_capacity(segments.len());
184 let mut pcurves2 = Vec::with_capacity(segments.len());
185 for segment in 0..segments.len() {
186 let mut window: Vec<&ChainSample> = samples
187 .iter()
188 .filter(|sample| sample.segment == segment)
189 .collect();
190 window.sort_by(|a, b| a.parameter.total_cmp(&b.parameter));
191 let params: Vec<f64> = window.iter().map(|sample| sample.parameter).collect();
192 let low = params[0];
193 let high = *params.last().unwrap();
194 let normalized: Vec<f64> = params
195 .iter()
196 .map(|parameter| (parameter - low) / (high - low))
197 .collect();
198 let fit_uv = |select: &dyn Fn(&Station) -> [f64; 2]| -> Result<NurbsCurve, String> {
199 let points: Vec<Vec4> = window
200 .iter()
201 .map(|sample| {
202 let uv = select(&sample.station);
203 Vec4::from_point(Vec3::new(uv[0], uv[1], 0.0), 1.0)
204 })
205 .collect();
206 let curve = fit::interpolate_homogeneous(&points, degree, &normalized)?;
207 Ok(curve)
211 };
212 pcurves1.push((low, high, fit_uv(&|station| station.uv1)?));
213 pcurves2.push((low, high, fit_uv(&|station| station.uv2)?));
214 }
215
216 chain_surgery(
217 solid,
218 segments,
219 ChainRows {
220 surface,
221 cr,
222 cs,
223 u_domain,
224 fit_low: low,
225 fit_range: range,
226 pcurves1,
227 pcurves2,
228 whole_pcurves,
229 },
230 name,
231 )
232}
233
234pub(super) struct ChainRows {
235 pub(super) surface: NurbsSurface,
236 pub(super) cr: NurbsCurve,
237 pub(super) cs: NurbsCurve,
238 pub(super) u_domain: [f64; 2],
239 pub(super) fit_low: f64,
243 pub(super) fit_range: f64,
244 pub(super) pcurves1: Vec<(f64, f64, NurbsCurve)>,
247 pub(super) pcurves2: Vec<(f64, f64, NurbsCurve)>,
248 pub(super) whole_pcurves: [Option<NurbsCurve>; 2],
250}
251
252impl ChainRows {
253 pub(super) fn to_global(&self, fit_parameter: f64) -> f64 {
254 self.fit_low + fit_parameter * self.fit_range
255 }
256}
257
258pub(super) fn pcurve_portion(fitted: &(f64, f64, NurbsCurve), a: f64, b: f64) -> Result<NurbsCurve, String> {
262 let (low, high, curve) = fitted;
263 let to_local = |value: f64| {
266 let mut best = value;
267 for candidate in [value, value + 1.0, value - 1.0] {
268 if candidate >= low - 1e-9 && candidate <= high + 1e-9 {
269 best = candidate;
270 break;
271 }
272 }
273 ((best - low) / (high - low)).clamp(0.0, 1.0)
274 };
275 let mut local_a = to_local(a);
276 let mut local_b = to_local(b);
277 if local_a > local_b {
278 std::mem::swap(&mut local_a, &mut local_b);
279 }
280 let epsilon = 1e-9;
281 let (_, tail) = if local_a > epsilon {
282 curve.split(local_a)?
283 } else {
284 (curve.clone(), curve.clone())
285 };
286 let tail = if local_a > epsilon {
287 tail
288 } else {
289 curve.clone()
290 };
291 let portion = if local_b < 1.0 - epsilon {
292 tail.split(local_b)?.0
293 } else {
294 tail
295 };
296 Ok(portion)
297}
298
299pub(super) fn cross_edge_at(
305 solid: &BrepSolid,
306 face_before: &FaceRecord,
307 loop_before: usize,
308 face_after: &FaceRecord,
309 loop_after: usize,
310 vertex: u64,
311 before_edge: u64,
312 after_edge: u64,
313) -> Result<Option<u64>, String> {
314 let mut found: Option<u64> = None;
315 let mut consider = |face: &FaceRecord, loop_index: usize| -> Result<(), String> {
316 for coedge in &face.loops[loop_index].coedges {
317 if coedge.edge_id == before_edge || coedge.edge_id == after_edge {
318 continue;
319 }
320 let edge = solid
321 .edges
322 .iter()
323 .find(|edge| edge.id == coedge.edge_id)
324 .ok_or("blend: loop references missing edge")?;
325 if edge.start_vertex_id == vertex || edge.end_vertex_id == vertex {
326 if found.is_some() && found != Some(edge.id) {
327 return Err("blend: multiple cross edges at a chain vertex".into());
328 }
329 found = Some(edge.id);
330 }
331 }
332 Ok(())
333 };
334 consider(face_before, loop_before)?;
335 if !(face_after.id == face_before.id && loop_after == loop_before) {
336 consider(face_after, loop_after)?;
337 }
338 Ok(found)
339}
340
341fn nearest_seam_image(value: f64, low: f64, high: f64) -> f64 {
344 let period = high - low;
345 low + ((value - low) / period).round() * period
346}
347
348pub(super) fn project_piece_pcurve(
366 piece: &NurbsCurve,
367 surface: &NurbsSurface,
368 start_is_crossing: bool,
369 end_is_crossing: bool,
370) -> Result<NurbsCurve, String> {
371 let [u0, u1] = surface.domain_u()?;
372 let [v0, v1] = surface.domain_v()?;
373 let (_, closed_v) = surface.closed_directions()?;
374 let period = u1 - u0;
375 let period_v = v1 - v0;
376 let [d0, d1] = piece.domain()?;
377 const DENSE: usize = 96;
380 let mut proj_u = Vec::with_capacity(DENSE + 1);
381 let mut proj_v = Vec::with_capacity(DENSE + 1);
382 let mut point3 = Vec::with_capacity(DENSE + 1);
383 let mut params = Vec::with_capacity(DENSE + 1);
384 let mut previous_u: Option<f64> = None;
385 for section in 0..=DENSE {
386 let t = d0 + (d1 - d0) * section as f64 / DENSE as f64;
387 let point = piece.evaluate(t)?;
388 let projection = crate::project_point_to_surface(surface, point)?;
389 let mut u = projection.u;
390 if let Some(previous) = previous_u {
391 while u - previous > period * 0.5 {
392 u -= period;
393 }
394 while previous - u > period * 0.5 {
395 u += period;
396 }
397 }
398 previous_u = Some(u);
399 proj_u.push(u);
400 proj_v.push(projection.v);
401 point3.push(point);
402 params.push(section as f64 / DENSE as f64);
403 }
404 if closed_v {
411 for index in 2..=DENSE {
412 while proj_v[index] - proj_v[index - 1] > period_v * 0.5 {
413 proj_v[index] -= period_v;
414 }
415 while proj_v[index - 1] - proj_v[index] > period_v * 0.5 {
416 proj_v[index] += period_v;
417 }
418 }
419 while proj_v[0] - proj_v[1] > period_v * 0.5 {
420 proj_v[0] -= period_v;
421 }
422 while proj_v[1] - proj_v[0] > period_v * 0.5 {
423 proj_v[0] += period_v;
424 }
425 }
426 let mean_u: f64 = proj_u[1..DENSE].iter().sum::<f64>() / (DENSE - 1) as f64;
429 let seam_u = if mean_u - u0 > u1 - mean_u { u1 } else { u0 };
430 let pinned = |index: usize,
439 neighbour: usize,
440 proj_u: &[f64],
441 proj_v: &[f64]|
442 -> Result<(f64, f64), String> {
443 let point = point3[index];
444 let u_error = surface
445 .evaluate(seam_u, proj_v[index])?
446 .sub(point)
447 .length();
448 if closed_v {
449 let seam_v = nearest_seam_image(proj_v[neighbour], v0, v1);
450 let v_error = surface
451 .evaluate(proj_u[index], seam_v)?
452 .sub(point)
453 .length();
454 if v_error < u_error {
455 return Ok((proj_u[index], seam_v));
456 }
457 }
458 Ok((seam_u, proj_v[index]))
459 };
460 if start_is_crossing {
461 let (u, v) = pinned(0, 1, &proj_u, &proj_v)?;
462 proj_u[0] = u;
463 proj_v[0] = v;
464 }
465 if end_is_crossing {
466 let (u, v) = pinned(DENSE, DENSE - 1, &proj_u, &proj_v)?;
467 proj_u[DENSE] = u;
468 proj_v[DENSE] = v;
469 }
470 let mut shift = 0.0;
471 while mean_u + shift > u1 + 1e-9 {
472 shift -= period;
473 }
474 while mean_u + shift < u0 - 1e-9 {
475 shift += period;
476 }
477 for u in proj_u.iter_mut() {
478 *u += shift;
479 }
480 if closed_v {
481 let mean_v: f64 = proj_v[1..DENSE].iter().sum::<f64>() / (DENSE - 1) as f64;
482 let mut shift_v = 0.0;
483 while mean_v + shift_v > v1 + 1e-9 {
484 shift_v -= period_v;
485 }
486 while mean_v + shift_v < v0 - 1e-9 {
487 shift_v += period_v;
488 }
489 for v in proj_v.iter_mut() {
490 *v += shift_v;
491 }
492 }
493 let tolerance = 0.002;
496 let mut best: Option<NurbsCurve> = None;
497 for sections in [8usize, 12, 16, 24, 32, 48, 64, 96] {
498 if sections > DENSE {
499 break;
500 }
501 let indices: Vec<usize> = (0..=sections)
502 .map(|k| (k * DENSE / sections).min(DENSE))
503 .collect();
504 let points: Vec<Vec4> = indices
505 .iter()
506 .map(|&i| Vec4::from_point(Vec3::new(proj_u[i], proj_v[i], 0.0), 1.0))
507 .collect();
508 let knot_params: Vec<f64> = indices.iter().map(|&i| params[i]).collect();
509 let curve = fit::interpolate_homogeneous(&points, FIT_DEGREE, &knot_params)?;
510 let mut max_deviation: f64 = 0.0;
511 for i in 0..=DENSE {
512 let uv = curve.evaluate(params[i])?;
513 let on_surface = surface.evaluate(uv.x, uv.y)?;
514 max_deviation = max_deviation.max(on_surface.sub(point3[i]).length());
515 }
516 best = Some(curve);
517 if max_deviation <= tolerance {
518 break;
519 }
520 }
521 best.ok_or_else(|| "blend: piece pcurve projection failed".to_string())
522}