mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 01:53:57 +00:00
Type sweeps as its glyph outlines and stays editable. On a group or layer every visible member sweeps about one axis on the edge of the members' bounds together, each in its own paint. A surface now takes up to 16,384 profile points and 163,840 faces. Curves take fewer samples, down to 4, when a surface passes those limits, and the command reports the failure below that, after the geometry effects before the Revolve and with the cumulative size of its paint copies counted. Mesh export turns the walls of holes toward them. Expand Appearance works on type with Extrude as well as Revolve. Fixes #3617
124 lines
5.7 KiB
Rust
124 lines
5.7 KiB
Rust
use crate::*;
|
|
|
|
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..1100).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());
|
|
}
|