1use geop_core_editor::{
5 edit_form::{EditForm, FormState},
6 editable::{Context, OperationGroup, OperationInfo, OperationTier},
7 editing::{Number, Track, Unit},
8 part_operation,
9 part_operation::PartOperation,
10};
11use geop_core_geometry::nurb_surface::NurbSurface3D;
12use geop_core_math::{
13 geop_error::{GeopError, GeopResult, WithContext},
14 primitives::CoordinateSystem,
15 scalars::Scalar,
16 vector::{Vector2, Vector3, Vector4},
17 with_context,
18};
19use geop_core_program::{numeric::NumericInput, part::StepContext, reader::Reader};
20use geop_core_sketch::{Shape, Sketch, profile::curve_polylines};
21use geop_core_topology::{Body, build::BuiltBody};
22use geop_lib_shell::{rebuilt, room};
23use geop_ops_model::sketch::{PlacedSketch, SketchRef, shape_profiles};
24use geop_ops_model::{
25 BodyNames, Extent, ModelOperation, NamedModel, Namer, build_model, reach_past, read_model,
26};
27use serde::{Deserialize, Serialize};
28
29use geop_lib_sweep::{SweepLoop, extrude};
30
31use crate::sweeping::{Extents, Output, Sweeping, build_extents};
32
33#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
63pub struct Extrude;
64
65#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
66pub struct ExtrudeArgs {
67 pub sketch: SketchRef,
69 pub extent: Extents,
72 #[serde(default)]
74 pub output: Output,
75 #[serde(default, skip_serializing_if = "is_zero")]
79 pub draft: f64,
80}
81
82fn is_zero(angle: &f64) -> bool {
83 *angle == 0.0
84}
85
86fn distance_handle(
90 reader: &Reader,
91 args: &ExtrudeArgs,
92 length: f64,
93 second: bool,
94) -> Option<Track> {
95 let placed = reader.retrieve_element(&args.sketch).ok()?;
96 let plane = &placed.plane;
97 let center = plane.uv_to_xyz(&sketch_center(&placed.sketch)?);
98 let scale = match (args.extent.symmetric, second) {
101 (true, _) => 0.5,
102 (false, false) => 1.0,
103 (false, true) => -1.0,
104 };
105 let normal = plane.w();
106 Some(Track {
107 at: center.add(&normal.prod_scalar(Scalar::from_f64(length * scale))),
108 direction: normal.prod_scalar(Scalar::from_f64(scale)),
109 })
110}
111
112part_operation!(Extrude(ExtrudeArgs));
113
114impl PartOperation for Extrude {
115 const KIND: &'static str = "extrude";
116 const INFO: OperationInfo = OperationInfo::new(
117 "Extrude",
118 "Sweep a sketch's area along its plane's normal into a solid, or its curves into faces.",
119 OperationGroup::Solid,
120 OperationTier::Big,
121 )
122 .shortcut("E")
123 .icon(crate::icons::EXTRUDE);
124 type Args = ExtrudeArgs;
125 type Session = ();
126
127 fn new_args(context: &Context) -> ExtrudeArgs {
130 let before = &context.before;
131 let newest = before.targets_of::<PlacedSketch>().pop();
132 ExtrudeArgs {
133 sketch: SketchRef::whole(newest.unwrap_or_else(|| geop_ops_model::sketch::sketch(""))),
134 extent: Extents::blind(10.0),
135 output: Output::new_for(&read_model(before).unwrap_or_default()),
136 draft: 0.0,
137 }
138 }
139
140 fn run(ctx: &mut StepContext, args: &ExtrudeArgs) -> GeopResult<()> {
141 let _step = ctx.step();
142 build_model::<Self>(ctx, args)
143 }
144
145 fn form<'a>(
151 context: &'a Context<'a>,
152 args: &ExtrudeArgs,
153 _: &FormState<()>,
154 ) -> EditForm<'a, ExtrudeArgs> {
155 let before = &context.before;
156 let mut f = EditForm::<ExtrudeArgs>::default();
157 f.pick_one(
158 "sketch",
159 "sketch",
160 before,
161 Some(&args.sketch),
162 |edit, sketch| edit.args.sketch = sketch,
163 );
164 let amount = |length: &NumericInput, second| {
165 let label = if second { "distance 2" } else { "distance" };
166 let number = Number::numeric(length, label, before, Unit::Length);
167 let at = distance_handle(before, args, number.value, second);
168 number.range(-10.0, 10.0).handle(at)
169 };
170 args.extent.fields(
171 &mut f,
172 ("distance", 1.0),
173 amount,
174 |args| &mut args.extent,
175 |edit, length| {
176 let to = length.plain();
177 let from = std::mem::replace(&mut edit.args.extent.side1, Extent::Blind(length));
178 if let (Extent::Blind(NumericInput::Plain(from)), Some(to)) = (from, to)
179 && let Output::Solid(combine) = &mut edit.args.output
180 {
181 combine.follow_sign(from, to);
182 }
183 },
184 );
185 let draft = Number::new("draft", args.draft, Unit::Angle).range(-45.0, 45.0);
186 f.number("draft", draft, |edit, v| edit.args.draft = v);
187 let reaches = args.extent.reaches_target();
188 let output = |args: &mut ExtrudeArgs, output| args.output = output;
189 (args.output).fields(&mut f, before, reaches, output);
190 f
191 }
192}
193
194impl ModelOperation for Extrude {
195 fn build(
196 model: &mut NamedModel,
197 reader: &Reader,
198 step: &str,
199 args: &ExtrudeArgs,
200 ) -> GeopResult<()> {
201 let namer = Namer::new("extrude", step)?;
202 let placed = reader.retrieve_element(&args.sketch).concerning("sketch")?;
203 let sketch = &placed.sketch;
204 let geometry = sketch.enclose()?;
205 let shape = args.output.shape(sketch)?;
206 args.extent.sweeps(&shape)?;
207 let slope = match args.draft {
208 0.0 => None,
209 angle => Some(draft_slope(angle, &args.output, &shape).concerning("draft")?),
210 };
211 let loops: Vec<_> = (shape_profiles(&placed.name, sketch, &geometry, shape)?.into_iter())
212 .map(SweepLoop::plain)
213 .collect();
214 let extruding = Extruding {
215 plane: &placed.plane,
216 loops: &loops,
217 slope,
218 };
219 let how = (&args.extent, &args.output);
220 build_extents(model, reader, &namer, step, how, &extruding)
221 }
222}
223
224fn draft_slope(angle: f64, output: &Output, shape: &Shape) -> GeopResult<Scalar> {
228 if matches!(shape, Shape::Chain(_)) {
229 return Err(GeopError::new(
230 "a draft narrows an area: an open chain of curves encloses none",
231 ));
232 }
233 if matches!(output, Output::Faces { .. }) {
234 return Err(GeopError::new(
235 "a draft tapers a solid's walls between its caps: extrude faces without one",
236 ));
237 }
238 if angle.is_nan() || angle.abs() >= 90.0 {
239 return Err(GeopError::new(format!(
240 "a draft of {angle} degrees: walls drafted by less than 90 degrees only"
241 )));
242 }
243 let radians = Scalar::from_f64(angle).degrees_to_radians()?;
244 radians.sin().div(radians.cos())
245}
246
247struct Extruding<'a> {
250 plane: &'a CoordinateSystem,
251 loops: &'a [SweepLoop],
252 slope: Option<Scalar>,
255}
256
257impl Sweeping for Extruding<'_> {
258 fn loops(&self) -> &[SweepLoop] {
259 self.loops
260 }
261
262 fn sweep(
263 &self,
264 model: &mut NamedModel,
265 namer: &Namer,
266 name: Option<&str>,
267 from: f64,
268 to: f64,
269 ) -> GeopResult<BuiltBody> {
270 let s = Scalar::from_f64;
271 let heights = match self.slope {
274 Some(_) if from * to < 0.0 => vec![s(from), Scalar::ZERO, s(to)],
275 _ => vec![s(from), s(to)],
276 };
277 let built = extrude(model, namer, name, self.plane, &heights, self.loops)?;
278 match self.slope {
279 Some(slope) => drafted(model, namer, &built, self.plane, slope),
280 None => Ok(built),
281 }
282 }
283
284 fn past(&self, hull: &[Vector3], sign: f64) -> GeopResult<f64> {
285 reach_past(hull, self.plane, sign)
286 }
287
288 fn ends(&self, _: f64) -> GeopResult<(String, String)> {
289 Ok(("start".into(), "end".into()))
290 }
291
292 fn all_the_way(&self, far: f64) -> f64 {
293 far
294 }
295
296 fn full_turn(&self) -> Option<f64> {
297 None
298 }
299
300 fn measure(&self, sign: f64, p: &Vector3) -> Option<f64> {
301 let along = (p.sub(self.plane.origin()))
302 .prod_dot(self.plane.w())
303 .mul(Scalar::from_f64(sign));
304 along
305 .definitely_greater(Scalar::ZERO)
306 .then(|| along.to_f64())
307 }
308}
309
310fn drafted(
325 model: &mut NamedModel,
326 namer: &Namer,
327 built: &BuiltBody,
328 plane: &CoordinateSystem,
329 slope: Scalar,
330) -> GeopResult<BuiltBody> {
331 let ctx = with_context!("drafted({}, slope={slope:?})", namer.root());
332 let solid = built
333 .solid
334 .ok_or_else(|| GeopError::new("only a solid is drafted"))
335 .with_context(ctx)?;
336 let (mut spec, sources) = model
337 .topology()
338 .body_spec(&built.faces, true)
339 .with_context(ctx)?;
340 let names = BodyNames {
341 solid: Some(model.name(solid)?.to_string()),
342 ..model.source_names(&sources)?
343 };
344 let caps = [namer.name(&["start"]), namer.name(&["end"])];
345 let walls: Vec<bool> = names.faces.iter().map(|f| !caps.contains(f)).collect();
346 for ((face, name), &wall) in spec.faces.iter_mut().zip(&names.faces).zip(&walls) {
347 if wall {
348 face.surface = drafted_wall(&face.surface, plane, slope).map_err(|e| {
349 GeopError::new(format!(
350 "the draft cannot lean face {name} in that far: {}",
351 e.root_message()
352 ))
353 })?;
354 }
355 }
356 let reseat = vec![false; spec.vertices.len()];
357 let spec = rebuilt(&spec, &names, &walls, &reseat).with_context(ctx)?;
358 model
359 .delete_bodies(&[Body::Solid(solid)])
360 .with_context(ctx)?;
361 model.build_body(spec, names).with_context(ctx)
362}
363
364fn drafted_wall(
376 wall: &NurbSurface3D,
377 plane: &CoordinateSystem,
378 slope: Scalar,
379) -> GeopResult<NurbSurface3D> {
380 let height = |point: &Vector4| -> GeopResult<Scalar> {
381 Ok(point
382 .dehomogenized()?
383 .sub(plane.origin())
384 .prod_dot(plane.w()))
385 };
386 let mut side = None;
387 for point in &wall.control_points {
388 side = side.or(height(point)?.definite_sign());
389 }
390 let slope = match side {
391 Some(true) => slope,
392 Some(false) => slope.neg(),
393 None => return Err(GeopError::new("a wall of no height")),
394 };
395 let wall = room(wall.clone())?;
396 let mut drafted = wall.clone();
397 for (k, point) in drafted.control_points.iter_mut().enumerate() {
398 let height = height(point)?;
399 if height.is_zero() {
400 continue;
401 }
402 let weight = point[3];
406 let offset = wall
407 .offset(height.mul(slope).neg())?
408 .ok_or_else(|| GeopError::new("the draft makes a wall collapse at this height"))?;
409 if (offset.degree_u, offset.degree_v, offset.num_u, offset.num_v)
413 != (wall.degree_u, wall.degree_v, wall.num_u, wall.num_v)
414 {
415 return Err(GeopError::new(
416 "a wall whose offset is not on its own net, a free-form one, is not drafted",
417 ));
418 }
419 let moved = offset.control_points[k];
420 let at = moved.dehomogenized()?.prod_scalar(weight);
421 *point = Vector4::from_array([at[0], at[1], at[2], weight]);
422 }
423 drafted.recompute_aabb();
424 Ok(drafted)
425}
426
427fn sketch_center(sketch: &Sketch) -> Option<Vector2> {
430 let mut drawn: Vec<Vector2> = sketch
431 .curves
432 .iter()
433 .filter(|(_, c)| !c.construction)
434 .flat_map(|(&id, _)| curve_polylines(sketch, id).unwrap_or_default())
435 .flatten()
436 .collect();
437 if drawn.is_empty() {
438 drawn = sketch.positions().into_values().collect();
439 }
440 let hull = drawn.into_iter().reduce(|a, b| a.union(&b))?;
441 Some(hull.map(Scalar::midpoint))
443}