* prc: rebuild compressed tessellations whose normals are recalculated
A TESS_3D_Compressed (WD 7.8.9.7) was read to its end but never rebuilt,
so prc-render drew nothing for it. Rebuild the form whose normals are
recalculated, with three edge statuses per triangle, into a Mesh grouped
by face and styled by each face's line attribute.
The walk (tess/compressed.rs) takes a seed, then one apex per triangle,
from a reference or the next point in the frame of the edge it continues
across (WD 7.8.9.2), with the WD 12.3 Length, Unitize and MakeOrthoRep
arithmetic bit for bit. Arrays that do not fit, or an edge shared by more
than two triangles, leave the tessellation Compressed. Every triangle,
style and face range is charged to the element allowance first.
Stored-normals meshes, the one-status-per-triangle form, the planar bend
check, the best-fit search and partial salvage are later PRs.
Adapted from pdfcer (MIT, same author) at 0a249a73.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* prc: charge rebuilt compressed meshes after they fit; model_render test
The rebuilt triangles, their styles and the face ranges are now charged
to the element allowance once the arrays fit and before the face ranges
are made. Arrays the walk cannot fit stay Compressed and spend nothing,
so they no longer fail the file over a face count they never use.
Tests:
- a_rebuilt_compressed_mesh_is_charged_its_triangles_and_faces pins the
exact charge (4 triangles x 3 elements + 2 faces);
- arrays_that_do_not_fit_spend_no_triangles_or_faces;
- rebuilt_triangles_are_grouped_by_face_with_their_styles, a direct
by_face test with out-of-order faces, empty faces and styles;
- model_render's a_compressed_prc_model_is_rebuilt_and_drawn, end to end
from a PDF, cited in misc.3d-view; its hex fixture is the prc-render
builder's output, kept so by the_automation_fixture_is_the_compressed_triangle.
The walk's working tables are documented as bounded by the triangle
count rather than charged. The module docs now say the arithmetic
matches the WD pseudocode as read and that the traversal rules are
inferred, not WD text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* prc: charge compressed styles and face ranges before making them
A rebuilt compressed mesh's per-triangle styles were built before the
budget was charged, and also when the normals were stored and the mesh
was read past. The face attributes are now kept as read, and the styles
are built only for a rebuilt mesh, after the charge.
Each face range is a Range<usize>, two budget elements on 64-bit
targets, so it is charged at its size, not as one element.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* prc: bound the compressed rebuild walk by the allowance left
The walk's vertices, triangles, edge counts, edge log and stack were
allocated before anything was charged, and could hold several times the
decoded arrays. The walk now gets the elements the reader has left and
stops once its arrays, by length, would hold more; the tessellation then
stays compressed, as when its arrays do not fit the walk.
A face whose line attribute is 0 but whose behaviour is not now styles
its triangles, so the behaviour bits are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* prc: say the rebuild walk is bounded by the allowance
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* prc: cap the rebuild walk's first reservation by its limit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: KenM76 <KenM76>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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