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