//! Extruded, bevelled meshes from 2D outlines (Advanced 3D text and shape layers' Geometry //! Options: Bevel Style, Bevel Depth, Hole Bevel Depth, Extrusion Depth). //! //! Input outlines are closed polylines in layer space (x right, y down). The mesh is in layer //! space too: the front cap lies on the layer plane (z = 0, facing the camera, −z), the back cap //! at z = depth. Each contour gets a ring of vertices per profile step (inset from the outline //! by the bevel, at a depth along the profile); consecutive rings are joined by quads, the caps //! are triangulated from the innermost front/back rings with holes. Caps and walls share //! positions exactly, so the mesh is watertight (welded by position). use serde::{Deserialize, Serialize}; use crate::Primitive; use crate::triangulate::{signed_area, triangulate}; type P = [f64; 2]; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum BevelStyle { #[default] None, Angular, Concave, Convex, } impl BevelStyle { pub const ALL: [BevelStyle; 4] = [BevelStyle::None, BevelStyle::Angular, BevelStyle::Concave, BevelStyle::Convex]; pub fn label(self) -> &'static str { match self { BevelStyle::None => "None", BevelStyle::Angular => "Angular", BevelStyle::Concave => "Concave", BevelStyle::Convex => "Convex", } } pub fn from_index(i: u32) -> BevelStyle { BevelStyle::ALL.get(i as usize).copied().unwrap_or_default() } /// (inset fraction 1 → 0, depth fraction 0 → 1) along the profile at `t` in 0..1. fn profile(self, t: f64) -> (f64, f64) { let th = t * std::f64::consts::FRAC_PI_2; match self { BevelStyle::None | BevelStyle::Angular => (1.0 - t, t), // Quarter circle bulging out towards the outline corner. BevelStyle::Convex => (1.0 - th.sin(), 1.0 - th.cos()), // Quarter circle scooped in. BevelStyle::Concave => (th.cos(), th.sin()), } } fn steps(self) -> usize { match self { BevelStyle::None => 0, BevelStyle::Angular => 1, _ => 6, } } } /// Geometry Options of an extruded layer. #[derive(Clone, Copy, Debug, PartialEq)] pub struct ExtrudeParams { pub bevel: BevelStyle, /// Pixels. pub bevel_depth: f64, /// Hole bevels as a fraction of the bevel depth (0..1). pub hole_bevel: f64, /// Pixels (> 0). pub depth: f64, } impl Default for ExtrudeParams { fn default() -> Self { ExtrudeParams { bevel: BevelStyle::None, bevel_depth: 2.0, hole_bevel: 1.0, depth: 50.0 } } } /// An outer contour with its holes. #[derive(Clone, Debug, PartialEq)] pub struct Outline { pub outer: Vec
,
pub holes: Vec ,
/// Outward unit normal of edge i (pts[i] → pts[i+1]).
edge_n: Vec ,
/// Miter offset direction per vertex (outward, length ≥ 1).
miter: Vec ,
/// Smooth-shaded vertex (shallow corner).
smooth: Vec , outer: bool, bevel: f64) -> Contour {
// Outer counter-clockwise (in x/y), holes clockwise: the outward normal is (dy, −dx).
let ccw = signed_area(&pts) > 0.0;
if ccw != outer {
pts.reverse();
}
let n = pts.len();
let edge_n: Vec = (0..n)
.map(|i| {
let (a, b) = (pts[i], pts[(i + 1) % n]);
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
let l = dx.hypot(dy).max(1e-12);
[dy / l, -dx / l]
})
.collect();
let mut miter = vec![[0.0; 2]; n];
let mut smooth = vec![false; n];
for i in 0..n {
let (n0, n1) = (edge_n[(i + n - 1) % n], edge_n[i]);
let d = n0[0] * n1[0] + n0[1] * n1[1];
let s = [n0[0] + n1[0], n0[1] + n1[1]];
let k = 1.0 + d;
miter[i] = if k < 0.25 {
// Very sharp corner: limit the miter (offset along the bisector, length 2.8).
let l = s[0].hypot(s[1]).max(1e-12);
[s[0] / l * 2.8, s[1] / l * 2.8]
} else {
[s[0] / k, s[1] / k]
};
smooth[i] = d > 0.82; // < ~35° turn
}
Contour { pts, edge_n, miter, smooth, bevel }
}
/// Rings of a contour: (inset, z) from the front cap to the back cap.
fn rings(style: BevelStyle, bevel: f64, depth: f64) -> Vec<(f64, f64)> {
let b = bevel.clamp(0.0, depth * 0.5);
let steps = if b > 1e-9 { style.steps() } else { 0 };
if steps == 0 {
return vec![(0.0, 0.0), (0.0, depth)];
}
let front: Vec<(f64, f64)> = (0..=steps)
.map(|k| {
let (f, g) = style.profile(k as f64 / steps as f64);
(b * f, b * g)
})
.collect();
let mut out = front.clone();
let back: Vec<(f64, f64)> = front.iter().rev().map(|&(i, z)| (i, depth - z)).collect();
// Skip a duplicate ring where the walls have zero height (bevel = depth / 2).
let start = if (back[0].1 - out[out.len() - 1].1).abs() < 1e-9 { 1 } else { 0 };
out.extend_from_slice(&back[start..]);
out
}
/// A surface of an extruded mesh, which After Effects colours and shades separately (the
/// Front / Bevel / Side / Back material groups).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Surface {
/// The front cap (facing the camera, on the layer plane).
Front,
/// The bevelled rings between the caps and the walls (front and back).
Bevel,
/// The straight walls.
Side,
/// The back cap.
Back,
}
impl Surface {
pub const ALL: [Surface; 4] = [Surface::Front, Surface::Bevel, Surface::Side, Surface::Back];
/// Index into [`Surface::ALL`] (and into the primitives of [`extrude_surfaces`]).
pub fn index(self) -> usize {
self as usize
}
}
/// Build the extruded mesh of outlines (one primitive, material 0).
pub fn extrude(outlines: &[Outline], p: &ExtrudeParams) -> Primitive {
build(outlines, p, false).into_iter().next().unwrap_or_default()
}
/// [`extrude`] split by surface: one primitive (material 0) per [`Surface`], in
/// [`Surface::ALL`] order. Together they hold the same triangles as [`extrude`].
pub fn extrude_surfaces(outlines: &[Outline], p: &ExtrudeParams) -> [Primitive; 4] {
let mut v = build(outlines, p, true).into_iter();
std::array::from_fn(|_| v.next().unwrap_or_default())
}
/// The extruded mesh in one primitive (`split` false) or one per [`Surface`].
fn build(outlines: &[Outline], p: &ExtrudeParams, split: bool) -> Vec = ring_pos[0][0].iter().map(|q| [q[0], q[1]]).collect();
let holes2: Vec