mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 05:18:40 +00:00
M12: prc-render: scale a triangle's edges before their cross product
normalize already scales by the largest component, but the face normal was the normalized cross product of two edges, and edges near 1e160 overflow that product to infinity first, so the triangle was dropped. unit_cross divides both edges by their largest component before the cross product. a_huge_square_is_drawn fails with the old line (white instead of [190,192,200]). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
1 parent
4e2e77a986
commit
ca21d4fb7b
3 files changed
+29
-2
No files matched your search
@@ -5,7 +5,7 @@
|
||||
|
||||
use crate::WorldMesh;
|
||||
use crate::raster::{Corner, Target, View, ViewCorner, at};
|
||||
use crate::vec3::{cross, dot, length, normalize, scale, sub};
|
||||
use crate::vec3::{cross, dot, length, normalize, scale, sub, unit_cross};
|
||||
use pdfcraft_prc::TriangleMesh;
|
||||
|
||||
/// The largest image [`render`] draws, in pixels (width × height).
|
||||
@@ -318,7 +318,7 @@ impl Scene {
|
||||
let Some(world) = mesh.triangles.get(t).and_then(|tri| corners(mesh, tri)) else {
|
||||
return;
|
||||
};
|
||||
let Some(face) = normalize(cross(sub(at3(&world, 1), at3(&world, 0)), sub(at3(&world, 2), at3(&world, 0)))) else {
|
||||
let Some(face) = unit_cross(sub(at3(&world, 1), at3(&world, 0)), sub(at3(&world, 2), at3(&world, 0))) else {
|
||||
return;
|
||||
};
|
||||
let stored = stored_normals(mesh, t);
|
||||
|
||||
@@ -268,3 +268,11 @@ fn unlit_draws_the_plain_colour_and_the_headlight_shades_by_angle() {
|
||||
assert_eq!(lit(Lighting::Headlight, 1.0), [151, 153, 159, 255]);
|
||||
assert_eq!(lit(Lighting::Unlit, 1.0), [190, 192, 200, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_huge_square_is_drawn() {
|
||||
// Its edges' cross product (about 1e320) overflows unless they are scaled first.
|
||||
let image = render(&[quad(0.0, 1e160, 0.0)], &ortho(4e160), &small()).unwrap();
|
||||
assert_eq!(pixel(&image, 20, 20), [190, 192, 200, 255]);
|
||||
assert_eq!(pixel(&image, 1, 1), [255, 255, 255, 255]);
|
||||
}
|
||||
@@ -40,6 +40,17 @@ pub(crate) fn normalize(a: Vec3) -> Option<Vec3> {
|
||||
(l.is_finite() && l > 0.0).then(|| scale(a, 1.0 / l))
|
||||
}
|
||||
|
||||
/// The unit normal of the plane spanned by `a` and `b` (`a × b`, normalized); `None` when they are
|
||||
/// parallel, zero or not finite. Both are first divided by their largest component, so the cross
|
||||
/// product of two huge edges cannot overflow before it is normalized.
|
||||
pub(crate) fn unit_cross(a: Vec3, b: Vec3) -> Option<Vec3> {
|
||||
let big = a.iter().chain(b.iter()).fold(0.0_f64, |m, c| m.max(c.abs()));
|
||||
if !(big.is_finite() && big > 0.0) {
|
||||
return None;
|
||||
}
|
||||
normalize(cross(a.map(|c| c / big), b.map(|c| c / big)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -63,4 +74,12 @@ mod tests {
|
||||
assert_eq!(normalize([1e200, 0.0, 0.0]), Some([1.0, 0.0, 0.0]));
|
||||
assert_eq!(normalize([0.0, -3e-320, 0.0]), Some([0.0, -1.0, 0.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit_cross_survives_edges_whose_product_overflows() {
|
||||
assert_eq!(unit_cross([1e160, 0.0, 0.0], [0.0, 1e160, 0.0]), Some([0.0, 0.0, 1.0]));
|
||||
assert_eq!(unit_cross([1e-170, 0.0, 0.0], [0.0, 1e-170, 0.0]), Some([0.0, 0.0, 1.0]));
|
||||
assert_eq!(unit_cross([2.0, 0.0, 0.0], [4.0, 0.0, 0.0]), None);
|
||||
assert_eq!(unit_cross([f64::INFINITY, 0.0, 0.0], [0.0, 1.0, 0.0]), None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user