Review fix from #6087. Bounds::of and Camera::fit_meshes framed every
finite triangle, but render skips those whose colour has zero alpha, so a
clear, distant mesh could shrink the visible model in the image. The
vertices they frame now skip fully transparent triangles, as render does.
Test, shown failing with the fix reverted:
a_fully_transparent_triangle_does_not_widen_the_box_or_the_fit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
- View::project divides by z (or the orthographic half-height) before it
multiplies, so an on-screen corner no longer turns NaN when x times the
focal length, or the half-height times the aspect ratio, overflows.
- View::clip cuts each edge from the same end whichever triangle meets it,
so two triangles sharing an edge across the near plane get one cut point.
- Scene::draw takes each edge with vec3::span, which halves the
difference when it overflows, so a finite triangle wider than f64::MAX
keeps its face normal.
- assemble charges each file structure's meshes to the assembly ceilings
as they are decoded, so decoding stops at the structure that passes one.
Tests, each shown failing with its fix reverted:
a_shared_edge_is_cut_at_the_same_point_from_either_triangle,
an_on_screen_triangle_is_drawn_when_projecting_it_would_overflow,
a_triangle_whose_edges_overflow_is_drawn,
span_keeps_the_direction_of_a_difference_that_overflows,
decoding_stops_at_the_structure_that_passes_the_ceiling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
- vec3: length divides by the largest component before squaring, so
a camera 1e200 away keeps a finite near plane; unit_cross normalizes
each edge first, so edges of very different sizes keep their normal.
- render: MAX_RENDER_WORK (2^31) caps the pixel centres one render
tests, charged per triangle's clipped bounding box before scanning;
past it, RenderError::TooMuchWork.
- Bounds::centre halves each end before adding, finite for any finite
box. Bounds::of and fit_meshes use only corners of triangles render
draws. Camera::frame builds its basis from the direction, not from
points near the model, so a small model far from the origin fits.
Each new test fails with its fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
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
- assemble: a placement naming a tessellation its file structure does not
have is now PrcError::Malformed instead of a silently missing part;
wires, markups and compressed tessellations are still skipped.
- assemble: positions, normals and texture coordinates are charged against
a new MAX_ASSEMBLED_VERTICES (2^24) with RenderError::TooManyVertices, so
a one-triangle mesh with many vertices placed many times cannot outgrow
the triangle ceiling's memory bound. A refused mesh spends nothing.
- raster: a field of view too small for a finite focal length is a Camera
error, not a background-only image.
- raster: a pixel centre exactly on an edge belongs to one triangle only
(the side the triangle lies on, +x first, then +y), so translucent
quads no longer blend their diagonal twice.
- raster: under a perspective camera the shade is interpolated with the
1/z depth key, so it follows the surface rather than the screen.
- vec3: normalize divides by the largest component first, so a cross
product above ~1e154 (or a subnormal one) still normalizes.
- render docs and README state the near plane: 1/10,000 of the
eye-to-target distance.
Each new test fails with its fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
New L3 crate over pdfcraft-prc: assemble places every product-tree
tessellation into world space with its colours (pure `place` does the
transform), Camera fits a view to the model, and render fills
depth-buffered triangles with headlight or unlit shading into an RGBA
image. Opaque triangles draw first, translucent ones blend over them;
perspective views clip at a near plane.
Never panics: MAX_RENDER_PIXELS and MAX_ASSEMBLED_TRIANGLES cap the
work, every pixel and vertex index is checked, and damage returns
RenderError. Single-threaded.
Tests: 24 unit tests (two drive the product-tree placement path) plus an end-to-end one that builds a synthetic
PRC stream in-test, renders it, and checks the background, the nearer
triangle and the farther one by pixel; every truncation of that stream
parses and assembles without panicking. Reverting the depth test or the
placement transform fails the matching tests.
Adapted from pdfcer (MIT, same author) at 912437c3, trimmed to
the camera, raster and headlight parts and rewritten to the
pdfcraft-prc types.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9