Skip to main content

brep_kernel/edit/
direct_edit.rs

1//! Direct editing — Golovanov §6.12: "delete face and heal" and "move a face
2//! group" (see [`move_faces`] for the sibling operation's algorithm and
3//! honest v1 scope).
4//!
5//! Removes a transition face (a chamfer, a constant-radius fillet, or any
6//! simple 4-sided face) that sits between two larger neighbour faces, and
7//! heals the resulting hole by EXTENDING the neighbours' carrier surfaces and
8//! RE-INTERSECTING them so the neighbours meet each other directly where the
9//! deleted face used to be. The classic use is "undo a chamfer/fillet": the
10//! two large planes that flanked the bevel extend and re-intersect at the
11//! original sharp edge, the end-cap faces lose the bevel corner, and the
12//! solid returns to its pre-blend shape.
13//!
14//! ## Algorithm (extend → re-intersect → re-trim → reconnect)
15//!
16//! For a 4-sided transition face `F` with neighbours `N0..N3` (one per
17//! boundary edge, in loop order):
18//!
19//! 1. **Classify.** The two neighbours on OPPOSITE boundary edges whose
20//!    (planar) carriers re-intersect in a line passing through `F`'s region
21//!    are the *primary* pair (the faces to extend and rejoin). The other two
22//!    opposite neighbours are the *lateral* faces (the end caps). Exactly one
23//!    opposite pairing must qualify, otherwise the heal is refused.
24//! 2. **Re-intersect (extend).** Because analytic planes are unbounded
25//!    carriers, the primary pair's intersection line `S` is computed in closed
26//!    form (no marching within a bounded patch). `S` clipped by each lateral
27//!    plane gives the two recovered corner points — the new sharp edge's
28//!    endpoints (`triple points`).
29//! 3. **Re-trim.** Every side edge that met `F` at a transition corner is
30//!    relocated onto the recovered corner; the deleted face's two support
31//!    edges become the single new edge `S` in both primary faces; the two
32//!    lateral (cap) edges collapse to points. Each affected neighbour face's
33//!    carrier plane is rebuilt large enough to cover its new boundary and all
34//!    its pcurves are recomputed against it (analytic planes extend for free).
35//! 4. **Reconnect.** `F` and its boundary edges/vertices are pruned, `S` and
36//!    its two vertices are inserted, the shell is reassembled, the genus is
37//!    preserved, and the result must `validate()` or the whole operation is
38//!    refused.
39//!
40//! ## Covered vs deferred (honest scope of this first slice)
41//!
42//! - COVERED: a 4-sided chamfer/fillet (or any simple 4-edge transition face)
43//!   between two PLANAR primary neighbours with two PLANAR lateral caps, convex
44//!   or concave. Chamfer (planar transition) and fillet (cylindrical
45//!   transition) both reduce to the same planar-neighbour heal. CURVED
46//!   ANALYTIC neighbours route to [`heal_open_transition_mixed`], which
47//!   handles both curved primaries (plane × cylinder/ruled revolution
48//!   re-intersection) and curved lateral caps (the re-intersection branch is
49//!   clipped by curve×surface intersection against the cap's carrier).
50//! - COVERED: the WALL OF A THROUGH FEATURE — a drilled bore, or any closed
51//!   wall whose two rims are each the whole hole loop of the face it was
52//!   punched through. Those neighbours were never meant to meet, so there is
53//!   no re-intersection to make: the wall and both hole loops are dropped and
54//!   the neighbours close back over the opening, un-drilling the hole. Nothing
55//!   is refit (the surviving carriers and outer loops are untouched), and the
56//!   genus drops by the handle that goes with it. A mouth that is TANGENT to
57//!   the boundary of the face it opens through is covered by the same cap: the
58//!   tangency pinches that face and the arrangement splits it, so the rim
59//!   arrives as runs of several faces' loops instead of as a hole loop, and
60//!   dropping it rejoins the pieces into the face they were (see
61//!   `direct_edit/delete_faces.rs`).
62//! - COVERED, as a SET: a BAND across a mirrored union — a fillet groove that
63//!   runs over a part and down its side, flanked on both sides by cosurface
64//!   TWINS of one plane or one fillet cylinder. Its free boundary stops on
65//!   setback and tangent lines the band interrupted rather than on a gap
66//!   between two carriers, so the rejoin BRIDGES each interrupted line across
67//!   the band and the twins close back into one face
68//!   (`direct_edit/delete_faces.rs`).
69//! - COVERED, as a SET: a whole CORNER BLEND — a three-sided vertex blend and
70//!   the three four-sided strips that meet on it. One face at a time it is
71//!   ill-posed (a strip's walls re-intersect in the sharp edge, which passes
72//!   the corner sphere at `r·√2` and never meets it, so the strip has no cap to
73//!   clip against); taken together the walls close a cycle of three carriers
74//!   and each strip's corner end is where its own recovered edge meets the
75//!   third wall (`direct_edit/corner_heal.rs`).
76//! - COVERED, as a SET: a CLOSED BAND — any ring of faces whose free boundary
77//!   is one closed run on each of exactly two survivors, such as a stepped nut
78//!   pocket sunk over a through bore. It is the closed transition strip with
79//!   many faces: the two survivors are extended, re-intersected in closed
80//!   form, and each run collapses onto the rim, so the bore runs on up to the
81//!   face the pocket opened through (`direct_edit/closed_band.rs`).
82//! - COVERED, as a SET: a whole BLEND NETWORK over planar faces — the strips,
83//!   vertex blends and mitres a fillet left around a gusset's foot, with more
84//!   than one corner and strips that end on other strips. Each cluster of end
85//!   edges shrinks to the vertex its planes meet in, and each strip becomes the
86//!   sharp edge between its two clusters (`direct_edit/blend_network_heal.rs`).
87//!   A cylindrical strip the selection leaves standing is KEPT: where it mitred
88//!   into a deleted strip it now ends on the cap plane square to its axis, on
89//!   that plane's circular section; any other curved face left beside a network
90//!   is refused by name.
91//! - COVERED, as a SET (see [`delete_faces_and_heal`] and
92//!   `direct_edit/delete_faces.rs`): a BLIND pocket, a blind bore, or a boss —
93//!   every face of the feature selected together. One face at a time these are
94//!   a multi-face gap with nothing to re-intersect; taken together they are a
95//!   PATCH whose free boundary is the whole hole loop of the face the feature
96//!   was sunk through, and the same cap that un-drills a bore un-cuts them.
97//! - DEFERRED (returns a clear `Err`, never a bad solid): free-form
98//!   neighbours (need NURBS carrier extension + marched re-intersect),
99//!   transition faces that are not 4-sided (multi-face gaps) — the cap lane
100//!   reads any number of boundary edges, but a face that has to be healed by
101//!   RE-INTERSECTION still has to be four-sided — a neighbour that
102//!   borders the transition more than once (periodic/closed transition), a
103//!   BLIND pocket's wall on its own (the wall and its floor disc are a
104//!   two-face gap — select the floor as well and the cap lane takes it), a
105//!   wall whose removal would SEVER the body into two parts (a post joining
106//!   two otherwise separate plates wears a through wall's exact signature, so
107//!   the cap checks the shell is still connected before it hands anything
108//!   back), and configurations where the neighbours cannot re-intersect
109//!   cleanly (non-adjacent healing, or parallel planes that are not a through
110//!   wall).
111
112use crate::topology::{CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, VertexRecord};
113use crate::transform_topology::{transform_curve, transform_surface};
114use crate::offset_retrim::{boundary_samples, retrim_face_in_solid};
115use crate::offset_reintersect::{
116    arc_of_section, edge_seeds, match_marched_rim_direction, reintersect_carriers, section_corner,
117    MarchPolicy, ReintersectRefusal, RimLane,
118};
119use crate::{
120    build_pcurve_on_surface, build_pcurve_on_surface_range, intersect_analytic_pair,
121    intersect_curve_surface, make_line, make_revolution, parameter_space_area,
122    plane_ruled_section_arc,
123    project_point_to_curve, solid_model_scale, solid_signed_volume, AffineTransform,
124    AnalyticSurface, BrepSolid,
125    NurbsCurve, NurbsSurface, OffsetEvaluator, OffsetNormal, SurfaceSide, Vec3,
126};
127use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
128
129#[path = "direct_edit/geom.rs"]
130mod geom;
131#[path = "direct_edit/delete_face.rs"]
132mod delete_face;
133#[path = "direct_edit/delete_faces.rs"]
134mod delete_faces;
135#[path = "direct_edit/refit_faces.rs"]
136mod refit_faces;
137#[path = "direct_edit/face_move.rs"]
138mod face_move;
139#[path = "direct_edit/face_move_carrier.rs"]
140mod face_move_carrier;
141#[path = "direct_edit/face_move_recut.rs"]
142mod face_move_recut;
143#[path = "direct_edit/face_rotate.rs"]
144mod face_rotate;
145#[path = "direct_edit/face_draft.rs"]
146mod face_draft;
147pub use face_draft::{draft_faces, draft_faces_parted};
148#[path = "direct_edit/face_offset.rs"]
149mod face_offset;
150#[path = "direct_edit/face_offset_sphere.rs"]
151mod face_offset_sphere;
152#[path = "direct_edit/face_offset_torus.rs"]
153mod face_offset_torus;
154#[path = "direct_edit/face_offset_revolution.rs"]
155mod face_offset_revolution;
156#[path = "direct_edit/face_offset_freeform.rs"]
157mod face_offset_freeform;
158#[path = "direct_edit/closed_heal.rs"]
159mod closed_heal;
160#[path = "direct_edit/open_carriers.rs"]
161mod open_carriers;
162#[path = "direct_edit/open_heal.rs"]
163mod open_heal;
164#[path = "direct_edit/open_heal_census.rs"]
165mod open_heal_census;
166#[path = "direct_edit/closed_heal_census.rs"]
167mod closed_heal_census;
168#[path = "direct_edit/triple_point.rs"]
169mod triple_point;
170#[path = "direct_edit/corner_heal.rs"]
171mod corner_heal;
172#[path = "direct_edit/closed_band.rs"]
173mod closed_band;
174#[path = "direct_edit/split_band.rs"]
175mod split_band;
176#[path = "direct_edit/conical_cap.rs"]
177mod conical_cap;
178#[path = "direct_edit/prism_ring.rs"]
179mod prism_ring;
180#[path = "direct_edit/blend_network_heal.rs"]
181mod blend_network_heal;
182
183
184use blend_network_heal::*;
185use closed_band::*;
186use split_band::*;
187use prism_ring::*;
188use closed_heal::*;
189use closed_heal_census::*;
190use corner_heal::*;
191use delete_face::*;
192use face_move_carrier::*;
193use geom::*;
194use open_carriers::*;
195use open_heal::*;
196use open_heal_census::*;
197use triple_point::*;
198
199/// The slugs of the direct-edit refusals the app explains to a user (its
200/// face-transform help, `BREP_render::face_transform_help`), shared by each
201/// mint and by [`face_transform_reason`].
202pub mod face_refusal_slugs {
203    /// The motion turns the whole body inside out (`UnsupportedGeometry`).
204    pub const SOLID_INVERSION: &str = "solid_inversion";
205    /// The same, measured on the sphere lane (`UnsupportedGeometry`).
206    pub const SPHERE_SOLID_INVERSION: &str = "sphere_solid_inversion";
207    /// A push that inverts the solid (`Internal` from the offset lanes,
208    /// `InvalidInput` from the shared acceptance in `geom.rs`).
209    pub const INVERTED: &str = "inverted";
210    /// The motion carries a face through a neighbour it must meet
211    /// (`UnsupportedGeometry`).
212    pub const EDGE_INVERSION: &str = "edge_inversion";
213    /// The same, across a curved edge (`UnsupportedGeometry`).
214    pub const CURVED_EDGE_INVERSION: &str = "curved_edge_inversion";
215    /// A turn that lays the face parallel to a neighbour (`UnsupportedGeometry`).
216    pub const PARALLEL_LIMIT: &str = "parallel_limit";
217    /// A turn that lays a curved face's axis parallel to a neighbour
218    /// (`UnsupportedGeometry`).
219    pub const AXIS_PARALLEL_LIMIT: &str = "axis_parallel_limit";
220    /// A Transform Face asked to scale (`InvalidInput`).
221    pub const RIGID_SCALE: &str = "scale";
222    /// A Transform Face whose selection has no geometry for the default pivot
223    /// (`InvalidInput`).
224    pub const PIVOT_GEOMETRY: &str = "pivot_geometry";
225}
226use face_refusal_slugs::*;
227
228/// Why a Transform Face refused, as far as the app explains it — read off the
229/// refusal's CLASS and slug, never its text, so a reworded refusal keeps its
230/// explanation and a refusal that merely mentions "parallel" does not borrow
231/// one. `None` is every other refusal: the app shows the kernel's own words.
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum FaceTransformReason {
234    /// The body turns inside out.
235    InsideOut,
236    /// A face is carried past a neighbour it has to meet.
237    PastNeighbour,
238    /// A TURN lays a face (or a curved face's axis) parallel to a neighbour.
239    TurnParallel,
240    /// A face transform asked to scale.
241    RigidScale,
242    /// No geometry to centre the default pivot on.
243    NoPivot,
244}
245
246/// [`FaceTransformReason`] of a refusal, by class and slug.
247pub fn face_transform_reason(refusal: &crate::KernelRefusal) -> Option<FaceTransformReason> {
248    use crate::RefusalClass::{Internal, InvalidInput, UnsupportedGeometry};
249    use FaceTransformReason::*;
250    match &refusal.class {
251        UnsupportedGeometry { what } if what == SOLID_INVERSION || what == SPHERE_SOLID_INVERSION => {
252            Some(InsideOut)
253        }
254        Internal { what } | InvalidInput { what } if what == INVERTED => Some(InsideOut),
255        UnsupportedGeometry { what } if what == EDGE_INVERSION || what == CURVED_EDGE_INVERSION => {
256            Some(PastNeighbour)
257        }
258        UnsupportedGeometry { what } if what == PARALLEL_LIMIT || what == AXIS_PARALLEL_LIMIT => {
259            Some(TurnParallel)
260        }
261        InvalidInput { what } if what == RIGID_SCALE => Some(RigidScale),
262        InvalidInput { what } if what == PIVOT_GEOMETRY => Some(NoPivot),
263        _ => None,
264    }
265}
266
267pub use delete_face::{delete_face_and_heal, resolve_face_by_point};
268pub use delete_faces::delete_faces_and_heal;
269pub use refit_faces::{
270    refit_faces_as_one, refit_refusal_phrases, RefitCarrier, RefitReport, RefitSurfaceKind,
271};
272pub use face_move::move_faces;
273pub use face_rotate::rotate_faces;
274pub(crate) use face_rotate::rotate_faces_rigid;
275// The ROUTING reading: which faces a moved selection would have to open a new
276// hole in, measured before anything is built. Public so the matrix probe can
277// print the partition over every cell without the road being wired to it.
278pub use face_move_carrier::{route_reading, RouteReading};
279// The RE-CUT road: the same edit performed from the original body and the
280// motion, for a translation whose answer needs the topology to change.
281pub use face_move_recut::{recut_moved_planes, recut_moved_planes_rigid, reblend_lost_fillets};
282pub use face_offset::offset_ruled_face;
283pub use face_offset_sphere::offset_sphere_face;
284pub use face_offset_torus::offset_torus_face;
285pub use face_offset_revolution::offset_revolution_face;
286pub use face_offset_freeform::offset_freeform_face;