Skip to main content

ogeom_offset/
feature.rs

1//! Form features: the named operations a modeller thinks in.
2//!
3//! A pocket is a prism cut into a solid; a pad is the same prism fused onto
4//! it; a rib is a thin pad; a slot is a pocket that runs out of both ends;
5//! a revolved feature turns a profile instead of sweeping it. None of these
6//! is a new geometric construction; each is a sweep and a boolean, and
7//! what makes it a *feature* is that the operation says which it was and
8//! carries the profile through the history.
9//!
10//! Building them here rather than leaving them to the caller is not
11//! ceremony. It fixes the two things a caller gets wrong: which way the
12//! sweep should run so the tool reaches the material it is meant to reach,
13//! and what the history should say afterwards. Both are in one place, and
14//! the vocabulary is the one drawings use.
15
16use ogeom_algo::{Built, make_prism, make_revolution};
17use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
18use ogeom_math::{Axis, Vector};
19use ogeom_topo::{Model, Shape, ShapeType};
20
21/// Which way a feature meets the material.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Feature {
24    /// The swept tool is added: a pad, a boss, a rib.
25    Added,
26    /// The swept tool is removed: a pocket, a slot, a groove.
27    Removed,
28}
29
30/// Sweep `profile` along `vector` and add or remove the result.
31///
32/// The profile is a face (a wire is not a tool, it is the boundary of one),
33/// and the sweep is the ordinary prism, so the feature's walls are ruled
34/// exactly as its profile's edges are. A pocket deeper than the material
35/// simply cuts through; a pad shorter than nothing is refused.
36///
37/// # Errors
38///
39/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
40/// profile is not a face, the vector has no length, or the boolean refuses
41/// the configuration.
42pub fn feature_prism(
43    model: &mut Model,
44    solid: &Shape,
45    profile: &Shape,
46    vector: Vector,
47    sense: Feature,
48    tol: Tolerances,
49) -> OgeomResult<Built> {
50    if model.kind_of(profile)? != ShapeType::Face {
51        ogeom_bail!(
52            Construction,
53            "a form feature sweeps a face; a wire is the boundary of one, \
54             not a tool"
55        );
56    }
57    let tool = make_prism(model, profile, vector, tol)?;
58    applied(model, solid, &tool.shape, profile, sense, tol)
59}
60
61/// Turn `profile` about `axis` through `angle` and add or remove the result.
62///
63/// # Errors
64///
65/// As [`feature_prism`], plus whatever the revolution refuses: an angle
66/// outside `(0, 2π]`, or a profile the axis passes through.
67pub fn feature_revol(
68    model: &mut Model,
69    solid: &Shape,
70    profile: &Shape,
71    axis: Axis,
72    angle: f64,
73    sense: Feature,
74    tol: Tolerances,
75) -> OgeomResult<Built> {
76    if model.kind_of(profile)? != ShapeType::Face {
77        ogeom_bail!(
78            Construction,
79            "a form feature turns a face; a wire is the boundary of one, \
80             not a tool"
81        );
82    }
83    let tool = make_revolution(model, profile, axis, angle, tol)?;
84    applied(model, solid, &tool.shape, profile, sense, tol)
85}
86
87/// A rib: a pad of stated thickness, swept from a profile face's own plane.
88///
89/// The rib is the profile thickened along `normal` by `thickness` and fused
90/// on. It is `feature_prism` with the vector spelled for the case, and it
91/// exists because a rib is a thing a drawing names and a caller should not
92/// have to spell as a prism every time.
93///
94/// # Errors
95///
96/// As [`feature_prism`].
97pub fn feature_rib(
98    model: &mut Model,
99    solid: &Shape,
100    profile: &Shape,
101    normal: Vector,
102    thickness: f64,
103    tol: Tolerances,
104) -> OgeomResult<Built> {
105    if !thickness.is_finite() || thickness <= tol.confusion() {
106        ogeom_bail!(Construction, "a rib of {thickness} thickness holds nothing");
107    }
108    let magnitude = normal.magnitude();
109    if magnitude <= tol.confusion() {
110        ogeom_bail!(Construction, "a rib needs a direction to stand in");
111    }
112    feature_prism(
113        model,
114        solid,
115        profile,
116        normal / magnitude * thickness,
117        Feature::Added,
118        tol,
119    )
120}
121
122/// A slot: a pocket swept along a direction and cut clean through.
123///
124/// The prism runs `depth` each way from the profile, so the tool leaves the
125/// material at both ends and the slot is open however the profile sits.
126///
127/// # Errors
128///
129/// As [`feature_prism`].
130pub fn feature_slot(
131    model: &mut Model,
132    solid: &Shape,
133    profile: &Shape,
134    along: Vector,
135    depth: f64,
136    tol: Tolerances,
137) -> OgeomResult<Built> {
138    if !depth.is_finite() || depth <= tol.confusion() {
139        ogeom_bail!(Construction, "a slot of depth {depth} cuts nothing");
140    }
141    let magnitude = along.magnitude();
142    if magnitude <= tol.confusion() {
143        ogeom_bail!(Construction, "a slot needs a direction to run in");
144    }
145    let direction = along / magnitude;
146    // Swept from behind the profile to past it: a slot is open at both
147    // ends, and a tool that starts *on* the profile leaves a skin.
148    let started = ogeom_algo::transformed(
149        model,
150        profile,
151        ogeom_math::Transform::translation(-direction * depth),
152    )?;
153    feature_prism(
154        model,
155        solid,
156        &started.shape,
157        direction * (depth * 2.0),
158        Feature::Removed,
159        tol,
160    )
161}
162
163/// The boolean half, with the profile carried into the history.
164fn applied(
165    model: &mut Model,
166    solid: &Shape,
167    tool: &Shape,
168    profile: &Shape,
169    sense: Feature,
170    tol: Tolerances,
171) -> OgeomResult<Built> {
172    let mut result = match sense {
173        Feature::Added => ogeom_bool::fuse(model, solid, tool, tol)?,
174        Feature::Removed => ogeom_bool::cut(model, solid, tool, tol)?,
175    };
176    // What the feature was made from is what a later edit will name, so the
177    // profile generates the result rather than vanishing into the tool.
178    result.history.generate(profile, result.shape.clone());
179    Ok(result)
180}