geop_ops_parts/operation/
revolve.rs1use geop_core_math::{
4 geop_error::{GeopError, GeopResult, WithContext},
5 primitives::CoordinateSystem,
6 scalars::Scalar,
7 vector::Vector2,
8 with_context,
9};
10use geop_core_part::{Namer, Part};
11use geop_core_sketch::{CurveId, CurveKind, Positions, ProfileLoop};
12use geop_ops_extrude_revolve::revolve::revolve_at_oriented;
13use geop_ops_parts_derive::OperationArgs;
14use serde::{Deserialize, Serialize};
15
16use super::{Combine, Operation, extrude::sketch_profile};
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
39pub struct Revolve;
40
41#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, OperationArgs)]
42pub struct RevolveArgs {
43 #[arg(Sketch)]
45 pub sketch: String,
46 #[arg(SketchLine { sketch: "sketch" })]
49 pub axis: CurveId,
50 #[serde(default)]
52 #[arg(Combine { sign: None })]
53 pub combine: Combine,
54}
55
56impl<S: Scalar> Operation<S> for Revolve {
57 type Args = RevolveArgs;
58
59 fn apply(
60 &self,
61 mut part: Part<S>,
62 operation_id: &str,
63 args: &RevolveArgs,
64 ) -> GeopResult<Part<S>> {
65 let ctx = with_context!("revolve({operation_id}, {args:?})");
66 let namer = Namer::new("revolve", operation_id)?;
67 let placed = part
68 .sketch(part.sketch_id(&args.sketch).with_context(ctx)?)?
69 .clone();
70 let sketch = &placed.sketch;
71 let CurveKind::Line { start, end } = sketch.curve(args.axis).with_context(ctx)?.kind else {
72 return Err(GeopError::new(format!(
73 "revolve axis {} is not a line",
74 args.axis
75 )))
76 .with_context(ctx);
77 };
78 let positions = sketch.positions();
79 let a = positions[&start];
80 let b = positions[&end];
81 let len = (b[0] - a[0]).hypot(b[1] - a[1]);
82 let dir = [(b[0] - a[0]) / len, (b[1] - a[1]) / len];
83 let left = [-dir[1], dir[0]];
84 let on_axis = sketch.on_line(args.axis)?;
85
86 let regions = sketch.regions().with_context(ctx)?;
87 let mut sides = Vec::new();
88 let mut solid = None;
89 for (index, region) in regions.iter().enumerate() {
90 let ctx = with_context!("revolving sketch region {index}");
91 if !region.holes.is_empty() {
92 return Err(GeopError::new(
93 "revolving a region with holes is not supported yet",
94 ))
95 .with_context(ctx);
96 }
97 let outline = region.outer.polyline(sketch, &positions);
103 let side = |p: &[f64; 2]| (p[0] - a[0]) * left[0] + (p[1] - a[1]) * left[1];
104 let (lo, hi) = outline
105 .iter()
106 .map(side)
107 .fold((0.0f64, 0.0f64), |(lo, hi), s| (lo.min(s), hi.max(s)));
108 let scale = hi - lo;
109 if lo < -1e-9 * scale && hi > 1e-9 * scale {
110 return Err(GeopError::new("the profile crosses the revolve axis"))
111 .with_context(ctx);
112 }
113 let sign = if hi > -lo { 1.0 } else { -1.0 };
114 sides.push(sign);
115 if sides.iter().any(|&s| s != sign) {
116 return Err(GeopError::new(
117 "the sketch has regions on both sides of the revolve axis, which would overlap \
118 once revolved; revolve them in separate operations",
119 ))
120 .with_context(ctx);
121 }
122
123 let r_dir = [sign * left[0], sign * left[1]];
128 let z_dir = [-r_dir[1], r_dir[0]];
129 let rz: Positions = positions
130 .iter()
131 .map(|(&p, xy)| {
132 let d = [xy[0] - a[0], xy[1] - a[1]];
133 let r = if on_axis.contains(&p) {
134 0.0
135 } else {
136 d[0] * r_dir[0] + d[1] * r_dir[1]
137 };
138 (p, [r, d[0] * z_dir[0] + d[1] * z_dir[1]])
139 })
140 .collect();
141
142 let edges = ®ion.outer.edges;
147 let flags: Vec<bool> = edges
148 .iter()
149 .map(|e| match sketch.curves[&e.curve].kind {
150 CurveKind::Line { start, end } => {
151 on_axis.contains(&start) && on_axis.contains(&end)
152 }
153 _ => false,
154 })
155 .collect();
156 let n = edges.len();
157 let starts_off_axis = |k: usize| !flags[k] && flags[(k + n - 1) % n];
158 if (0..n).filter(|&k| starts_off_axis(k)).count() != 1 {
159 return Err(GeopError::new(
160 "the profile must touch the revolve axis along exactly one run of edges \
161 (constrain its edge onto the axis line)",
162 ))
163 .with_context(ctx);
164 }
165 let first_off = (0..n).find(|&k| starts_off_axis(k)).unwrap();
166 let chain = ProfileLoop {
167 edges: (0..n)
168 .map(|k| (first_off + k) % n)
169 .take_while(|&k| !flags[k])
170 .map(|k| edges[k])
171 .collect(),
172 }
173 .reversed();
174 let profile = sketch_profile(
175 &args.sketch,
176 chain.to_nurbs(sketch, &rz).with_context(ctx)?,
177 false,
178 );
179
180 let plane = &placed.plane;
181 let dir3 = |d: [f64; 2]| {
182 plane
183 .u()
184 .prod_scalar(S::from_f64(d[0]))
185 .add(&plane.v().prod_scalar(S::from_f64(d[1])))
186 };
187 let (u, w) = (dir3(r_dir), dir3(z_dir));
188 let origin =
189 plane.uv_to_xyz(&Vector2::from_array([S::from_f64(a[0]), S::from_f64(a[1])]));
190 let cs = CoordinateSystem::try_new(origin, u, w.prod_cross(&u), w)?;
191 let solid_name = match solid {
194 None => args.combine.built_name(&namer),
195 Some(_) => namer.name(&["solid", &profile.curve_names[0]]),
196 };
197 let built = revolve_at_oriented(&mut part, &namer, &solid_name, &profile, &cs)
198 .with_context(ctx)?;
199 match solid {
200 None => solid = Some(built),
201 Some(first) => part.merge_solids(first, built)?,
202 }
203 }
204 if let Some(built) = solid {
205 args.combine
206 .apply(&mut part, &namer, operation_id, built)
207 .with_context(ctx)?;
208 }
209 Ok(part)
210 }
211}