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}