1use super::*;
2
3pub(super) fn translated_curve(curve: &NurbsCurve, delta: Vec3) -> Result<NurbsCurve, KernelRefusal> {
4 NurbsCurve::new(
5 curve.degree,
6 curve.knots.clone(),
7 curve
8 .control_points
9 .iter()
10 .map(|point| Vec4 {
11 x: point.x + point.w * delta.x,
12 y: point.y + point.w * delta.y,
13 z: point.z + point.w * delta.z,
14 w: point.w,
15 })
16 .collect(),
17 )
18 .or_refuse(KernelStage::Fragment, "curve_new")
19}
20
21pub(crate) fn profile_area(
22 curves: &[NurbsCurve],
23 origin: Vec3,
24 x_axis: Vec3,
25 y_axis: Vec3,
26) -> Result<f64, String> {
27 let mut area = 0.0;
28 for curve in curves {
29 let [start, end] = curve.domain()?;
30 let mut previous = curve.evaluate(start)?;
31 for index in 1..=64 {
32 let point = curve.evaluate(start + (end - start) * index as f64 / 64.0)?;
33 let a = previous.sub(origin);
34 let b = point.sub(origin);
35 area += 0.5 * (a.dot(x_axis) * b.dot(y_axis) - b.dot(x_axis) * a.dot(y_axis));
36 previous = point;
37 }
38 }
39 Ok(area)
40}
41
42pub fn extrude_profile_brep(
43 input_curves: &[NurbsCurve],
44 direction: Vec3,
45 distance: f64,
46) -> Result<BrepSolid, KernelRefusal> {
47 let mut profile = crate::loft_topology::ConstructionProfile::new("extrude");
48 if input_curves.len() < 2 {
49 return Err(KernelRefusal::input(
50 KernelStage::Collect,
51 "profile_count",
52 "profile needs at least 2 curves",
53 ));
54 }
55 if distance.abs() <= 1e-12 {
56 return Err(KernelRefusal::input(
57 KernelStage::Collect,
58 "distance",
59 "extrudeSolid: distance must be non-zero",
60 ));
61 }
62 let displacement = direction
63 .normalized()
64 .or_input(KernelStage::Collect, "direction")?
65 .scale(distance);
66 let tolerance = 1e-6;
67 let mut curves: Vec<NurbsCurve> = input_curves
68 .iter()
69 .map(|curve| {
70 NurbsCurve::new(
71 curve.degree,
72 curve.knots.clone(),
73 curve.control_points.clone(),
74 )
75 })
76 .collect::<Result<_, _>>().or_refuse(KernelStage::Fragment, "collect")?;
77
78 let mut samples = Vec::new();
79 for (index, curve) in curves.iter().enumerate() {
80 let [start, end] = curve.domain().or_refuse(KernelStage::Fragment, "domain")?;
81 let next = &curves[(index + 1) % curves.len()];
82 let next_start = next.domain().or_refuse(KernelStage::Fragment, "domain")?[0];
83 if curve
84 .evaluate(end).or_refuse(KernelStage::Fragment, "evaluate")?
85 .sub(next.evaluate(next_start).or_refuse(KernelStage::Fragment, "evaluate")?)
86 .length()
87 > tolerance
88 {
89 return Err(KernelRefusal::input(
90 KernelStage::Collect,
91 "profile_closed",
92 format!("profile not closed at curve {index}"),
93 ));
94 }
95 for sample in 0..16 {
96 samples.push(curve.evaluate(start + (end - start) * sample as f64 / 16.0).or_refuse(KernelStage::Fragment, "evaluate")?);
97 }
98 }
99 let mut normal = crate::polygon::newell_normal(&samples);
100 normal = normal.normalized().or_input(KernelStage::Collect, "profile_area")?;
102 if normal.dot(displacement) < 0.0 {
103 normal = normal.scale(-1.0);
104 }
105 if normal.dot(displacement.normalized().or_refuse(KernelStage::Fragment, "normalized")?).abs() < 0.1 {
106 return Err(KernelRefusal::input(
107 KernelStage::Classify,
108 "sliver",
109 "extrudeSolid: sweep direction is nearly parallel to profile plane",
110 ));
111 }
112 let origin = samples[0];
113 if samples
114 .iter()
115 .any(|point| point.sub(origin).dot(normal).abs() > tolerance * 100.0)
116 {
117 return Err(KernelRefusal::input(
118 KernelStage::Collect,
119 "profile_planar",
120 "extrudeSolid: profile is not planar",
121 ));
122 }
123 let x_axis = normal.perpendicular().or_refuse(KernelStage::Fragment, "perpendicular")?;
124 let y_axis = normal.cross(x_axis).normalized().or_refuse(KernelStage::Fragment, "normalized")?;
125 let reversed_winding = profile_area(&curves, origin, x_axis, y_axis).or_refuse(KernelStage::Fragment, "profile_area")? < 0.0;
126 if reversed_winding {
127 curves = curves
128 .iter()
129 .rev()
130 .map(NurbsCurve::reversed)
131 .collect::<Result<_, _>>().or_refuse(KernelStage::Fragment, "collect")?;
132 }
133
134 profile.lap("profile");
135 let count = curves.len();
136 let points: Vec<Vec3> = curves
137 .iter()
138 .map(|curve| curve.domain().and_then(|domain| curve.evaluate(domain[0])))
139 .collect::<Result<_, _>>().or_refuse(KernelStage::Fragment, "collect")?;
140 let mut vertices = Vec::with_capacity(count * 2);
141 for (index, point) in points.iter().enumerate() {
142 vertices.push(VertexRecord {
143 id: index as u64 + 1,
144 point: *point,
145 });
146 }
147 for (index, point) in points.iter().enumerate() {
148 vertices.push(VertexRecord {
149 id: (count + index) as u64 + 1,
150 point: point.add(displacement),
151 });
152 }
153
154 let mut edges = Vec::with_capacity(count * 3);
155 for (index, curve) in curves.iter().enumerate() {
156 let [start, end] = curve.domain().or_refuse(KernelStage::Fragment, "domain")?;
157 edges.push(EdgeRecord {
158 id: 10 + index as u64,
159 curve: curve.clone(),
160 t0: start,
161 t1: end,
162 start_vertex_id: index as u64 + 1,
163 end_vertex_id: ((index + 1) % count) as u64 + 1,
164 degenerate: false,
165 name: None,
166 });
167 edges.push(EdgeRecord {
168 id: 10 + count as u64 + index as u64,
169 curve: translated_curve(curve, displacement)?,
170 t0: start,
171 t1: end,
172 start_vertex_id: (count + index) as u64 + 1,
173 end_vertex_id: (count + (index + 1) % count) as u64 + 1,
174 degenerate: false,
175 name: None,
176 });
177 edges.push(EdgeRecord {
178 id: 10 + 2 * count as u64 + index as u64,
179 curve: make_line(points[index], points[index].add(displacement)).or_refuse(KernelStage::Fragment, "make_line")?,
180 t0: 0.0,
181 t1: 1.0,
182 start_vertex_id: index as u64 + 1,
183 end_vertex_id: (count + index) as u64 + 1,
184 degenerate: false,
185 name: None,
186 });
187 }
188
189 profile.lap("edges");
190 let mut next_id = 1000_u64;
191 let mut faces = Vec::with_capacity(count + 2);
192 for (index, curve) in curves.iter().enumerate() {
193 let [start, end] = curve.domain().or_refuse(KernelStage::Fragment, "domain")?;
194 let coedges = vec![
195 CoedgeRecord {
196 id: next_id,
197 edge_id: 10 + index as u64,
198 forward: true,
199 pcurve: parameter_line(start, 0.0, end, 0.0).or_refuse(KernelStage::Fragment, "parameter_line")?,
200 },
201 CoedgeRecord {
202 id: next_id + 1,
203 edge_id: 10 + 2 * count as u64 + ((index + 1) % count) as u64,
204 forward: true,
205 pcurve: parameter_line(end, 0.0, end, 1.0).or_refuse(KernelStage::Fragment, "parameter_line")?,
206 },
207 CoedgeRecord {
208 id: next_id + 2,
209 edge_id: 10 + count as u64 + index as u64,
210 forward: false,
211 pcurve: parameter_line(end, 1.0, start, 1.0).or_refuse(KernelStage::Fragment, "parameter_line")?,
212 },
213 CoedgeRecord {
214 id: next_id + 3,
215 edge_id: 10 + 2 * count as u64 + index as u64,
216 forward: false,
217 pcurve: parameter_line(start, 1.0, start, 0.0).or_refuse(KernelStage::Fragment, "parameter_line")?,
218 },
219 ];
220 next_id += 4;
221 faces.push(FaceRecord {
222 id: next_id + 1,
223 surface: make_extrusion(curve, displacement).or_refuse(KernelStage::Fragment, "make_extrusion")?,
224 same_sense: true,
225 loops: vec![LoopRecord {
226 id: next_id,
227 coedges,
228 }],
229 name: None,
230 });
231 next_id += 2;
232 }
233 if reversed_winding {
234 faces.reverse();
240 }
241
242 let mut min_x = f64::INFINITY;
243 let mut min_y = f64::INFINITY;
244 let mut max_x = f64::NEG_INFINITY;
245 let mut max_y = f64::NEG_INFINITY;
246 for point in &samples {
247 let delta = point.sub(origin);
248 min_x = min_x.min(delta.dot(x_axis));
249 max_x = max_x.max(delta.dot(x_axis));
250 min_y = min_y.min(delta.dot(y_axis));
251 max_y = max_y.max(delta.dot(y_axis));
252 }
253 let padding = (max_x - min_x).max(max_y - min_y) * 0.05 + 1e-6;
254 let width = max_x - min_x + 2.0 * padding;
255 let height = max_y - min_y + 2.0 * padding;
256 let bottom_origin = origin
257 .add(x_axis.scale(min_x - padding))
258 .add(y_axis.scale(min_y - padding));
259 let mut bottom_coedges = Vec::with_capacity(count);
260 for index in (0..count).rev() {
261 bottom_coedges.push(CoedgeRecord {
262 id: next_id,
263 edge_id: 10 + index as u64,
264 forward: false,
265 pcurve: curve_to_plane_parameters(&curves[index], bottom_origin, x_axis, y_axis).or_refuse(KernelStage::Fragment, "plane_parameters")?
266 .reversed().or_refuse(KernelStage::Fragment, "reversed")?,
267 });
268 next_id += 1;
269 }
270 faces.push(FaceRecord {
271 id: next_id + 1,
272 surface: make_plane(bottom_origin, x_axis, y_axis, width, height).or_refuse(KernelStage::Fragment, "make_plane")?,
273 same_sense: false,
274 loops: vec![LoopRecord {
275 id: next_id,
276 coedges: bottom_coedges,
277 }],
278 name: None,
279 });
280 next_id += 2;
281
282 let top_origin = bottom_origin.add(displacement);
283 let mut top_coedges = Vec::with_capacity(count);
284 for (index, curve) in curves.iter().enumerate() {
285 let translated = translated_curve(curve, displacement)?;
286 top_coedges.push(CoedgeRecord {
287 id: next_id,
288 edge_id: 10 + count as u64 + index as u64,
289 forward: true,
290 pcurve: curve_to_plane_parameters(&translated, top_origin, x_axis, y_axis).or_refuse(KernelStage::Fragment, "plane_parameters")?,
291 });
292 next_id += 1;
293 }
294 faces.push(FaceRecord {
295 id: next_id + 1,
296 surface: make_plane(top_origin, x_axis, y_axis, width, height).or_refuse(KernelStage::Fragment, "make_plane")?,
297 same_sense: true,
298 loops: vec![LoopRecord {
299 id: next_id,
300 coedges: top_coedges,
301 }],
302 name: None,
303 });
304 next_id += 2;
305
306 let solid = BrepSolid {
307 mass_properties_cache: Default::default(),
308 cells: Default::default(),
309 id: next_id + 1,
310 vertices,
311 edges,
312 shells: vec![ShellRecord { id: next_id, faces }],
313 genus: 0,
314 kind: crate::BodyKind::Solid,
315 };
316 profile.lap("topology_surface_pcurves");
317 let issues = solid.validate();
318 profile.lap("validation");
319 if issues.is_empty() {
320 Ok(solid)
321 } else {
322 Err(KernelRefusal::internal(
323 KernelStage::Validate,
324 "validate",
325 format!("Rust extrusion builder produced invalid topology: {issues:?}"),
326 ))
327 }
328}