Skip to main content

geop_ops_assembly/
add_part_derived.rs

1//! Parts derived from another: [`DerivedPart`] brings the bodies another
2//! program file builds into the part, and [`MirrorPart`] mirrors every body
3//! of the part in place — the other hand of it. A mirrored component of an
4//! assembly is a placed part derived so: its file's program with a
5//! [`MirrorPart`] step after it (see [`crate::AddPartArgs::mirror`]).
6
7use geop_core_editor::{
8    edit_form::{EditForm, FormState},
9    editable::{Context, OperationGroup, OperationInfo, OperationTier},
10    editing::Choice,
11    part_operation,
12    part_operation::PartOperation,
13};
14use geop_core_math::{
15    geop_error::{GeopError, GeopResult, WithContext},
16    primitives::{FrameAxis, Motion},
17    scalars::Scalar,
18    vector::Vector3,
19    with_context,
20};
21use geop_core_program::{
22    numeric::{ParameterValue, ParameterValues, parameter_values},
23    part::StepContext,
24    placed::{PlacedArgs, build_derived},
25    program::ProgramStep,
26    reader::Reader,
27    values::Settings,
28    workspace::is_program,
29};
30use geop_core_topology::Body;
31use geop_ops_model::{
32    ModelOperation, NamedModel, Namer, build_model, parameters::file_choices, read_model,
33};
34use serde::{Deserialize, Serialize};
35
36/// The plane of a part's own origin a part is mirrored in, by its normal:
37/// `x` for its `yz` plane.
38pub(crate) fn plane_name(normal: FrameAxis) -> &'static str {
39    match normal {
40        FrameAxis::X => "yz",
41        FrameAxis::Y => "zx",
42        FrameAxis::Z => "xy",
43    }
44}
45
46/// Every body of `model` — its solids, then the sheets — in the order they
47/// were made.
48fn bodies(model: &NamedModel) -> Vec<Body> {
49    let topology = model.topology();
50    let solids = topology.solids.keys().map(|&solid| Body::Solid(solid));
51    let sheets = (topology.shells.iter())
52        .filter(|(_, shell)| shell.solid.is_none())
53        .map(|(&shell, _)| Body::Sheet(shell));
54    solids.chain(sheets).collect()
55}
56
57/// Mirrors every body of the part in place, in a plane of its own origin:
58/// the other hand of it, every entity keeping its name — so a step or a
59/// mate naming one still finds it. Its datums stay as they are: a frame
60/// mirrored is no frame. The part says it is mirrored where it is listed
61/// (the parameter `mirrored`, the plane's name): a bill of materials tells
62/// it from the part as it was.
63#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
64pub struct MirrorPart;
65
66#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
67pub struct MirrorPartArgs {
68    /// The normal of the plane of the part's origin mirrored in: `x` for
69    /// its `yz` plane.
70    pub normal: FrameAxis,
71}
72
73/// The parameter a mirrored part says it is mirrored by.
74pub const MIRRORED: &str = "mirrored";
75
76part_operation!(MirrorPart(MirrorPartArgs));
77
78impl PartOperation for MirrorPart {
79    const KIND: &'static str = "mirror_part";
80    const INFO: OperationInfo = OperationInfo::new(
81        "Mirror part",
82        "Mirror the whole part in place in a plane of its origin: the other hand of it.",
83        OperationGroup::Bodies,
84        OperationTier::Menu,
85    )
86    .icon(crate::icons::MIRROR_PART);
87    type Args = MirrorPartArgs;
88    type Session = ();
89
90    /// In the origin's `yz` plane.
91    fn new_args(_: &Context) -> MirrorPartArgs {
92        MirrorPartArgs {
93            normal: FrameAxis::X,
94        }
95    }
96
97    /// The bodies mirrored, then the part said to be mirrored.
98    fn run(ctx: &mut StepContext, args: &MirrorPartArgs) -> GeopResult<()> {
99        build_model::<Self>(ctx, args)?;
100        ctx.produce(&parameter_values(), &args.normal, |reader, normal| {
101            let mut values: ParameterValues = reader.previous()?;
102            values.0.push((
103                MIRRORED.to_string(),
104                ParameterValue::Text(plane_name(*normal).to_string()),
105            ));
106            Ok(values)
107        })
108    }
109
110    /// The plane, of three.
111    fn form<'a>(
112        _: &'a Context<'a>,
113        args: &MirrorPartArgs,
114        _: &FormState<()>,
115    ) -> EditForm<'a, MirrorPartArgs> {
116        let mut f = EditForm::<MirrorPartArgs>::default();
117        f.select(
118            "normal",
119            "Mirrored in",
120            plane_name(args.normal),
121            plane_choices(),
122            false,
123            |edit, plane| {
124                if let Some(normal) = FrameAxis::ALL.into_iter().find(|n| plane_name(*n) == plane) {
125                    edit.args.normal = normal;
126                }
127            },
128        );
129        f
130    }
131}
132
133/// The planes of a part's origin, to choose one from.
134pub(crate) fn plane_choices() -> Vec<Choice> {
135    (FrameAxis::ALL.into_iter())
136        .map(|normal| {
137            let name = plane_name(normal);
138            Choice::new(name, format!("its {name} plane"))
139        })
140        .collect()
141}
142
143impl ModelOperation for MirrorPart {
144    fn build(
145        model: &mut NamedModel,
146        _: &Reader,
147        step: &str,
148        args: &MirrorPartArgs,
149    ) -> GeopResult<()> {
150        let ctx = with_context!("mirror_part({step}, {args:?})");
151        let mirror = mirror_in(args.normal).with_context(ctx)?;
152        for body in bodies(model) {
153            model.transform_body(body, &mirror).with_context(ctx)?;
154        }
155        Ok(())
156    }
157}
158
159/// The mirror in the plane of the origin normal to `normal`.
160fn mirror_in(normal: FrameAxis) -> GeopResult<Motion> {
161    let mut n = Vector3::zero();
162    n[normal as usize] = Scalar::ONE;
163    Motion::mirror(&Vector3::zero(), &n)
164}
165
166/// The step mirroring a part in the plane of its origin normal to
167/// `normal`, after the steps of its own program `ids` names: what a
168/// mirrored component is placed with (see [`crate::AddPartArgs::mirror`]).
169pub(crate) fn mirror_step(normal: FrameAxis) -> GeopResult<ProgramStep> {
170    ProgramStep::new::<MirrorPart>("mirrored", &MirrorPartArgs { normal })
171}
172
173/// Brings the bodies the program in another file builds into the part —
174/// its parameters set as `parameters` says, else as its own — each entity
175/// named `X` there named `derived(D,X)` here, for the step `D`: a part
176/// derived from another, which follows it as it changes. Mirrored after
177/// (see [`MirrorPart`]), it is the other hand of that part, a part of its
178/// own.
179#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
180pub struct DerivedPart;
181
182#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
183pub struct DerivedPartArgs {
184    /// The program file whose bodies are brought in, relative to this
185    /// program's own file; empty until one is chosen.
186    #[serde(default)]
187    pub file: String,
188    /// The values its parameters are given instead of its own, by name.
189    #[serde(default, skip_serializing_if = "Settings::is_empty")]
190    pub parameters: Settings,
191}
192
193part_operation!(DerivedPart(DerivedPartArgs));
194
195impl PartOperation for DerivedPart {
196    const KIND: &'static str = "derived_part";
197    const INFO: OperationInfo = OperationInfo::new(
198        "Derived part",
199        "Bring the bodies another program file builds into the part, following it as it changes.",
200        OperationGroup::Bodies,
201        OperationTier::Menu,
202    )
203    .icon(crate::icons::DERIVED_PART);
204    type Args = DerivedPartArgs;
205    type Session = ();
206
207    fn new_args(_: &Context) -> DerivedPartArgs {
208        DerivedPartArgs::default()
209    }
210
211    fn run(ctx: &mut StepContext, args: &DerivedPartArgs) -> GeopResult<()> {
212        if args.file.is_empty() {
213            return Err(GeopError::new("choose a file to derive the part from"));
214        }
215        build_model::<Self>(ctx, args)
216    }
217
218    /// The file, chosen from the workspace's programs.
219    fn form<'a>(
220        context: &'a Context<'a>,
221        args: &DerivedPartArgs,
222        _: &FormState<()>,
223    ) -> EditForm<'a, DerivedPartArgs> {
224        let mut f = EditForm::<DerivedPartArgs>::default();
225        let files = file_choices(context.runner, is_program);
226        f.file(
227            "file",
228            "file",
229            args.file.clone(),
230            files,
231            &[],
232            |edit, file| {
233                edit.args.file = file.to_string();
234            },
235        );
236        f
237    }
238}
239
240impl ModelOperation for DerivedPart {
241    fn build(
242        model: &mut NamedModel,
243        reader: &Reader,
244        step: &str,
245        args: &DerivedPartArgs,
246    ) -> GeopResult<()> {
247        let ctx = with_context!("derived_part({step}, file={:?})", args.file);
248        let placed = PlacedArgs {
249            file: args.file.clone(),
250            inputs: args.parameters.clone(),
251            then: Vec::new(),
252        };
253        let derived = build_derived(reader, &placed).with_context(ctx)?;
254        if let Some((failed, e)) = derived.part.failures().first() {
255            let e = e.clone();
256            return Err(e.with_context(format!("step `{failed}` of {:?}", args.file)))
257                .with_context(ctx);
258        }
259        let source = read_model(&derived.part.current()).with_context(ctx)?;
260        let namer = Namer::new("derived", step).with_context(ctx)?;
261        for body in bodies(&source) {
262            let (spec, names) = source.body_record(body).with_context(ctx)?;
263            let names = names.renamed(|name| namer.name(&[name]));
264            model.build_body(spec, names).with_context(ctx)?;
265        }
266        Ok(())
267    }
268}