Files
vectorcraft/crates/three-d/src/tests_lighting.rs
T
alpha-rock 039a467dcb M9: add multiple colored lights to 3D effects
Add opaque RGB light color and up to ten independently adjustable lights to
Revolve, Extrude & Bevel, Inflate and Rotate. Keep light selection, Add/Delete,
selected-light gizmos and preview transactions on the existing command path.

Preserve legacy white-light Inflate polygons, paint composition and seam joins,
including the 0.05 seam minimum. Use shared smooth normals for colored Inflate,
and batch solid-fill RGB channel paths behind one surface clip. Keep source
paths editable and SVG/PDF output vector artwork.

Rebase onto main f7de3d6a, retaining both sides of progress docs and all 22
catalogs. Shade freeform points once through the shared paint mapper. Document
release measurements and a reproducible CLI fixture, including the remaining
source-gradient cost and the one-light timing tradeoff.

Validation: full cargo xtask ci passed all 10 steps; 7,978 tests passed, zero
failed and 11 ignored. Four legacy white-light PNGs match unchanged main byte
for byte. Three paired release bench/perf repetitions completed without busy
warnings; all six standard perf runs passed every budget. Two-light solid
Inflate improved from 136.2 to 96.8 ms/frame at 1200x1200, default eight threads.
2026-10-18 01:11:39 +07:00

118 lines
5.4 KiB
Rust

use super::*;
#[test]
fn color_combines_with_neutral_ambient_before_channel_clamping() {
let n = V3(0.0, 0.0, 1.0);
let o = Revolve { light_color: [0.0, 0.5, 1.0], ..Default::default() };
assert_eq!(illumination(&o, n, n), [0.25, 0.65, 1.0]);
assert_eq!(illumination(&o, V3(0.0, 0.0, -1.0), n), [0.25; 3]);
assert_eq!(illumination(&Revolve { light_intensity: 0.0, ..o }, n, n), [0.25; 3]);
assert_eq!(illumination(&Revolve { shade: false, ..o }, n, n), [1.0; 3]);
assert_eq!(illumination(&Revolve { light_color: [0.0; 3], ..o }, n, n), [0.25; 3]);
}
#[test]
fn white_light_matches_existing_brightness_with_softness_front_and_back() {
for softness in [0.0, 0.6, 1.0] {
for behind in [false, true] {
let o = Revolve { light_softness: softness, light_behind: behind, ..Default::default() };
let [x, y, z] = o.light_direction();
let l = V3(x, y, z);
for angle in [-180.0f64, -90.0, 0.0, 45.0, 90.0, 180.0] {
let (s, c) = angle.to_radians().sin_cos();
let n = V3(s, 0.0, c);
assert_eq!(illumination(&o, n, l), [brightness(&o, n, l); 3]);
}
}
}
}
#[test]
fn every_geometry_type_produces_finite_rgb_illumination_without_changing_geometry() {
let p = PathData::from_bezpath(&BezPath::from_svg("M0 0L100 0L100 100L0 100Z").unwrap());
let b = p.bounds().unwrap();
let view = View { light_height: 90.0, light_color: [1.0, 0.0, 0.0], ..Default::default() };
let shapes = [
plane(&p, vectorcraft_geom::FillRule::NonZero, b, view).unwrap(),
extrude(&p, vectorcraft_geom::FillRule::NonZero, b, Extrude::default(), view).unwrap(),
inflate(&p, vectorcraft_geom::FillRule::NonZero, b, Inflate::default(), view).unwrap(),
];
for f in shapes.iter().flatten() {
assert!(f.illumination[0] >= f.illumination[1]);
assert_eq!(f.illumination[1], 0.25);
assert_eq!(f.illumination[2], 0.25);
}
assert_eq!(shapes[0][0].illumination, [1.0, 0.25, 0.25]);
let profile = PathData::from_bezpath(&BezPath::from_svg("M0 0L0 100").unwrap());
let o = Revolve { offset: 50.0, light_color: [1.0, 0.0, 0.0], ..Default::default() };
let red = revolve(&profile, profile.bounds().unwrap(), o).unwrap();
let white = revolve(&profile, profile.bounds().unwrap(), Revolve { light_color: [1.0; 3], ..o }).unwrap();
assert_eq!(red.len(), white.len());
for (r, w) in red.iter().zip(&white) {
assert_eq!(r.points, w.points);
assert_eq!(r.depth, w.depth);
assert_eq!(r.illumination[0], w.brightness);
assert_eq!(&r.illumination[1..], &[0.25, 0.25]);
}
let visible = visible_faces(&red).unwrap();
assert!(!visible.is_empty());
for f in visible {
assert_eq!(f.illumination, [f.brightness, 0.25, 0.25]);
}
}
#[test]
fn non_finite_and_out_of_range_light_channels_are_bounded() {
let o = Revolve { light_color: [f64::NAN, -10.0, f64::INFINITY], ..Default::default() }.sanitized();
assert_eq!(o.light_color, [1.0, 0.0, 1.0]);
assert_eq!(illumination(&o, V3(0.0, 0.0, 1.0), V3(0.0, 0.0, 1.0)), [1.0, 0.25, 1.0]);
}
#[test]
fn smooth_color_keeps_channel_saturation_and_shared_edges() {
let a = Point::new(0.0, 0.0);
let b = Point::new(100.0, 0.0);
let c = Point::new(100.0, 100.0);
let d = Point::new(0.0, 100.0);
let make = |points: &[Point], values: &[f64]| LightGradient {
diffuse: ShadeGradient::triangle(points, values).unwrap(),
ambient: 0.25,
color: [1.0, 0.5, 0.0],
};
let g = make(&[a, b, c], &[0.0, 2.0, 1.0]);
let h = make(&[a, c, d], &[0.0, 1.0, 0.5]);
assert_eq!(g.stops().len(), 4, "two distinct channel clipping points");
for (t, rgb) in g.stops() {
assert_eq!(rgb, g.at(g.diffuse.start.lerp(g.diffuse.end, f64::from(t))));
}
for t in [0.0, 0.2, 0.5, 0.9, 1.0] {
let p = a.lerp(c, t);
for (x, y) in g.at(p).into_iter().zip(h.at(p)) {
assert!((x - y).abs() < 1e-12);
}
}
assert_eq!(g.at(b), [1.0, 1.0, 0.25]);
assert_eq!(g.at(a.lerp(b, 0.5)), [1.0, 0.75, 0.25], "clipped red must not flatten green");
}
#[test]
fn colored_smooth_surfaces_keep_caps_curves_and_visibility_placement() {
let p = PathData::from_bezpath(&BezPath::from_svg("M40 0 C70 20 70 60 40 80").unwrap());
let o = Revolve { offset: 20.0, rotation_x: -25.0, light_color: [1.0, 0.4, 0.1], segments: 24, ..Default::default() };
let faces = revolve(&p, p.bounds().unwrap(), o).unwrap();
let white = revolve(&p, p.bounds().unwrap(), Revolve { light_color: [1.0; 3], ..o }).unwrap();
assert_eq!(faces.len(), white.len());
assert!(faces.iter().any(|f| f.light_gradient.is_some()));
let caps: Vec<_> = faces.iter().filter(|f| f.cap).collect();
assert_eq!(caps.len(), 2);
assert!(caps.iter().all(|f| f.outline.is_some() && f.light_gradient.is_none()), "caps remain continuous flat-lit paths");
for (red, white) in faces.iter().zip(&white) {
assert_eq!(red.path(), white.path());
assert_eq!(red.depth, white.depth);
assert_eq!(red.gradient, white.gradient, "the legacy white ramp is unchanged");
}
let visible = visible_faces(&faces).unwrap();
assert!(visible.iter().any(|f| f.light_gradient.is_some()));
assert!(visible.iter().all(|v| faces.iter().any(|f| f.light_gradient == v.light_gradient && f.illumination == v.illumination)));
}