Skip to main content

manifold_rust/
smoothing.rs

1// Copyright 2026 Lars Brubaker
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Phase 9: smoothing/tangent generation — ported from cpp-reference/manifold/src/smoothing.cpp
16
17use std::collections::HashMap;
18
19use crate::impl_mesh::ManifoldImpl;
20use crate::linalg::{cross, dot, length, Mat3, Vec3, Vec4};
21use crate::math;
22use crate::types::{K_PI, K_TWO_PI, Smoothness, TriRef};
23
24/// Minimum sharp angle in degrees, below which edges are considered coplanar.
25/// Floating point noise in the dihedral angle computation can reach ~1e-6
26/// degrees for nearly-parallel face normals; this threshold must exceed that.
27const K_MIN_SHARP_ANGLE: f64 = 1e-4;
28
29#[inline]
30pub(super) fn vec3_from_vec4(v: Vec4) -> Vec3 {
31    Vec3::new(v.x, v.y, v.z)
32}
33
34#[inline]
35pub(super) fn safe_normalize(v: Vec3) -> Vec3 {
36    let len2 = dot(v, v);
37    if len2 <= 0.0 || !len2.is_finite() {
38        Vec3::new(0.0, 0.0, 0.0)
39    } else {
40        v / len2.sqrt()
41    }
42}
43
44pub(super) fn wrap(radians: f64) -> f64 {
45    if radians < -K_PI {
46        radians + K_TWO_PI
47    } else if radians > K_PI {
48        radians - K_TWO_PI
49    } else {
50        radians
51    }
52}
53
54pub(super) fn angle_between(a: Vec3, b: Vec3) -> f64 {
55    let d = dot(a, b);
56    if d >= 1.0 {
57        0.0
58    } else if d <= -1.0 {
59        K_PI
60    } else {
61        math::acos(d)
62    }
63}
64
65/// Two normals are considered equal when their normalized dot product exceeds
66/// 0.9999 (~0.81°). Per C++ #1671 this tolerance replaces the old exact /
67/// `kPrecision`-squared comparisons in the tangent-generation code.
68pub(super) fn equal_normals(a: Vec3, b: Vec3) -> bool {
69    dot(safe_normalize(a), safe_normalize(b)) > 0.9999
70}
71
72pub fn circular_tangent(tangent: Vec3, edge_vec: Vec3) -> Vec4 {
73    let dir = safe_normalize(tangent);
74    let weight = 0.5f64.max(dot(dir, safe_normalize(edge_vec)));
75    let bz2 = Vec4::new(
76        dir.x * 0.5 * length(edge_vec),
77        dir.y * 0.5 * length(edge_vec),
78        dir.z * 0.5 * length(edge_vec),
79        weight,
80    );
81    let bz3 = Vec4::new(
82        bz2.x * (2.0 / 3.0),
83        bz2.y * (2.0 / 3.0),
84        bz2.z * (2.0 / 3.0),
85        1.0 + (bz2.w - 1.0) * (2.0 / 3.0),
86    );
87    Vec4::new(bz3.x / bz3.w, bz3.y / bz3.w, bz3.z / bz3.w, bz3.w)
88}
89
90pub(super) fn collect_vertex_cycle(mesh: &ManifoldImpl, start: usize) -> Vec<usize> {
91    let mut cycle = Vec::new();
92    let mut current = start;
93    loop {
94        cycle.push(current);
95        let paired = mesh.halfedge[current].paired_halfedge;
96        if paired < 0 {
97            break;
98        }
99        current = crate::impl_mesh::next_halfedge(paired) as usize;
100        if current == start {
101            break;
102        }
103    }
104    cycle
105}
106
107impl ManifoldImpl {
108    pub fn get_normal(&self, halfedge: usize, normal_idx: usize) -> Vec3 {
109        let prop = self.halfedge[halfedge].prop_vert as usize;
110        let base = prop * self.num_prop + normal_idx;
111        let normal = Vec3::new(
112            self.properties[base],
113            self.properties[base + 1],
114            self.properties[base + 2],
115        );
116        // Per #1718: hasNormals=true means CalculateNormals (or a flagged
117        // round-trip) wrote world-frame values, kept world-frame through
118        // Transform/Compose — return them directly. Without the flag, treat
119        // the slot as per-mesh frame and re-rotate to world (legacy contract
120        // for hand-built MeshGL inputs that don't set the bit).
121        let mesh_id = self.mesh_relation.tri_ref[halfedge / 3].mesh_id;
122        match self.mesh_relation.mesh_id_transform.get(&mesh_id) {
123            Some(rel) if !rel.has_normals => rel.get_normal_transform() * normal,
124            _ => normal,
125        }
126    }
127
128    pub fn tangent_from_normal(&self, normal: Vec3, halfedge: usize) -> Vec4 {
129        let edge = self.halfedge[halfedge];
130        let edge_vec = self.vert_pos[edge.end_vert as usize] - self.vert_pos[edge.start_vert as usize];
131        let edge_normal =
132            self.face_normal[halfedge / 3] + self.face_normal[edge.paired_halfedge as usize / 3];
133        // Per C++ #1671 (More smoothing fixes): pick the bi-tangent from the
134        // edge pseudo-normal or the supplied normal depending on their
135        // relative orientation, then cross with the normal. This is more
136        // numerically robust than the old single cross-of-cross form.
137        let bi_tangent = if dot(normal, edge_normal) < 0.0 {
138            cross(edge_normal, edge_vec)
139        } else {
140            cross(normal, edge_vec)
141        };
142        circular_tangent(cross(bi_tangent, normal), edge_vec)
143    }
144
145    pub fn is_inside_quad(&self, halfedge: usize) -> bool {
146        if !self.halfedge_tangent.is_empty() {
147            return self.halfedge_tangent[halfedge].w < 0.0;
148        }
149        let tri = halfedge / 3;
150        let ref_tri = self.mesh_relation.tri_ref[tri];
151        let pair = self.halfedge[halfedge].paired_halfedge as usize;
152        let pair_tri = pair / 3;
153        let pair_ref = self.mesh_relation.tri_ref[pair_tri];
154        if !ref_tri.same_face(&pair_ref) {
155            return false;
156        }
157
158        let same_face = |edge_idx: usize, reference: TriRef| -> bool {
159            let pair = self.halfedge[edge_idx].paired_halfedge as usize / 3;
160            reference.same_face(&self.mesh_relation.tri_ref[pair])
161        };
162
163        let mut neighbor = crate::impl_mesh::next_halfedge(halfedge as i32) as usize;
164        if same_face(neighbor, ref_tri) {
165            return false;
166        }
167        neighbor = crate::impl_mesh::next_halfedge(neighbor as i32) as usize;
168        if same_face(neighbor, ref_tri) {
169            return false;
170        }
171        neighbor = crate::impl_mesh::next_halfedge(pair as i32) as usize;
172        if same_face(neighbor, pair_ref) {
173            return false;
174        }
175        neighbor = crate::impl_mesh::next_halfedge(neighbor as i32) as usize;
176        if same_face(neighbor, pair_ref) {
177            return false;
178        }
179        true
180    }
181
182    pub fn is_marked_inside_quad(&self, halfedge: usize) -> bool {
183        // Check for kInsideQuad == -1.0 exactly (distinct from kMissingNormal == -3.0)
184        !self.halfedge_tangent.is_empty() && self.halfedge_tangent[halfedge].w == -1.0
185    }
186
187    pub fn update_sharpened_edges(&self, sharpened_edges: &[Smoothness]) -> Vec<Smoothness> {
188        let mut old_halfedge_to_new = HashMap::new();
189        for tri in 0..self.num_tri() {
190            let old_tri = self.mesh_relation.tri_ref[tri].face_id;
191            for i in 0..3 {
192                old_halfedge_to_new.insert((3 * old_tri + i as i32) as usize, 3 * tri + i);
193            }
194        }
195        let mut out = sharpened_edges.to_vec();
196        for edge in &mut out {
197            if let Some(&new_edge) = old_halfedge_to_new.get(&edge.halfedge) {
198                edge.halfedge = new_edge;
199            }
200        }
201        out
202    }
203
204    pub fn flat_faces(&self) -> Vec<bool> {
205        let num_tri = self.num_tri();
206        let mut tri_is_flat_face = vec![false; num_tri];
207        for tri in 0..num_tri {
208            let reference = self.mesh_relation.tri_ref[tri];
209            let mut face_neighbors = 0;
210            let mut face_tris = [-1; 3];
211            for j in 0..3 {
212                let neighbor_tri = self.halfedge[3 * tri + j].paired_halfedge as usize / 3;
213                let j_ref = self.mesh_relation.tri_ref[neighbor_tri];
214                if j_ref.same_face(&reference) {
215                    face_neighbors += 1;
216                    face_tris[j] = neighbor_tri as i32;
217                }
218            }
219            if face_neighbors > 1 {
220                tri_is_flat_face[tri] = true;
221                for j in 0..3 {
222                    if face_tris[j] >= 0 {
223                        tri_is_flat_face[face_tris[j] as usize] = true;
224                    }
225                }
226            }
227        }
228        tri_is_flat_face
229    }
230
231    pub fn vert_flat_face(&self, flat_faces: &[bool]) -> Vec<i32> {
232        let mut vert_flat_face = vec![-1; self.num_vert()];
233        let mut vert_ref = vec![TriRef::default(); self.num_vert()];
234        for tri in 0..self.num_tri() {
235            if flat_faces[tri] {
236                for j in 0..3 {
237                    let vert = self.halfedge[3 * tri + j].start_vert as usize;
238                    if vert_ref[vert].same_face(&self.mesh_relation.tri_ref[tri]) {
239                        continue;
240                    }
241                    vert_ref[vert] = self.mesh_relation.tri_ref[tri];
242                    vert_flat_face[vert] = if vert_flat_face[vert] == -1 { tri as i32 } else { -2 };
243                }
244            }
245        }
246        vert_flat_face
247    }
248
249    /// Port of C++ Manifold::Impl::SetNormals()
250    /// Fills in vertex properties with unshared normals across edges bent
251    /// more than minSharpAngle (in degrees).
252    pub fn set_normals(&mut self, normal_idx: i32, min_sharp_angle: f64) {
253        if self.is_empty() || normal_idx < 0 {
254            return;
255        }
256        // Clamp to avoid treating nearly-coplanar faces as sharp due to
257        // floating point noise in the dihedral computation (~1e-6 degrees).
258        let min_sharp_angle = min_sharp_angle.max(K_MIN_SHARP_ANGLE);
259        let normal_idx = normal_idx as usize;
260
261        let old_num_prop = self.num_prop;
262
263        // Count sharp edges per vertex. Per C++ #1724 (Fix CalculateNormals),
264        // SetNormals no longer special-cases flat faces — an edge is sharp iff
265        // its dihedral exceeds min_sharp_angle. `angle_between` matches C++'s
266        // AngleBetween helper (acos with ±1 clamping), the #1634 form.
267        let mut vert_num_sharp = vec![0i32; self.num_vert()];
268        for e in 0..self.halfedge.len() {
269            if !self.halfedge[e].is_forward() {
270                continue;
271            }
272            let pair = self.halfedge[e].paired_halfedge as usize;
273            let tri1 = e / 3;
274            let tri2 = pair / 3;
275            let dihedral =
276                angle_between(self.face_normal[tri1], self.face_normal[tri2]).to_degrees();
277            if dihedral > min_sharp_angle {
278                vert_num_sharp[self.halfedge[e].start_vert as usize] += 1;
279                vert_num_sharp[self.halfedge[e].end_vert as usize] += 1;
280            }
281        }
282
283        // Expand properties to accommodate normals.
284        // orig_props: old data at old stride (old_num_prop per vert)
285        // new_properties: output buffer at new stride (num_prop per vert)
286        let num_prop = old_num_prop.max(normal_idx + 3);
287        let num_prop_vert = self.num_prop_vert();
288        let orig_props = std::mem::take(&mut self.properties); // compact original
289        let mut new_properties = vec![0.0f64; num_prop * num_prop_vert];
290
291        self.num_prop = num_prop;
292
293        // Save old prop assignments and reset
294        let old_halfedge_prop: Vec<i32> = self.halfedge.iter().map(|h| h.prop_vert).collect();
295        for h in self.halfedge.iter_mut() {
296            h.prop_vert = -1;
297        }
298
299        // Build per-mesh-id inverse normal transform cache.
300        // Matches C++ SetNormals which applies GetInverseNormalTransform() to each vertex's normal
301        // before writing to properties, so that GetNormal (which applies GetNormalTransform()) can
302        // correctly reconstruct the world-space normal.
303        let mut mesh_id_to_inv_transform: HashMap<i32, Mat3> = HashMap::new();
304
305        let num_edge = self.halfedge.len();
306        for start_edge in 0..num_edge {
307            if self.halfedge[start_edge].prop_vert >= 0 {
308                continue;
309            }
310            let vert = self.halfedge[start_edge].start_vert as usize;
311
312            // Look up the inverse normal transform for this vertex's mesh.
313            let mesh_id = self.mesh_relation.tri_ref[start_edge / 3].mesh_id;
314            let inv_transform = *mesh_id_to_inv_transform.entry(mesh_id).or_insert_with(|| {
315                self.mesh_relation
316                    .mesh_id_transform
317                    .get(&mesh_id)
318                    .map(|rel| rel.get_inverse_normal_transform())
319                    .unwrap_or_else(Mat3::identity)
320            });
321
322            if vert_num_sharp[vert] < 2 {
323                // Vertex has single normal. Per #1724 this is always the vertex
324                // pseudo-normal (no flat-face substitution).
325                let world_normal = self.vert_normal[vert];
326                // Per #1718: slot 0 (standard) stores world-frame directly — the
327                // eager-transform contract keeps it in sync with vert_pos /
328                // face_normal. Legacy non-zero idx stores per-mesh frame, which
329                // get_mesh_gl's run transform recovers to world on export.
330                let normal = if normal_idx == 0 {
331                    world_normal
332                } else {
333                    inv_transform * world_normal
334                };
335
336                let mut last_prop: i32 = -1;
337                // ForVert traversal
338                let mut current = start_edge;
339                loop {
340                    let prop = old_halfedge_prop[current];
341                    self.halfedge[current].prop_vert = prop;
342                    if prop != last_prop {
343                        last_prop = prop;
344                        // Copy old properties using old stride, write into new stride
345                        let dst_start = (prop as usize) * num_prop;
346                        let src_start = (prop as usize) * old_num_prop;
347                        for p in 0..old_num_prop.min(num_prop) {
348                            if src_start + p < orig_props.len() {
349                                new_properties[dst_start + p] = orig_props[src_start + p];
350                            }
351                        }
352                        // Set normal
353                        new_properties[prop as usize * num_prop + normal_idx] = normal.x;
354                        new_properties[prop as usize * num_prop + normal_idx + 1] = normal.y;
355                        new_properties[prop as usize * num_prop + normal_idx + 2] = normal.z;
356                    }
357                    current = crate::types::next_halfedge(
358                        self.halfedge[current].paired_halfedge,
359                    ) as usize;
360                    if current == start_edge {
361                        break;
362                    }
363                }
364            } else {
365                // Vertex has multiple normals
366                let center_pos = self.vert_pos[vert];
367                let mut group: Vec<usize> = Vec::new();
368                let mut normals: Vec<Vec3> = Vec::new();
369
370                // Find a sharp edge to start on
371                let mut current = start_edge;
372                let mut prev_face = current / 3;
373                loop {
374                    let next = crate::types::next_halfedge(
375                        self.halfedge[current].paired_halfedge,
376                    ) as usize;
377                    let face = next / 3;
378                    let dihedral =
379                        angle_between(self.face_normal[face], self.face_normal[prev_face])
380                            .to_degrees();
381                    if dihedral > min_sharp_angle {
382                        break;
383                    }
384                    current = next;
385                    prev_face = face;
386                    if current == start_edge {
387                        break;
388                    }
389                }
390
391                let end_edge = current;
392
393                // Calculate pseudo-normals between each sharp edge.
394                // Mirrors C++ ForVert<FaceEdge>(endEdge, transform, binaryOp):
395                //   here = transform(endEdge); current = endEdge;
396                //   do { next = transform(NextHalfedge(current.paired));
397                //        binaryOp(current, here, next); here = next; current = next_halfedge;
398                //   } while (current != endEdge);
399                // normals starts EMPTY — first sharp edge creates group 0.
400                // Per #1724 the edge vector is simply the normalized direction
401                // from the center vertex to the far end of the halfedge; the
402                // old quad/flair-out tangent logic was removed.
403                let get_edge_vec = |he: usize| -> Vec3 {
404                    let end_v = self.halfedge[he].end_vert as usize;
405                    safe_normalize(self.vert_pos[end_v] - center_pos)
406                };
407
408                let mut here_face = end_edge / 3;
409                let mut here_ev = get_edge_vec(end_edge);
410                current = end_edge;
411                loop {
412                    let next_he = crate::types::next_halfedge(
413                        self.halfedge[current].paired_halfedge,
414                    ) as usize;
415                    let next_face = next_he / 3;
416                    let mut next_ev = get_edge_vec(next_he);
417
418                    // Check for sharp edge between here and next.
419                    let dihedral =
420                        angle_between(self.face_normal[here_face], self.face_normal[next_face])
421                            .to_degrees();
422                    if dihedral > min_sharp_angle {
423                        normals.push(Vec3::splat(0.0));
424                    }
425                    group.push(normals.len() - 1);
426
427                    // Accumulate angle-weighted normal (C++: cross(next.edgeVec, here.edgeVec)).
428                    if next_ev.x.is_finite() {
429                        let c = cross(next_ev, here_ev);
430                        let angle = angle_between(here_ev, next_ev);
431                        *normals.last_mut().unwrap() =
432                            *normals.last().unwrap() + safe_normalize(c) * angle;
433                    } else {
434                        next_ev = here_ev;
435                    }
436
437                    here_face = next_face;
438                    here_ev = next_ev;
439                    current = next_he;
440                    if current == end_edge {
441                        break;
442                    }
443                }
444
445                // Per #1724: transform into the storage frame first, then
446                // normalize. Per #1718: only the legacy non-zero idx applies the
447                // inv_transform; slot 0 stores world-frame directly.
448                for n in normals.iter_mut() {
449                    *n = if normal_idx == 0 {
450                        safe_normalize(*n)
451                    } else {
452                        safe_normalize(inv_transform * *n)
453                    };
454                }
455
456                // Assign property vertices.
457                // Mirrors C++ ForVert(endEdge, func) which advances BEFORE visiting:
458                //   do { current = NextHalfedge(paired); func(current); } while (current != endEdge)
459                // So endEdge itself is visited LAST (gets group[N-1]), not first.
460                let mut last_group: usize = 0;
461                let mut last_prop: i32 = -1;
462                let mut new_prop: i32 = -1;
463                let mut idx = 0usize;
464                current = crate::types::next_halfedge(
465                    self.halfedge[end_edge].paired_halfedge,
466                ) as usize;
467                loop {
468                    let prop = old_halfedge_prop[current];
469                    let g = if idx < group.len() { group[idx] } else { 0 };
470
471                    if g != last_group && g != 0 && prop == last_prop {
472                        // Split property vertex
473                        last_group = g;
474                        new_prop = (new_properties.len() / num_prop) as i32;
475                        new_properties.resize(new_properties.len() + num_prop, 0.0);
476                        let src_start = (prop as usize) * old_num_prop;
477                        for p in 0..old_num_prop.min(num_prop) {
478                            if src_start + p < orig_props.len() {
479                                new_properties[new_prop as usize * num_prop + p] = orig_props[src_start + p];
480                            }
481                        }
482                        if g < normals.len() {
483                            new_properties[new_prop as usize * num_prop + normal_idx] = normals[g].x;
484                            new_properties[new_prop as usize * num_prop + normal_idx + 1] = normals[g].y;
485                            new_properties[new_prop as usize * num_prop + normal_idx + 2] = normals[g].z;
486                        }
487                    } else if prop != last_prop {
488                        // Update property vertex
489                        last_prop = prop;
490                        new_prop = prop;
491                        let dst_start = (prop as usize) * num_prop;
492                        let src_start = (prop as usize) * old_num_prop;
493                        for p in 0..old_num_prop.min(num_prop) {
494                            if src_start + p < orig_props.len() {
495                                new_properties[dst_start + p] = orig_props[src_start + p];
496                            }
497                        }
498                        if g < normals.len() {
499                            new_properties[prop as usize * num_prop + normal_idx] = normals[g].x;
500                            new_properties[prop as usize * num_prop + normal_idx + 1] = normals[g].y;
501                            new_properties[prop as usize * num_prop + normal_idx + 2] = normals[g].z;
502                        }
503                    }
504
505                    self.halfedge[current].prop_vert = new_prop;
506                    idx += 1;
507
508                    let next_current = crate::types::next_halfedge(
509                        self.halfedge[current].paired_halfedge,
510                    ) as usize;
511                    // Stop after visiting end_edge (C++ stops when current == halfedge)
512                    if current == end_edge {
513                        break;
514                    }
515                    current = next_current;
516                }
517            }
518        }
519
520        self.properties = new_properties;
521    }
522}
523
524// Tangent-related methods (vert_halfedge, sharpen_edges, sharpen_tangent,
525// linearize_flat_tangents, distribute_tangents, create_tangents_from_normals,
526// create_tangents) extracted to smoothing_tangents.rs
527#[path = "smoothing_tangents.rs"]
528mod smoothing_tangents;
529
530#[cfg(test)]
531#[path = "smoothing_tests.rs"]
532mod tests;