1use crate::topology::FaceRecord;
30use crate::{
31 boolean_operation, boolean_operation_nary, extrude_profile_brep, make_arc, make_line, make_sphere_brep_framed, project_point_to_surface,
32 solid_from_polygons, thicken_trimmed_sheet, BooleanOperation, BooleanOptions, BrepSolid, KernelRefusal, KernelStage, NurbsCurve, OffsetCorners,
33 OrRefuse, Vec3,
34};
35
36pub const THICKEN_FACES_REFUSAL: &str = "thickenFaces";
38
39fn input(what: &'static str, message: String) -> KernelRefusal {
40 KernelRefusal::input(KernelStage::Collect, what, format!("{THICKEN_FACES_REFUSAL}: {message}"))
41}
42
43fn refuse(what: &'static str, message: String) -> KernelRefusal {
44 KernelRefusal::unsupported(KernelStage::Classify, what, format!("{THICKEN_FACES_REFUSAL}: {message}"))
45}
46
47pub fn extrude_curves_to_sheet(curves: &[NurbsCurve], direction: Vec3, distance: f64) -> Result<Vec<BrepSolid>, KernelRefusal> {
55 if curves.is_empty() {
56 return Err(input("curves", "at least one curve is required".into()));
57 }
58 if !distance.is_finite() || distance.abs() <= 1e-12 {
59 return Err(input("distance", "distance must be a nonzero finite value".into()));
60 }
61 let slide = direction.normalized().map_err(|_| input("direction", "direction must be non-zero".into()))?.scale(distance);
62 curves
63 .iter()
64 .map(|curve| {
65 let net = curve
66 .control_points
67 .iter()
68 .map(|pole| vec![*pole, crate::Vec4 { x: pole.x + slide.x * pole.w, y: pole.y + slide.y * pole.w, z: pole.z + slide.z * pole.w, w: pole.w }])
69 .collect();
70 let surface = crate::NurbsSurface::new(curve.degree, 1, curve.knots.clone(), vec![0.0, 0.0, 1.0, 1.0], net).or_refuse(KernelStage::Collect, "thickenFaces.swept_face")?;
71 crate::face_from_surface(&surface)
72 })
73 .collect()
74}
75
76struct Side {
78 face: usize,
79 start: Vec3,
80 middle: Vec3,
81 end: Vec3,
82 normals: [Vec3; 3],
84}
85
86fn face_normal(face: &FaceRecord, point: Vec3) -> Result<Vec3, KernelRefusal> {
87 let foot = project_point_to_surface(&face.surface, point).or_refuse(KernelStage::Classify, "thickenFaces.project")?;
88 let normal = face.surface.normal(foot.u, foot.v).or_refuse(KernelStage::Classify, "thickenFaces.normal")?;
89 Ok(if face.same_sense { normal } else { normal.scale(-1.0) })
90}
91
92pub fn thicken_sheet_faces(faces: &[BrepSolid], thickness: f64, corners: OffsetCorners) -> Result<BrepSolid, KernelRefusal> {
104 if !thickness.is_finite() || thickness.abs() <= 1e-12 {
105 return Err(input("thickness", "thickness must be a nonzero finite value".into()));
106 }
107 let (t, sign) = (thickness.abs(), thickness.signum());
108 let mut records: Vec<(&BrepSolid, &FaceRecord)> = Vec::new();
109 for body in faces {
110 for face in body.shells.iter().flat_map(|shell| &shell.faces) {
111 records.push((body, face));
112 }
113 }
114 if records.is_empty() {
115 return Err(input("faces", "at least one face is required".into()));
116 }
117 let size = records
118 .iter()
119 .flat_map(|(body, _)| &body.vertices)
120 .map(|vertex| vertex.point.sub(records[0].0.vertices[0].point).length())
121 .fold(t, f64::max);
122 let tolerance = 1e-7 * size;
123 let near = |a: Vec3, b: Vec3| a.sub(b).length() <= tolerance;
124
125 let mut sides = Vec::new();
127 for (index, (body, face)) in records.iter().enumerate() {
128 for coedge in face.loops.iter().flat_map(|record| &record.coedges) {
129 let edge = body.edges.iter().find(|edge| edge.id == coedge.edge_id).ok_or_else(|| input("faces", "a face refers to a missing edge".into()))?;
130 if edge.degenerate {
131 continue;
132 }
133 let at = |fraction: f64| edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction).or_refuse(KernelStage::Classify, "thickenFaces.edge");
134 let (mut start, middle, mut end) = (at(0.0)?, at(0.5)?, at(1.0)?);
135 if !coedge.forward {
136 std::mem::swap(&mut start, &mut end);
137 }
138 let normals = [face_normal(face, start)?, face_normal(face, middle)?, face_normal(face, end)?];
139 sides.push(Side { face: index, start, middle, end, normals });
140 }
141 }
142
143 let mut parts = Vec::new();
145 for (_, face) in &records {
146 let loops: Vec<Vec<NurbsCurve>> = face.loops.iter().map(|record| record.coedges.iter().map(|coedge| coedge.pcurve.clone()).collect()).collect();
147 let along = if face.same_sense { thickness } else { -thickness };
148 match past_its_axis(face, along, tolerance)? {
149 Some(wedges) => parts.extend(wedges),
150 None => parts.extend(thicken_trimmed_sheet(&face.surface, &loops, along, false)?),
151 }
152 }
153
154 struct Open {
156 faces: [usize; 2],
157 ends: [Vec3; 2],
158 moved: [Vec3; 2],
159 }
160 let mut open = Vec::new();
161 let mut cuts: Vec<BrepSolid> = Vec::new();
162 for (i, a) in sides.iter().enumerate() {
163 for b in sides.iter().skip(i + 1) {
164 if a.face == b.face || !near(a.middle, b.middle) {
165 continue;
166 }
167 if !(near(a.start, b.end) && near(a.end, b.start)) {
168 if near(a.start, b.start) && near(a.end, b.end) {
169 return Err(input("normals", format!("faces {} and {} have their normals on opposite sides of the sheet", a.face, b.face)));
170 }
171 continue;
172 }
173 let run = a.end.sub(a.start);
174 let tangent = run.normalized().map_err(|_| refuse("crease", "a crease has no length".into()))?;
175 let inward = a.normals[1].cross(tangent);
177 let lean = b.normals[1].scale(sign).dot(inward);
178 let moved = [a.normals[1].scale(sign), b.normals[1].scale(sign)];
179 let straight = a.middle.sub(a.start).cross(tangent).length() <= tolerance;
180 let steady = |side: &Side| side.normals.iter().all(|normal| normal.sub(side.normals[1]).length() <= 1e-9);
181 if lean.abs() <= 1e-9 {
182 continue;
184 }
185 if lean > 0.0 {
186 let cosine = moved[0].dot(moved[1]);
190 if cosine >= -1e-9 {
191 continue;
192 }
193 if !straight || !steady(a) || !steady(b) {
194 return Err(refuse("crease_closing", format!("the crease between faces {} and {} closes toward the thickened side tighter than a right angle and is not a straight line with constant normals", a.face, b.face)));
195 }
196 let inward_b = b.normals[1].cross(tangent.scale(-1.0));
197 for (own, other, into) in [(moved[0], moved[1], inward), (moved[1], moved[0], inward_b)] {
198 let top = a.start.add(own.scale(t));
199 let reach = top.add(into.scale(t * -cosine / into.dot(other)));
200 let line = |from: Vec3, to: Vec3| make_line(from, to).or_refuse(KernelStage::Collect, "thickenFaces.cut_line");
201 cuts.push(extrude_profile_brep(&[line(a.start, top)?, line(top, reach)?, line(reach, a.start)?], run, run.length())?);
202 }
203 continue;
204 }
205 if !straight || !steady(a) || !steady(b) {
207 return Err(refuse("crease_curved", format!("the crease between faces {} and {} opens toward the thickened side and is not a straight line with constant normals", a.face, b.face)));
208 }
209 parts.push(crease_fill(a.start, run, moved, t, corners)?);
210 open.push(Open { faces: [a.face, b.face], ends: [a.start, a.end], moved });
211 }
212 }
213
214 let mut points: Vec<Vec3> = Vec::new();
216 for crease in &open {
217 for end in crease.ends {
218 if !points.iter().any(|point| near(*point, end)) {
219 points.push(end);
220 }
221 }
222 }
223 for point in points {
224 let here: Vec<&Open> = open.iter().filter(|crease| crease.ends.iter().any(|end| near(*end, point))).collect();
225 if here.len() == 1 {
226 continue;
227 }
228 let mut around: Vec<(usize, Vec3)> = Vec::new();
230 for crease in &here {
231 for (face, moved) in crease.faces.iter().zip(crease.moved) {
232 if !around.iter().any(|(known, _)| known == face) {
233 around.push((*face, moved));
234 }
235 }
236 }
237 if here.len() != 3 || around.len() != 3 {
238 return Err(refuse("corner", format!("{} open creases between {} faces meet in one corner; exactly three between three faces are built", here.len(), around.len())));
239 }
240 parts.push(corner_fill(point, [around[0].1, around[1].1, around[2].1], t, corners)?);
241 }
242
243 let united = if parts.len() == 1 { parts.remove(0) } else { boolean_operation_nary(&parts, BooleanOperation::Union)? };
244 if cuts.is_empty() {
245 return Ok(united);
246 }
247 let mut operands = vec![united];
248 operands.extend(cuts);
249 boolean_operation_nary(&operands, BooleanOperation::Subtract)
250}
251
252fn past_its_axis(face: &FaceRecord, along: f64, tolerance: f64) -> Result<Option<Vec<BrepSolid>>, KernelRefusal> {
260 let surface = &face.surface;
261 let stage = KernelStage::Classify;
262 if surface.degree_v != 1 || surface.control_points.first().map(Vec::len) != Some(2) || face.loops.len() != 1 || face.loops[0].coedges.len() != 4 {
263 return Ok(None);
264 }
265 let [u0, u1] = surface.domain_u().or_refuse(stage, "thickenFaces.domain")?;
266 let [v0, v1] = surface.domain_v().or_refuse(stage, "thickenFaces.domain")?;
267 let corners = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
269 for coedge in &face.loops[0].coedges {
270 let [t0, t1] = coedge.pcurve.domain().or_refuse(stage, "thickenFaces.pcurve")?;
271 for t in [t0, t1] {
272 let at = coedge.pcurve.evaluate(t).or_refuse(stage, "thickenFaces.pcurve")?;
273 if !corners.iter().any(|corner| (at.x - corner[0]).abs() + (at.y - corner[1]).abs() <= 1e-9 * ((u1 - u0).abs() + (v1 - v0).abs())) {
274 return Ok(None);
275 }
276 }
277 }
278 let point = |u: f64, v: f64| surface.evaluate(u, v).or_refuse(stage, "thickenFaces.point");
279 let normal = |u: f64, v: f64| surface.normal(u, v).or_refuse(stage, "thickenFaces.normal");
280 let slide = point(u0, v1)?.sub(point(u0, v0)?);
281 let toward = along.signum();
283 let (p0, n0, p1, n1) = (point(u0, v0)?, normal(u0, v0)?.scale(toward), point(u1, v0)?, normal(u1, v0)?.scale(toward));
284 let chord = p1.sub(p0);
285 let spread = n0.sub(n1);
286 if spread.length() <= 1e-9 {
287 return Ok(None);
288 }
289 let radius = chord.dot(spread) / spread.dot(spread);
291 if !(radius > 0.0) || along.abs() < radius - tolerance {
292 return Ok(None);
293 }
294 let centre = p0.add(n0.scale(radius));
295 const SAMPLES: usize = 16;
296 for i in 0..=SAMPLES {
297 let u = u0 + (u1 - u0) * i as f64 / SAMPLES as f64;
298 for (v, shift) in [(v0, Vec3::new(0.0, 0.0, 0.0)), (v1, slide)] {
299 let focus = point(u, v)?.add(normal(u, v)?.scale(toward * radius));
300 if focus.sub(centre.add(shift)).length() > tolerance {
301 return Ok(None);
302 }
303 }
304 }
305 if slide.cross(n0).length() < (1.0 - 1e-9) * slide.length() {
306 return Ok(None);
307 }
308 let arc = surface.iso_curve_v(v0).or_refuse(stage, "thickenFaces.arc")?;
310 let wedge = |factor: f64| -> Result<BrepSolid, KernelRefusal> {
311 let scaled = |q: Vec3| centre.add(q.sub(centre).scale(factor));
312 let poles = arc
313 .control_points
314 .iter()
315 .map(|pole| crate::Vec4::from_point(scaled(Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w)), pole.w))
316 .collect();
317 let rim = NurbsCurve::new(arc.degree, arc.knots.clone(), poles).or_refuse(stage, "thickenFaces.wedge_arc")?;
318 let section = vec![
319 make_line(centre, scaled(p0)).or_refuse(stage, "thickenFaces.wedge_side")?,
320 rim,
321 make_line(scaled(p1), centre).or_refuse(stage, "thickenFaces.wedge_side")?,
322 ];
323 extrude_profile_brep(§ion, slide, slide.length())
324 };
325 let mut wedges = vec![wedge(1.0)?];
326 let over = along.abs() - radius;
327 if over > tolerance {
328 wedges.push(wedge(-over / radius)?);
329 }
330 Ok(Some(wedges))
331}
332
333fn crease_fill(start: Vec3, run: Vec3, moved: [Vec3; 2], t: f64, corners: OffsetCorners) -> Result<BrepSolid, KernelRefusal> {
336 let collect = KernelStage::Collect;
337 let (a, b) = (start.add(moved[0].scale(t)), start.add(moved[1].scale(t)));
338 let line = |from: Vec3, to: Vec3| make_line(from, to).or_refuse(collect, "thickenFaces.fill_line");
339 let section = match corners {
340 OffsetCorners::Sharp => {
341 let apex = start.add(moved[0].add(moved[1]).scale(t / (1.0 + moved[0].dot(moved[1]))));
343 vec![line(start, a)?, line(a, apex)?, line(apex, b)?, line(b, start)?]
344 }
345 OffsetCorners::Rounded => {
346 let across = moved[1].sub(moved[0].scale(moved[1].dot(moved[0]))).normalized().or_refuse(collect, "thickenFaces.fill_frame")?;
347 let angle = moved[0].dot(moved[1]).clamp(-1.0, 1.0).acos();
348 vec![line(start, a)?, make_arc(start, moved[0], across, t, 0.0, angle).or_refuse(collect, "thickenFaces.fill_arc")?, line(b, start)?]
349 }
350 };
351 extrude_profile_brep(§ion, run, run.length())
352}
353
354fn corner_fill(corner: Vec3, moved: [Vec3; 3], t: f64, corners: OffsetCorners) -> Result<BrepSolid, KernelRefusal> {
357 let at = |direction: Vec3, length: f64| corner.add(direction.scale(length));
358 match corners {
359 OffsetCorners::Sharp => {
360 let [a, b, c] = moved;
362 let volume = a.dot(b.cross(c));
363 if volume.abs() <= 1e-9 {
364 return Err(refuse("corner", "the three faces at a corner do not span space".into()));
365 }
366 let far = b.cross(c).add(c.cross(a)).add(a.cross(b)).scale(1.0 / volume);
367 let pair = |i: usize, j: usize| at(moved[i].add(moved[j]), t / (1.0 + moved[i].dot(moved[j])));
368 let tip = |i: usize| at(moved[i], t);
369 let far = at(far, t);
370 let mut faces = Vec::new();
371 for (i, j) in [(0, 1), (1, 2), (2, 0)] {
372 faces.push(vec![corner, tip(i), pair(i, j), tip(j)]);
373 }
374 for (i, j, k) in [(0, 1, 2), (1, 2, 0), (2, 0, 1)] {
375 faces.push(vec![tip(i), pair(i, j), far, pair(i, k)]);
376 }
377 solid_from_polygons(&faces)
378 }
379 OffsetCorners::Rounded => {
380 let middle = moved[0].add(moved[1]).add(moved[2]).normalized().or_refuse(KernelStage::Collect, "thickenFaces.corner_frame")?;
383 let pole = middle.cross(moved[0]).normalized().or_refuse(KernelStage::Collect, "thickenFaces.corner_frame")?;
384 let ball = make_sphere_brep_framed(corner, t, pole, Some(middle.scale(-1.0))).or_refuse(KernelStage::Collect, "thickenFaces.corner_ball")?;
385 let reach = 4.0 * t;
386 let tips = [at(moved[0], reach), at(moved[1], reach), at(moved[2], reach)];
387 let base = tips[1].sub(tips[0]).cross(tips[2].sub(tips[0]));
388 if (tips[0].sub(corner).dot(base) / base.length()).abs() <= t {
389 return Err(refuse("corner", "the three faces at a corner are too nearly flat for a ball between them".into()));
390 }
391 let wedge = solid_from_polygons(&[
392 vec![corner, tips[0], tips[1]],
393 vec![corner, tips[1], tips[2]],
394 vec![corner, tips[2], tips[0]],
395 vec![tips[0], tips[1], tips[2]],
396 ])?;
397 boolean_operation(&ball, &wedge, BooleanOperation::Intersect, &BooleanOptions::default())
398 }
399 }
400}
401