geop_ops_extrude_revolve/
loft.rs1use geop_core_editor::{
4 edit_form::{EditForm, FormState},
5 editable::{Context, OperationGroup, OperationInfo, OperationTier},
6 part_operation,
7 part_operation::PartOperation,
8};
9use geop_core_math::{
10 geop_error::{GeopError, GeopResult, WithContext},
11 with_context,
12};
13use geop_core_program::{element::ElementReference, part::StepContext, reader::Reader};
14use geop_core_sketch::{PointId, Shape};
15use geop_ops_model::sketch::{PlacedSketch, SketchRef, chain_of_pieces};
16use geop_ops_model::{ModelOperation, NamedModel, Namer, Point, build_model, read_model};
17use serde::{Deserialize, Serialize};
18
19use geop_core_math::vector::Vector3;
20use geop_lib_sweep::{Apex, Ends, Section, loft, mark};
21
22use crate::sweeping::{Output, PathRef};
23
24#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
67pub struct Loft;
68
69#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
70pub struct LoftArgs {
71 pub profiles: Vec<SketchRef>,
73 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub matches: Vec<ElementReference<PlacedSketch, Point>>,
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
82 pub guides: Vec<PathRef>,
83 #[serde(default)]
85 pub output: Output,
86}
87
88part_operation!(Loft(LoftArgs));
89
90impl PartOperation for Loft {
91 const KIND: &'static str = "loft";
92 const INFO: OperationInfo = OperationInfo::new(
93 "Loft",
94 "Build a solid through the areas of several sketches, or faces through their curves.",
95 OperationGroup::Solid,
96 OperationTier::Small,
97 )
98 .shortcut("L")
99 .icon(crate::icons::LOFT);
100 type Args = LoftArgs;
101 type Session = ();
102
103 fn new_args(context: &Context) -> LoftArgs {
106 let before = &context.before;
107 let sketches = before.targets_of::<PlacedSketch>();
108 LoftArgs {
109 profiles: (sketches
110 .get(sketches.len().saturating_sub(2)..)
111 .unwrap_or_default())
112 .iter()
113 .cloned()
114 .map(SketchRef::whole)
115 .collect(),
116 matches: Vec::new(),
117 guides: Vec::new(),
118 output: Output::new_for(&read_model(before).unwrap_or_default()),
119 }
120 }
121
122 fn run(ctx: &mut StepContext, args: &LoftArgs) -> GeopResult<()> {
123 let _step = ctx.step();
124 build_model::<Self>(ctx, args)
125 }
126 fn form<'a>(
129 context: &'a Context<'a>,
130 args: &LoftArgs,
131 _: &FormState<()>,
132 ) -> EditForm<'a, LoftArgs> {
133 let before = &context.before;
134 let mut f = EditForm::<LoftArgs>::default();
135 f.pick(
136 "profiles",
137 "profiles",
138 before,
139 &args.profiles,
140 true,
141 |edit, profiles| edit.args.profiles = profiles,
142 )
143 .pick(
144 "matches",
145 "matching points",
146 before,
147 &args.matches,
148 true,
149 |edit, matches| edit.args.matches = matches,
150 )
151 .pick(
152 "guides",
153 "guide curves",
154 before,
155 &args.guides,
156 true,
157 |edit, guides| edit.args.guides = guides,
158 );
159 f.form.dialog.optional("matches");
160 f.form.dialog.optional("guides");
161 let output = |args: &mut LoftArgs, output| args.output = output;
162 (args.output).fields(&mut f, before, false, output);
163 f
164 }
165}
166
167impl ModelOperation for Loft {
168 fn build(
169 model: &mut NamedModel,
170 reader: &Reader,
171 step: &str,
172 args: &LoftArgs,
173 ) -> GeopResult<()> {
174 let namer = Namer::new("loft", step)?;
175 let profiles = match args.profiles.as_slice() {
180 [first, .., again] if args.profiles.len() > 1 && first == again => {
181 &args.profiles[..args.profiles.len() - 1]
182 }
183 all => all,
184 };
185 let closed = profiles.len() < args.profiles.len();
186 let last = profiles.len().saturating_sub(1);
187 let mut apexes = [None, None];
188 let mut sections = Vec::new();
189 for (k, profile) in profiles.iter().enumerate() {
190 let placed = reader.retrieve_element(profile)?;
191 if let Some(point) = single_point(placed)? {
192 let end = match k {
193 0 => 0,
194 k if k == last => 1,
195 _ => {
196 return Err(GeopError::new(format!(
197 "loft: the sketch {:?} is a point, between profiles: a loft comes to a point only at its start or end",
198 placed.name
199 )));
200 }
201 };
202 apexes[end] = Some(Apex {
203 point,
204 name: placed.name.to_string(),
205 });
206 continue;
207 }
208 sections.push(section(
209 reader,
210 profile,
211 &args.output,
212 &matched_points(args, profile)?,
213 )?);
214 }
215 let ends = match (closed, &apexes) {
216 (false, _) => Ends::Open(apexes),
217 (true, [None, None]) => Ends::Closed,
218 (true, _) => {
219 return Err(GeopError::new(
220 "loft: a loft closing on itself has no end to come to a point at",
221 ));
222 }
223 };
224 let guides = (args.guides.iter())
225 .map(|guide| {
226 if args.profiles.iter().any(|profile| guide.is_sketch(profile)) {
227 return Err(GeopError::new(format!(
228 "loft: the guide {guide:?} is one of the profiles: a guide is a curve of its own"
229 )));
230 }
231 guide.resolve(reader)
232 })
233 .collect::<GeopResult<Vec<_>>>()?;
234 (args.output).build_whole(model, &namer, step, |model, name| {
235 loft(model, &namer, name, §ions, &ends, &guides)
236 })
237 }
238}
239
240fn matched_points(args: &LoftArgs, profile: &SketchRef) -> GeopResult<Vec<PointId>> {
243 let mut found = Vec::new();
244 for point in &args.matches {
245 let sketch = point.target();
246 if !args.profiles.iter().any(|p| p.target() == sketch) {
247 return Err(GeopError::new(format!(
248 "loft: the matching point {} is of the sketch {:?}, which is no profile",
249 point.name(),
250 sketch.name()
251 )));
252 }
253 if sketch == profile.target() {
254 found.push(point.name().parse()?);
255 }
256 }
257 Ok(found)
258}
259
260fn single_point(placed: &PlacedSketch) -> GeopResult<Option<Vector3>> {
263 let sketch = &placed.sketch;
264 if sketch.curves.values().any(|c| !c.construction) || sketch.points.len() != 1 {
265 return Ok(None);
266 }
267 let geometry = sketch.enclose()?;
268 Ok(geometry
269 .points
270 .values()
271 .next()
272 .map(|at| placed.plane.uv_to_xyz(at)))
273}
274
275fn section(
279 reader: &Reader,
280 profile: &SketchRef,
281 output: &Output,
282 matched: &[PointId],
283) -> GeopResult<Section> {
284 let ctx = with_context!("profile {:?}", profile.target().name());
285 let placed = reader.retrieve_element(profile).with_context(ctx)?;
286 let name = &placed.name;
287 let sketch = &placed.sketch;
288 let geometry = sketch.enclose().with_context(ctx)?;
289 let shape = output.shape(sketch).with_context(ctx)?;
290 let (lp, closed) = match shape {
291 Shape::Regions(mut regions) if regions.len() == 1 && regions[0].holes.is_empty() => {
292 (regions.remove(0).outer, true)
293 }
294 Shape::Regions(_) => {
295 return Err(GeopError::new(format!(
296 "loft: the profile {name:?} has holes or several areas, but a loft runs through single loops"
297 )));
298 }
299 Shape::Chain(chain) => (chain, false),
300 };
301 let mut profile = chain_of_pieces(
302 name,
303 lp.to_nurbs(sketch, &geometry).with_context(ctx)?,
304 closed,
305 );
306 let mut joints = Vec::new();
307 for &point in matched {
308 let at = geometry.points.get(&point).ok_or_else(|| {
309 GeopError::new(format!(
310 "loft: the matching point {point} is no point of the sketch"
311 ))
312 })?;
313 let (marked, joint) = mark(&profile, at, &format!("{name},{point}"))
314 .with_context(with_context!("the matching point {point}"))
315 .with_context(ctx)?;
316 profile = marked;
317 joints.push(joint);
318 }
319 Ok(Section {
320 plane: placed.plane.clone(),
321 profile,
322 name: name.to_string(),
323 matched: joints,
324 })
325}