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//! 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}