geop_ops_parts/operation/
extrude.rs1use geop_core_math::{
5 geop_error::{GeopError, GeopResult, WithContext},
6 primitives::CoordinateSystem,
7 scalars::Scalar,
8 vector::{Vector2, Vector3},
9 with_context,
10};
11use geop_core_part::{Namer, Part};
12use geop_core_sketch::{ProfilePiece, Sketch, profile::curve_polyline};
13use geop_ops_extrude_revolve::{
14 common::Profile,
15 extrude::{ExtrudeNames, extrude_from_plane},
16};
17use geop_ops_parts_derive::OperationArgs;
18use serde::{Deserialize, Serialize};
19
20use super::{Combine, Handle, HandleGroup, HandleMotion, Operation, arg_path};
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
47pub struct Extrude;
48
49#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, OperationArgs)]
50pub struct ExtrudeArgs {
51 #[arg(Sketch)]
53 pub sketch: String,
54 #[arg(Number { default: 1.0, min: -10.0, max: 10.0 })]
56 pub distance: f64,
57 #[serde(default)]
60 #[arg(Bool { default: false })]
61 pub symmetric: bool,
62 #[serde(default)]
64 #[arg(Combine { sign: Some("distance") })]
65 pub combine: Combine,
66}
67
68impl<S: Scalar> Operation<S> for Extrude {
69 type Args = ExtrudeArgs;
70
71 fn apply(
72 &self,
73 mut part: Part<S>,
74 operation_id: &str,
75 args: &ExtrudeArgs,
76 ) -> GeopResult<Part<S>> {
77 let ctx = with_context!("extrude({operation_id}, {args:?})");
78 let namer = Namer::new("extrude", operation_id)?;
79 let placed = part
80 .sketch(part.sketch_id(&args.sketch).with_context(ctx)?)?
81 .clone();
82 let sketch = &placed.sketch;
83 let distance = S::from_f64(args.distance);
84 let plane = if args.symmetric {
85 let half = distance.div(S::TWO)?;
86 let origin = placed
87 .plane
88 .origin()
89 .sub(&placed.plane.w().prod_scalar(half));
90 CoordinateSystem::try_new(
91 origin,
92 *placed.plane.u(),
93 *placed.plane.v(),
94 *placed.plane.w(),
95 )?
96 } else {
97 placed.plane.clone()
98 };
99
100 let positions = sketch.positions();
101 let regions = sketch.regions().with_context(ctx)?;
102 let mut solid = None;
103 for region in ®ions {
104 let outer_pieces = region.outer.to_nurbs::<S>(sketch, &positions)?;
105 let region_name = (regions.len() > 1).then(|| {
106 let lowest = outer_pieces.iter().map(|p| p.source).min();
107 format!("{},{}", args.sketch, lowest.expect("a loop has pieces"))
108 });
109 let outer = sketch_profile(&args.sketch, outer_pieces, true);
110 let holes = region
111 .holes
112 .iter()
113 .map(|h| {
114 Ok(sketch_profile(
115 &args.sketch,
116 h.to_nurbs(sketch, &positions)?,
117 true,
118 ))
119 })
120 .collect::<GeopResult<Vec<_>>>()?;
121 let names = ExtrudeNames {
122 namer: &namer,
123 region: region_name.as_deref(),
124 solid: match (&solid, ®ion_name) {
127 (Some(_), Some(region)) => namer.name(&["solid", region]),
128 _ => args.combine.built_name(&namer),
129 },
130 };
131 let built = extrude_from_plane(&mut part, &names, &plane, &outer, &holes, distance)
132 .with_context(ctx)
133 .with_context(with_context!(
134 "(a sketch's regions are extruded into one solid and must not share curves or points)"
135 ))?;
136 match solid {
137 None => solid = Some(built),
138 Some(first) => part.merge_solids(first, built)?,
139 }
140 }
141 let Some(built) = solid else {
142 return Err(GeopError::new("sketch has no region to extrude")).with_context(ctx);
143 };
144 args.combine
145 .apply(&mut part, &namer, operation_id, built)
146 .with_context(ctx)?;
147 Ok(part)
148 }
149
150 fn handles(&self, before: &Part<S>, args: &ExtrudeArgs) -> GeopResult<Vec<Handle>> {
153 let placed = before.sketch(before.sketch_id(&args.sketch)?)?;
154 let Some(center) = sketch_center(&placed.sketch) else {
155 return Ok(Vec::new());
156 };
157 let plane = &placed.plane;
158 let at = plane.uv_to_xyz(&Vector2::from_array(center.map(S::from_f64)));
159 let normal = to_f64(plane.w());
160 let scale = if args.symmetric { 0.5 } else { 1.0 };
162 let offset = args.distance * scale;
163 let at = to_f64(&at);
164 Ok(vec![Handle {
165 label: "distance".into(),
166 group: HandleGroup::Feature,
167 position: [0, 1, 2].map(|k| at[k] + normal[k] * offset),
168 motion: HandleMotion::Linear {
169 direction: normal,
170 arg: arg_path(&["distance"]),
171 value: args.distance,
172 scale,
173 },
174 }])
175 }
176}
177
178pub(crate) fn sketch_profile<S: Scalar>(
182 sketch: &str,
183 pieces: Vec<ProfilePiece<S>>,
184 closed: bool,
185) -> Profile<S> {
186 let curve_names = pieces
187 .iter()
188 .map(|p| format!("{sketch},{}", p.name()))
189 .collect();
190 let mut joint_names: Vec<String> = pieces
191 .iter()
192 .map(|p| format!("{sketch},{}", p.start))
193 .collect();
194 if !closed && let Some(last) = pieces.last() {
195 joint_names.push(format!("{sketch},{}", last.end));
196 }
197 Profile {
198 curves: pieces.into_iter().map(|p| p.curve).collect(),
199 curve_names,
200 joint_names,
201 }
202}
203
204fn sketch_center(sketch: &Sketch) -> Option<[f64; 2]> {
207 let positions = sketch.positions();
208 let mut drawn: Vec<[f64; 2]> = sketch
209 .curves
210 .iter()
211 .filter(|(_, c)| !c.construction)
212 .flat_map(|(&id, _)| curve_polyline(sketch, &positions, id))
213 .collect();
214 if drawn.is_empty() {
215 drawn = positions.into_values().collect();
216 }
217 let (lo, hi) = drawn.iter().fold(
218 ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]),
219 |(lo, hi), p| {
220 (
221 [lo[0].min(p[0]), lo[1].min(p[1])],
222 [hi[0].max(p[0]), hi[1].max(p[1])],
223 )
224 },
225 );
226 (!drawn.is_empty()).then(|| [(lo[0] + hi[0]) / 2.0, (lo[1] + hi[1]) / 2.0])
227}
228
229pub(crate) fn to_f64<S: Scalar>(v: &Vector3<S>) -> [f64; 3] {
231 [v[0].to_f64(), v[1].to_f64(), v[2].to_f64()]
232}