Skip to main content

geop_lib_booleans/remesh/
mod.rs

1mod remesh_edges_x_edges;
2mod remesh_edges_x_faces;
3mod remesh_tangent_branches;
4pub(crate) mod remesh_vertices;
5pub(crate) mod remesh_vertices_x_edges;
6#[cfg(test)]
7mod render_test;
8
9use geop_core_geometry::search;
10use geop_core_math::{
11    geop_error::{GeopResult, WithContext},
12    scalars::Scalar,
13    vector::Vector3,
14    with_context,
15};
16use std::collections::HashMap;
17
18use geop_core_topology::{Body, EdgeId, FaceId, VertexId};
19use geop_ops_model::{NamedModel, Namer};
20
21use crate::naming::BooleanNaming;
22use crate::remesh::{
23    remesh_edges_x_edges::remesh_edges_x_edges, remesh_edges_x_faces::remesh_edges_x_faces,
24    remesh_tangent_branches::remesh_tangent_branches, remesh_vertices::remesh_vertices,
25    remesh_vertices_x_edges::remesh_vertices_x_edges,
26};
27
28/// Every tunable [`remesh`] and its phases need. Bundled rather than passed
29/// positionally: the phases don't share one tolerance (see
30/// `curve_curve_min_subdivision_size`'s own note below), so the list is
31/// long enough that positional arguments stop being readable at the call
32/// site — and each field's meaning is documented once here instead of in
33/// every signature it's threaded through.
34///
35/// Every field only affects *how hard a search tries* (budget, tolerance,
36/// step length), never what a converged answer means — see `AGENTS.md`'s
37/// "never use epsilons" rule.
38#[derive(Clone, Copy, Debug)]
39pub struct RemeshParams {
40    /// Solution cap for the pairwise `curve_curve_intersect` /
41    /// `curve_surface_intersect` searches: how many points one returns at
42    /// most — its crossings, or, where the two coincide along an arc, that
43    /// overlap's ends and points spread over it. The searches find overlaps
44    /// directly, not by hitting the cap.
45    pub max_edge_intersections: usize,
46    /// Node budget for every subdivision search.
47    pub max_nodes: usize,
48    /// Convergence tolerance for the *single*-shape searches
49    /// (`curve_could_contain` / `surface_could_contain`).
50    pub min_subdivision_size: Scalar,
51    /// Convergence tolerance for the *pairwise* searches. Deliberately its
52    /// own, looser value: empirically a pairwise search needs a much
53    /// looser tolerance than a single-shape query to reliably converge
54    /// within `max_nodes` at all — see `ValidationParameters`'s own doc
55    /// comment.
56    pub curve_curve_min_subdivision_size: Scalar,
57    /// How many marching steps one traced curve may take before it's
58    /// considered to have failed to reach a terminating vertex. An effort
59    /// budget only: a trace that reaches its vertex does so in the same
60    /// steps whatever the budget. The steps scale with the surfaces traced
61    /// between and their curvature, not with the model's units, so a curve
62    /// takes about 16 steps across a flat face and 64 around a turn.
63    pub max_trace_steps: usize,
64    /// How many rewrites — splits, merges, imprints — one stage of the
65    /// remesh may make before it is considered not to settle, and how many
66    /// pairs of faces one start point may trace between. An effort budget
67    /// only: a stage that settles makes the same rewrites whatever the
68    /// budget, and one that runs out is an error, never taken as settled.
69    pub max_rewrites: usize,
70}
71
72impl Default for RemeshParams {
73    fn default() -> Self {
74        Self {
75            max_edge_intersections: 3,
76            max_nodes: search::MAX_NODES,
77            min_subdivision_size: search::min_subdivision_size(),
78            curve_curve_min_subdivision_size: Scalar::from_f64(1e-4),
79            max_trace_steps: 1_000,
80            max_rewrites: 100_000,
81        }
82    }
83}
84
85/// Split `edge` at its parameter `t`, where it crosses `face` at `point`:
86/// at `vertex`, the vertex already there, or at a new one named as the
87/// piercing of `face`.
88#[allow(clippy::too_many_arguments)]
89pub(crate) fn split_at_crossing(
90    model: &mut NamedModel,
91    naming: &mut BooleanNaming,
92    edge: EdgeId,
93    t: Scalar,
94    vertex: Option<VertexId>,
95    point: Vector3,
96    face: FaceId,
97    params: &RemeshParams,
98) -> GeopResult<()> {
99    let vertex = match vertex {
100        Some(vertex) => vertex,
101        None => {
102            let vertex = model.insert_vertex(point, naming.provisional())?;
103            naming.piercing(vertex, edge, t, face)?;
104            vertex
105        }
106    };
107    let new_edge = model
108        .split_edge_at_vertex(
109            edge,
110            t,
111            vertex,
112            params.max_nodes,
113            params.curve_curve_min_subdivision_size,
114            naming.provisional(),
115        )
116        .with_context(with_context!(
117            "edge={edge}, t={t:?}, vertex={vertex} at {point:?}, crossing face={face}"
118        ))?;
119    naming.edge_split(edge, new_edge, vertex)
120}
121
122/// Prepare `body_a` and `body_b`'s topology for a boolean operation or a
123/// split: every coincidence between them (vertex/vertex, vertex/edge,
124/// edge/edge, edge/face) is resolved into shared topology, so that
125/// afterwards the two bodies meet only along entities they genuinely share.
126/// Either may be a sheet: imprinting asks nothing of a body but its faces,
127/// edges and vertices.
128///
129/// Runs in strictly increasing order of dimension — vertices, then edges
130/// against vertices, then edges against edges, then edges against faces
131/// (but for the branch points of tangent edges, which are vertices) —
132/// so each phase can assume everything lower-dimensional has already
133/// settled (e.g. `remesh_vertices_x_edges` treats a point-coincidence with
134/// a *different* vertex id as a bug, since `remesh_vertices` should
135/// already have merged them).
136///
137/// Everything it creates is named by `namer`, following
138/// `naming`'s scheme.
139///
140/// Returns what every face and edge of either body came from (see
141/// [`Origins`]).
142pub fn remesh(
143    model: &mut NamedModel,
144    namer: &Namer,
145    body_a: impl Into<Body>,
146    body_b: impl Into<Body>,
147    params: RemeshParams,
148) -> GeopResult<Origins> {
149    let (body_a, body_b) = (body_a.into(), body_b.into());
150    let ctx = with_context!("remesh(body_a={body_a}, body_b={body_b}, params={params:?})");
151
152    let mut naming = BooleanNaming::new(model, namer, &[body_a, body_b]).with_context(&ctx)?;
153
154    remesh_vertices(model, body_a, body_b).with_context(&ctx)?;
155
156    // The one place an edge x face question creates a vertex ahead of the
157    // edge phases: where an intersection branch leaves an edge along which
158    // the two solids' faces touch tangentially (see
159    // `remesh_tangent_branches`). Such an edge is usually one of a pair —
160    // the shared profile, as each solid built it — so the new vertex lies on
161    // the other solid's copy too, and `remesh_vertices_x_edges` below splits
162    // that one like any other vertex on an edge.
163    for (edge_solid, face_solid) in [(body_a, body_b), (body_b, body_a)] {
164        remesh_tangent_branches(model, &mut naming, edge_solid, face_solid, &params)
165            .with_context(&ctx)?;
166    }
167
168    // `remesh_vertices_x_edges` only splits `body_a`'s edges at `body_b`'s
169    // vertices — it has no idea a `body_a` vertex might just as well be
170    // sitting on one of `body_b`'s edges, so that direction needs its own,
171    // separate call with the solids swapped. Vertex positions never change
172    // (splitting only refines topology), so running direction A then
173    // direction B can't reopen the other: a coincidence direction B finds
174    // was already there when direction A ran and would have been found
175    // then too, and vice versa.
176    remesh_vertices_x_edges(model, &mut naming, body_a, body_b, &params).with_context(&ctx)?;
177    remesh_vertices_x_edges(model, &mut naming, body_b, body_a, &params).with_context(&ctx)?;
178
179    // Unlike the vertex-onto-edge step above, this one already checks every
180    // `body_a` edge against every `body_b` edge directly (both directions
181    // in one nested loop), so it doesn't need a second, swapped call.
182    remesh_edges_x_edges(model, &mut naming, body_a, body_b, &params).with_context(&ctx)?;
183
184    // Last, and highest-dimensional: every edge that pierces or lies within
185    // one of the other solid's faces. Like `remesh_edges_x_edges` this
186    // handles both directions internally (see its own doc comment).
187    remesh_edges_x_faces(model, &mut naming, body_a, body_b, &params).with_context(&ctx)?;
188
189    let origins = Origins {
190        faces: naming.face_origins().clone(),
191        edges: naming.edge_origins().clone(),
192    };
193    naming.finish(model).with_context(&ctx)?;
194    Ok(origins)
195}
196
197/// What the faces and edges of two remeshed bodies came from: the name each
198/// had before, or, for a piece split off one, the name that one had — its
199/// *origin* (see `naming`). Entities created otherwise, and since
200/// deleted, may be missing or left over.
201#[derive(Clone, Debug, Default)]
202pub struct Origins {
203    pub faces: HashMap<FaceId, String>,
204    pub edges: HashMap<EdgeId, String>,
205}