mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
Use automatic vector smooth shading without a separate control. Enable Cap by default and keep continuous rim curves and bounded seam joins. Integrate the shared dialog with the other three 3D effects.
124 lines
5.7 KiB
Rust
124 lines
5.7 KiB
Rust
use super::*;
|
|
|
|
fn path(d: &str) -> PathData {
|
|
PathData::from_bezpath(&BezPath::from_svg(d).unwrap())
|
|
}
|
|
|
|
fn value(f: &Face, p: Point) -> f64 {
|
|
f.gradient.map_or(f.brightness, |g| g.at(p))
|
|
}
|
|
|
|
#[test]
|
|
fn gradient_matches_three_vertex_values_and_shared_diagonal() {
|
|
let [a, b, c, d] = [Point::new(10.0, 20.0), Point::new(90.0, 30.0), Point::new(80.0, 95.0), Point::new(0.0, 85.0)];
|
|
let g = ShadeGradient::triangle(&[a, b, c], &[0.2, 0.9, 0.6]).unwrap();
|
|
let h = ShadeGradient::triangle(&[a, c, d], &[0.2, 0.6, 0.4]).unwrap();
|
|
for (p, expected) in [(a, 0.2), (b, 0.9), (c, 0.6)] {
|
|
assert!((g.at(p) - expected).abs() < 1e-12);
|
|
}
|
|
for t in [0.0, 0.1, 0.5, 0.9, 1.0] {
|
|
assert!((g.at(a.lerp(c, t)) - h.at(a.lerp(c, t))).abs() < 1e-12);
|
|
}
|
|
assert!(ShadeGradient::triangle(&[a, a, c], &[0.1, 0.5, 0.9]).is_none());
|
|
assert!(ShadeGradient::triangle(&[a, b, c], &[0.4; 3]).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn curved_profile_shares_lighting_across_radial_and_curve_sample_edges() {
|
|
let p = path("M40 0 C70 20 70 60 40 80");
|
|
let o = Revolve { smooth_shading: true, cap_ends: false, offset: 20.0, angle: 90.0, rotation_y: -45.0, segments: 16, ..Default::default() };
|
|
let faces = revolve(&p, p.bounds().unwrap(), o).unwrap();
|
|
assert!(faces.iter().filter(|f| f.gradient.is_some()).count() > 60);
|
|
let mut edges = 0;
|
|
for (i, a) in faces.iter().enumerate() {
|
|
for b in faces.iter().skip(i + 1) {
|
|
let shared: Vec<_> = a.points.iter().filter(|p| b.points.iter().any(|q| p.distance(*q) < 1e-8)).copied().collect();
|
|
if let [p, q] = shared.as_slice() {
|
|
edges += 1;
|
|
for t in [0.0, 0.5, 1.0] {
|
|
let p = p.lerp(*q, t);
|
|
assert!((value(a, p) - value(b, p)).abs() < 1e-8, "lighting seam at {p:?}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
assert!(edges > 100, "test both curve sample boundaries and triangle diagonals");
|
|
}
|
|
|
|
#[test]
|
|
fn profile_corners_and_caps_retain_hard_edges() {
|
|
let p = path("M40 0 L40 40 L70 40");
|
|
let o = Revolve { smooth_shading: true, offset: 30.0, angle: 90.0, rotation_y: -45.0, rotation_x: -20.0, segments: 16, ..Default::default() };
|
|
let faces = revolve(&p, p.bounds().unwrap(), o).unwrap();
|
|
let mut hard = false;
|
|
for a in &faces {
|
|
for b in &faces {
|
|
let shared: Vec<_> = a.points.iter().filter(|p| b.points.iter().any(|q| p.distance(*q) < 1e-8)).copied().collect();
|
|
if shared.len() == 2 && shared.iter().any(|p| (value(a, *p) - value(b, *p)).abs() > 0.1) {
|
|
hard = true;
|
|
}
|
|
}
|
|
}
|
|
assert!(hard, "the profile's ninety-degree corner is not rounded by shading");
|
|
let p = path("M40 0 L40 80");
|
|
let flat = revolve(&p, p.bounds().unwrap(), Revolve { cap_ends: true, smooth_shading: false, ..o }).unwrap();
|
|
let smooth = revolve(&p, p.bounds().unwrap(), Revolve { cap_ends: true, ..o }).unwrap();
|
|
let caps: Vec<_> = flat.iter().filter(|f| f.cap).collect();
|
|
assert_eq!(caps.len(), 2);
|
|
assert!(caps.into_iter().all(|cap| smooth.contains(cap)), "caps keep their exact flat shading");
|
|
}
|
|
|
|
#[test]
|
|
fn smooth_vertices_use_the_shared_soft_light_and_back_light() {
|
|
let p = path("M40 0 L40 80");
|
|
let bounds = p.bounds().unwrap();
|
|
for behind in [false, true] {
|
|
let o = Revolve {
|
|
offset: 30.0,
|
|
angle: 90.0,
|
|
rotation_x: -20.0,
|
|
rotation_y: -30.0,
|
|
cap_ends: false,
|
|
light_rotation: 0.0,
|
|
light_height: 30.0,
|
|
light_behind: behind,
|
|
light_softness: 0.8,
|
|
ambient: 0.15,
|
|
..Default::default()
|
|
};
|
|
let faces = revolve(&p, bounds, o).unwrap();
|
|
let vertex = o.rotate(V3(30.0, -40.0, 0.0));
|
|
let point = Point::new(10.0 + vertex.0, 40.0 + vertex.1);
|
|
let normal = o.rotate(V3(1.0, 0.0, 0.0));
|
|
let [x, y, z] = o.light_direction();
|
|
let expected = (0.15 + 0.8 * ((normal.dot(V3(x, y, z)) + 0.8) / 1.8).max(0.0)).clamp(0.0, 1.0);
|
|
let samples: Vec<_> = faces.iter().filter(|face| face.points.iter().any(|p| p.distance(point) < 1e-10)).collect();
|
|
assert!(!samples.is_empty());
|
|
for face in samples {
|
|
assert!((value(face, point) - expected).abs() < 1e-10, "smooth shading preserves Softness and the light's side");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unshaded_legacy_degenerate_and_high_complexity_surfaces_stay_bounded() {
|
|
let p = path("M40 0 L40 80");
|
|
let b = p.bounds().unwrap();
|
|
let o = Revolve { offset: 30.0, ..Default::default() };
|
|
assert!(revolve(&p, b, o).unwrap().iter().any(|f| f.gradient.is_some()));
|
|
assert!(revolve(&p, b, Revolve { smooth_shading: false, ..o }).unwrap().iter().all(|f| f.gradient.is_none()));
|
|
assert_eq!(revolve(&p, b, Revolve { shade: false, ..o }).unwrap(), revolve(&p, b, Revolve { shade: false, smooth_shading: true, ..o }).unwrap());
|
|
for edge in [Edge::Left, Edge::Right] {
|
|
for perspective in [0.0, perspective_from_strength(1.0), 160.0] {
|
|
let o = Revolve { smooth_shading: true, edge, perspective, rotation_x: -35.0, ..o };
|
|
let faces = revolve(&p, b, o).unwrap();
|
|
assert!(faces.iter().all(|f| f.points.iter().all(|p| value(f, *p).is_finite() && (0.0..=1.0).contains(&value(f, *p)))));
|
|
assert!(visible_faces(&faces).is_ok());
|
|
}
|
|
}
|
|
assert!(revolve(&p, b, Revolve { smooth_shading: true, angle: 0.0, ..o }).unwrap().is_empty());
|
|
let mut p = path("M40 0");
|
|
p.subpaths[0].anchors.extend((1..400).map(|i| vectorcraft_geom::Anchor::corner(Point::new(40.0 + (i % 2) as f64, i as f64))));
|
|
assert!(revolve(&p, p.bounds().unwrap(), Revolve { smooth_shading: true, segments: 128, offset: 30.0, ..o }).is_err());
|
|
}
|