axiolid_mesh_compile/deviation.rs
1//! How far the exact geometry may lie from a compiled mesh (#232).
2//!
3//! [`ReferenceMeshCompiler::compile_mesh_with_deviation`] returns, next to
4//! the mesh, a [`DeviationReport`]: a certified upper bound on the distance
5//! from any point of the exact surface the graph describes to the nearest
6//! mesh triangle, which paths contributed to it, and whether it is within
7//! the chord budget the caller asked for. This is the quantity #231 proves
8//! for revolutions, spheres, tori and sweeps along one arc; for the paths
9//! that have no proof by construction it is computed for the mesh at hand.
10//!
11//! # A bound, never an estimate
12//!
13//! Every number in a report is an upper bound: either a construction's
14//! proof ([`DeviationBound::Proven`]) or a computation that only uses
15//! certified derivative bounds and exact distances
16//! ([`DeviationBound::Certified`]). Where neither exists the path is named
17//! [`DeviationBound::Unbounded`] with its reason, and the report's overall
18//! bound is `None`; nothing is filled in with a sampled or heuristic value.
19//! The bounds are one-sided, exact surface to mesh: the bound limits how far
20//! the exact surface lies from the mesh; it does not detect extra material
21//! in the mesh (a cap the exact solid does not have) and does not change
22//! with it. A measured all-planar boolean is the exception (#329): it also
23//! reports how far its mesh lies from the exact result, under
24//! [`MESH_TO_EXACT`].
25//!
26//! # How each path is bounded
27//!
28//! - Authored meshes are their own exact surface: `0`. An authored polygon
29//! face within the linear tolerance of its plane counts as planar and
30//! adds nothing. A face warped beyond it has no unique surface (#254):
31//! it is triangulated in its fit plane and reported
32//! [`DeviationBound::Certified`] under the detail
33//! `"non-planar authored face"`, never `Proven`. The value is the width
34//! of the slab the face's corners (holes included) span about that fit
35//! plane, largest minus smallest signed distance (#261): every reading
36//! of the face that lies in that slab (either diagonal triangulation,
37//! the face flattened onto the plane, a bilinear patch) is within it of
38//! the mesh wherever it lies over the face, and the mesh within it of
39//! each reading that covers the face (`crate::planar` gives the
40//! argument); the worst face of the mesh is reported.
41//! - Profiles ([`axiolid_construct::profile::profile_deviation`]): the
42//! chord budget for every family the flattener certifies (lines, arcs,
43//! circles, ellipses, B-splines), plus any merged near-duplicate point,
44//! scaled by a derived profile's stretch. An extrusion's walls and caps
45//! are within that of the exact solid.
46//! - Revolutions: the profile's bound plus the half budget the turn gets
47//! (#231). Tapered revolutions only between straight-edged profiles,
48//! whose rings correspond exactly; tapered extrusions are unbounded
49//! (their twisted walls are not measured).
50//! - Swept disks: along lines, polylines (filleted, open or closed) and
51//! chains of segments and arcs by `axiolid_construct::pipe`'s proof
52//! (#232, #245, #248: a loop's closing twist is part of it), along one
53//! circular arc by #231's; a corner mitred beside an arc (#248) is
54//! certified by the branch and bound below, over the bend's band of
55//! torus up to the mitre plane and the planar face between the two
56//! tubes' sections (each trimmed by certain inequalities on its cells),
57//! and reported [`DeviationBound::Certified`] under the detail
58//! [`MITRE_BESIDE_AN_ARC`]; along an ellipse, a B-spline, or an
59//! elevated or banked curve without a grade break (#252, its derivative
60//! bounds in plan distance from `axiolid_reference::elevated`), certified
61//! against the exact tube (walls, bore and square end caps) by branch and
62//! bound, below, its centre over an intrinsic plan read from a certified
63//! position table whose error the bound carries (#393); an elevated curve
64//! whose grade breaks, or over a chain plan, or under an intrinsic
65//! profile, is unbounded by name. A straight plan under a constant grade
66//! is a segment.
67//! Fixed-reference sweeps along a segment, or along a circle whose plane
68//! the reference is normal to, by #231; other frame laws, polyline and
69//! composite directrices are unbounded here.
70//! - Primitives: spheres and tori by #231, cylinders and cones by #232
71//! (`axiolid_reference::primitive`), flat-faced ones exactly.
72//! - B-rep faces: planar faces with straight edges exactly; a face that
73//! declares no surface and whose corners lie off their fit plane beyond
74//! the linear tolerance (#257) as a warped authored face is, by its
75//! slab width, [`DeviationBound::Certified`] under the detail
76//! `"non-planar face without a surface"`; curved faces
77//! per triangle from the surface's second derivatives over the
78//! triangle, plus the trim lens of each pcurve chord (the B-rep
79//! tessellator's module notes give the argument).
80//!
81//! # Branch and bound against an exact parameterisation
82//!
83//! Where the mesh's vertices sit at no known surface parameters (a tube's
84//! stations stand on frames read off its sampled path, a boolean's cut
85//! curve wherever the mesh boolean put it), the exact surface
86//! `T` is covered by parameter cells. A cell with centre `x_c` and
87//! half-widths `h` maps into `T(x_c) + DT(x_c) [-h, h]` grown by
88//! `e2 = 1/2 (A h_x^2 + 2 B h_x h_y + C h_y^2)`, with `A, B, C` certified
89//! bounds on `T`'s second partials over the cell (for a tube, from the
90//! directrix's derivative bounds through its normalised frame). Distance
91//! to one triangle is convex, so over a piece of that parallelogram it is
92//! at most the largest of the piece's corners'; the best triangle per piece
93//! and the worst piece, plus `e2`, bound the cell. Cells are split worst
94//! first until the worst bound is within 10% of the largest exact distance
95//! sampled at a cell centre, or a work cap is reached; the reported value
96//! is always the worst bound over all cells. A tube (#252) settles once
97//! within the budget, joins its slightly twisted quads as flat regions up
98//! to a twentieth of the budget thick (that thickness is part of the
99//! bound), and cuts each cell the way that lowers its halves more, so a
100//! long tube's long, thin quads do not force every cell down to the width
101//! of their diagonals.
102//! - Surfaces trimmed by a parameter rectangle (#311): a uniform grid of
103//! cells whose corners are exact surface points, each cell within
104//! `1/8 ((A + B) h_u^2 + (C + B) h_v^2)` of its two triangles from the
105//! surface's certified interpolation bounds over the cell
106//! (`crate::trimmed`), [`DeviationBound::Certified`]; a cell a B-spline
107//! knot line of full multiplicity crosses is
108//! [`DeviationBound::Unbounded`] by name.
109//! - Instances scale the bound by an upper bound on their transform's
110//! largest stretch (`stretch`): the largest column length grown by
111//! `1 + 96 eps` when the columns are orthogonal, else the Frobenius
112//! norm. The margin keeps it above one even for an identity or a
113//! rotation, since an `f64` rotation is orthonormal only to rounding;
114//! the source is meshed to the budget shrunk by that factor, down to the
115//! largest value whose rounded product with it is within the budget
116//! (`shrunk_budget`, as #255 shrinks a curve-bounded plane's), so a
117//! rigid or scaled instance of a body meshed to exactly its budget
118//! reports at most the requested budget, nested instances included
119//! (#258). Uniform, non-uniform and sheared instances grow the bound by
120//! their full stretch and stay sound. The rounding of moving each vertex
121//! is added on top (#295, "Placement rounding" below). The rounding of
122//! computing a construction's own vertices is not part of any bound.
123//! Collections take the worst member.
124//! - Runs of sections at stations (#247): stationed spines and sectioned
125//! surfaces are bounded cell by cell as they are sampled
126//! (`crate::station`'s `bound` module): the vertex paths' chord bound
127//! from certified derivative bounds of the frame law, breakpoints at
128//! seams inside a cell, and the wall quad's
129//! `axiolid_construct::loft::quad_deviation`, plus a spine's profile
130//! flattening, [`DeviationBound::Certified`], a frame read by quadrature
131//! (an ellipse, a B-spline, an offset of a curve in closed form) bounded
132//! against its exact law (#316); a frame on an intrinsic curve, a chain or
133//! an offset beside one, a cell beside a mitre on a curved piece or with
134//! a changing section, and different curved profiles are
135//! [`DeviationBound::Unbounded`] by name.
136//! - Instances at stations (#264) move their source rigidly: on a line
137//! basis the frame is exact and the source's bound stands. On any other
138//! the frame is the exact curve's at a distance read by an estimate (the
139//! arc-length inverse, quadrature), and `crate::station::placement_accuracy`
140//! states how far the placement lies from the exact one (#405): on a
141//! circle, a polyline, an elevated or banked curve over a line or a
142//! circle, an intrinsic plan alone or under a closed-form profile and
143//! composites of them, a [`DeviationBound::Certified`] error `F` of any
144//! point of the source (`point_error + axis_error R`, `R` the largest
145//! sum `|x| + |y| + |z|` over the source mesh's vertices) that every contribution
146//! grows by as it grows by the placement's rounding, and that is named
147//! once under the path [`DeviationPath::StationPlacement`]; the source is
148//! re-meshed with room for the rounding and `F` when they would carry it
149//! over the budget. Any other basis (a frame read by quadrature, a chain,
150//! a basis itself placed inexactly, a station beside a seam it is not
151//! read on) is [`DeviationBound::Unbounded`] under that path, with the
152//! reason named.
153//! - Booleans (#235): cutting two meshes moves the intersection curve, and
154//! a one-sided bound on each operand does not bound it, so nothing is
155//! derived from the operands. Where `ReferenceExactCompiler` builds the
156//! exact result of the same node (booleans of placed extrusions and
157//! revolutions, #228, #376, #378, and half-space clips, #234; for the report also booleans with
158//! a mesh operand, any operator, below), the mesh is certified against that
159//! exact B-rep by the branch and bound above, over each face's trimmed
160//! parameter domain; a boolean it refuses is unbounded with the
161//! refusal's name. The bound is relative to the exact compiler's result,
162//! the exact boolean of operands moved by at most the tolerance (#228),
163//! not to the boolean of the unperturbed operands. It runs only for a
164//! report, and only for the booleans whose result is emitted, not for
165//! the inner booleans of a chain; the search stops once within the
166//! requested budget. When the exact result is all planar the other
167//! direction is measured too (#329): how far any point of the mesh lies
168//! from the exact result, against a witness mesh built on the exact
169//! faces, a separate [`DeviationPath::Boolean`] contribution under the
170//! detail [`MESH_TO_EXACT`], so material the mesh has and the exact
171//! result lacks (a strip of a clipped wall the mesh kept) shows in the
172//! report's bound. Both directions are carried through instances like
173//! any other contribution.
174//! - Operands snapped together (#276, #291): before the mesh boolean,
175//! operand coordinates a rounding residue (16 `f64::EPSILON` of the
176//! operands' largest coordinate) from the other operand's axis-aligned
177//! faces are set onto them exactly (`crate::snap`), so a skin that thin
178//! is not cut as one; a thicker skin is kept. The largest move is a
179//! [`DeviationBound::Certified`] contribution of
180//! [`DeviationPath::Boolean`] under the detail [`SNAPPED_OPERANDS`],
181//! carried through instances and enclosing booleans, the measured ones
182//! included; it never exceeds the tolerance. A boolean whose snapped
183//! operands are refused is cut as given and reports no snap.
184//! - Warped operands (#266): an operand that is an authored mesh, or a
185//! B-rep whose faces are all planes with straight edges or warped faces
186//! without a surface, is measured as the exact polyhedron of its
187//! triangle mesh: a warped face flattened to its triangles, within its
188//! slab width `w` of every reading of the face (`crate::planar`), both
189//! ways. That is how an operand's certified bound composes into a
190//! boolean's: not as a one-sided bound (above), but as a two-sided
191//! perturbation of the operand. The boolean's mesh is measured against
192//! the exact boolean of the flattened operands (ADR 0080: the exact
193//! boolean of operands perturbed within the reported deviation), by `d`,
194//! and its contribution is [`DeviationBound::Certified`] `d + w`, `w`
195//! the largest slab width of any operand flattened beneath it, under
196//! the detail "measured against the exact boolean of operands flattened
197//! within their slab width, plus it". The search settles within the
198//! budget less `w`, so a boolean whose slab widths fit the budget
199//! reports within it, and `meets_requested` follows. Each warped
200//! operand's own contribution ("non-planar authored face", "non-planar
201//! face without a surface") is carried as the snap is, so the report
202//! names it. Transversal to the cut, the boolean of the unflattened operands
203//! is within `d + w` of the mesh too; near tangency the guarantee is
204//! ADR 0080's, relative to the flattened operands. Where the exact
205//! boolean cannot take the flattened operand (a mesh that is not a
206//! closed two-manifold, a B-rep with a curved face, a tool that is
207//! itself a boolean, a configuration the general boolean refuses) the
208//! boolean is [`DeviationBound::Unbounded`] with that refusal's name,
209//! and the warp is still named. An unwarped mesh operand (`w = 0`) is
210//! measured like any exact operand, against the exact boolean of the
211//! operands as given when no decision within tolerance fired.
212//! - Operand faces read as one surface (#324): where the general boolean
213//! reads a face of one operand on a face of the other within tolerance
214//! (a niche flush with a curved wall's inner face, or a unit in the last
215//! place inside it), the furthest the reading moved an operand is a
216//! [`DeviationBound::Certified`] contribution of
217//! [`DeviationPath::Boolean`] under the detail [`COINCIDENT_OPERANDS`],
218//! next to the boolean's measured one. Like a snap's move, it names the
219//! perturbation the exact result stands for; it is not carried through
220//! enclosing booleans, whose exact result merges the reading's report.
221//! - Every reading within tolerance (#421): the furthest any kind of reading
222//! (merged points, contact, an incident point, coincident supports, ...)
223//! moved a feature is a [`DeviationBound::Certified`] contribution of
224//! [`DeviationPath::Boolean`] under [`TOLERANCE_READINGS`], and the
225//! report's rounding floor under [`BOOLEAN_ROUNDING_FLOOR`]. The
226//! boolean's own contribution is widened by their sum: the bound covers
227//! the boolean of the operands as read, each moved by at most the named
228//! readings, so they add to the measured distance rather than being the
229//! worst of it and them. A reading may change the topology against the
230//! operands as given; the named readings are what say so.
231//!
232//! # Placement rounding
233//!
234//! An `Instance` or an `InstanceAtStation` moves its source's vertices by
235//! applying its transform in `f64`, which rounds each coordinate by a few
236//! ulps of its magnitude: an *absolute* error, about `1e-9` at
237//! georeferenced coordinates of `6e6`, that no relative factor covers
238//! (#295). Every reported bound therefore carries, for each transform
239//! applied, the term `r = 4 eps M` ([`PLACEMENT_EPSILONS`],
240//! `placement_rounding`): `M` is the largest magnitude a placed coordinate
241//! sums, `sum_j |a_ij| |p_j| + |t_i|` over the source's vertices `p`
242//! (the coordinate's own magnitude where nothing cancels), and the
243//! constant covers `sqrt 3 gamma_4 < 3.47 eps`: four rounded operations
244//! per coordinate, three coordinates per point. A source bound `d` placed
245//! with stretch `s` reports `s d + r`, its kind kept; a nested instance
246//! grows the inner `r` by the outer stretch and adds its own. The term is
247//! named under [`DeviationPath::Placement`] / [`PLACEMENT_ROUNDING`]. A
248//! row that copies a coordinate (one coefficient `+-1`, no translation)
249//! rounds nothing, so an identity or a mirror adds nothing.
250//!
251//! The source's budget leaves room for it: the source is meshed to the
252//! budget less a room, shrunk by the stretch, so `s b + room` rounds to
253//! within the budget (`shrunk_budget_within`). The room starts at the
254//! floor `r` reaches at the translation's scale (the term at the origin)
255//! plus a `2^-20` share of what is left, for the source's own
256//! coordinates; where the source's mesh then rounds by more than that and
257//! its bound would carry the instance over the budget, it is meshed once
258//! more with room for an eighth more than it showed. A budget at or below
259//! the floor at the translation's scale, or below the rounding of the
260//! placed vertices themselves, is refused with
261//! [`BUDGET_BELOW_PLACEMENT_ROUNDING`] rather than reported as met.
262//!
263//! [`ReferenceMeshCompiler::compile_mesh_with_deviation`]: crate::ReferenceMeshCompiler::compile_mesh_with_deviation
264
265use axiolid_core::{Point3, Scalar, Transform3};
266
267mod boolean;
268mod mitre;
269mod paths;
270pub(crate) use boolean::{
271 emitted_booleans, extra_shells, geometric_components, of_boolean, with_readings,
272};
273pub(crate) use paths::{
274 of_curve_bounded, of_primitive, of_sectioned_surface, of_solid, stationed_spine,
275};
276
277/// The [`DeviationContribution::detail`] of authored polygon faces warped
278/// beyond the linear tolerance (#254).
279pub(crate) const WARPED_AUTHORED_FACE: &str = "non-planar authored face";
280
281/// The [`DeviationContribution::detail`] of a keyhole face, authored or a
282/// B-rep's, whose Steiner points lie off an axis plane (#283): a face
283/// takes them so that its seam is no triangle edge, each a chain of
284/// midpoints of its corners, so within rounding of the face. The bound is
285/// the slab width of [`crate::planar::steiner_offset`]. A face in an axis
286/// plane holds its Steiner points exactly and reports nothing.
287pub(crate) const KEYHOLE_STEINER_POINT: &str =
288 "point added inside a keyhole face, off an axis plane";
289
290/// The [`DeviationContribution::detail`] of the corners a swept disk mitres
291/// beside an arc (#248): the bend's band and the planar face between the
292/// two tubes' sections, certified against the mesh by branch and bound;
293/// the rest of the pipe stays proven under its own detail.
294pub const MITRE_BESIDE_AN_ARC: &str = "mitre beside an arc";
295
296/// The [`DeviationContribution::detail`] of a node placed at a station
297/// whose frame is read at a numerically resolved distance (#264): the
298/// frame's certified error (#405), or why it has none.
299pub(crate) const NUMERICAL_STATION_FRAME: &str = "frame at a numerically resolved station";
300
301/// The [`DeviationContribution::detail`] of B-rep faces that declare no
302/// surface and are warped beyond the linear tolerance (#257).
303pub(crate) const WARPED_BREP_FACE: &str = "non-planar face without a surface";
304
305/// The [`DeviationContribution::detail`] of a boolean whose operands were
306/// moved onto each other's faces by a rounding residue, within the linear
307/// tolerance, before the mesh boolean cut them (#276, #291): its bound is
308/// the largest distance any operand vertex moved, so the mesh is the
309/// boolean of operands perturbed by at most that much. It travels with the boolean's result through
310/// instances (scaled, plus the placement's rounding, #295) and enclosing booleans, also where an enclosing
311/// boolean is measured against its exact result.
312pub const SNAPPED_OPERANDS: &str = "operands snapped onto each other within tolerance";
313
314/// The [`DeviationContribution::detail`] of a boolean measured against an
315/// exact result in which faces of the two operands were read as one
316/// surface within tolerance (#324: a niche's inner cylinder a unit in the
317/// last place off a curved wall's inner face): its bound is the furthest
318/// that reading moved an operand, so the exact result the mesh is measured
319/// against is the boolean of operands perturbed by at most that much
320/// (ADR 0080). It names the perturbation, as [`SNAPPED_OPERANDS`] does;
321/// the boolean's own contribution bounds the mesh's distance to that
322/// result.
323pub const COINCIDENT_OPERANDS: &str = "operand faces read as one surface within tolerance";
324
325/// The [`DeviationContribution::detail`] of a boolean measured against an
326/// exact result whose report holds readings within tolerance (#421): its
327/// bound is the largest distance any kind of reading moved a feature
328/// (`BooleanReport::linear`: merged points, an incident point, coincident
329/// supports, ..., or an angle turned, as a distance over the operands'
330/// extent), so the exact result the mesh is measured against is the boolean
331/// of operands perturbed by at most that much (a reading may change the
332/// topology against the operands as given: this contribution is what says
333/// so). The boolean's own bound is widened by it. [`COINCIDENT_OPERANDS`] names the coincident supports'
334/// share.
335pub const TOLERANCE_READINGS: &str = "operands moved by readings within tolerance";
336
337/// The [`DeviationContribution::detail`] of a boolean measured against an
338/// exact result: the rounding floor of its report (#244, #421), within
339/// which constructed points were read as one without a decision. Present
340/// whenever the floor is not zero, exact result or not. The boolean's own
341/// bound is widened by it.
342pub const BOOLEAN_ROUNDING_FLOOR: &str = "constructed points read as one within the rounding floor";
343
344/// The [`DeviationContribution::detail`] of a measured boolean's other
345/// direction (#329): a certified bound on how far any point of the mesh
346/// lies from the exact compiler's result (the same result as the boolean's
347/// own contribution), widened like it by the flattening, the readings and
348/// the rounding floor. Only for an all-planar exact result.
349pub const MESH_TO_EXACT: &str =
350 "mesh to the exact compiler's result: how far any point of the mesh lies from it";
351
352/// The [`DeviationContribution::detail`] of a boolean whose exact result is
353/// empty and whose mesh is too (#326): nothing is left in either, so the
354/// distance between them is certified `0`, widened like any bound by the
355/// readings and the rounding floor of the exact result's report. The
356/// bound is unbounded by name when the empty result is that of operands
357/// flattened within a warped slab width. The exact compiler holds the
358/// empty result internally and refuses it only as a root, so the report
359/// cannot be measured against a B-rep; an empty exact result against a
360/// mesh with triangles, or the reverse, is unbounded by name.
361pub const EMPTY_RESULT: &str = "the exact compiler's result is empty, and so is the mesh";
362
363/// The [`DeviationContribution::detail`] of a union the mesh cut could not
364/// settle and the exact compiler decided (#391, ADR 0098): the mesh is
365/// the exact compiler's union tessellated, and the exact union is that of
366/// operands read as in contact within the tolerance
367/// ([`COINCIDENT_OPERANDS`] names how far). The bound is the
368/// tessellation's: how far the exact union's surface lies from the mesh.
369pub const REFEREED_UNION: &str =
370 "tessellated from the exact compiler's union, operands within tolerance";
371
372/// [`REFEREED_UNION`] when no decision within the tolerance fired: the mesh
373/// is the tessellation of the exact union of the operands as given.
374pub const REFEREED_UNION_EXACT: &str =
375 "tessellated from the exact compiler's union, the exact union of the given operands";
376
377/// [`REFEREED_UNION`] for a difference whose mesh held a void the exact
378/// result lacks, bounded by skins within rounding where the cutter met its
379/// host, and which the exact compiler decided (#423, ADR 0098). A
380/// difference with no extra shell, or whose faces the exact compiler read
381/// further than rounding, is never refereed: a skin or void thinner than
382/// the tolerance stays as authored (ADR 0096, D7).
383pub const REFEREED_DIFFERENCE: &str =
384 "tessellated from the exact compiler's difference, operands within tolerance";
385
386/// [`REFEREED_DIFFERENCE`] when no decision within the tolerance fired.
387pub const REFEREED_DIFFERENCE_EXACT: &str =
388 "tessellated from the exact compiler's difference, the exact difference of the given operands";
389
390/// [`REFEREED_DIFFERENCE`] for an intersection, on the same trigger: its
391/// mesh held a shell the exact result lacks (#423, ADR 0098).
392pub const REFEREED_INTERSECTION: &str =
393 "tessellated from the exact compiler's intersection, operands within tolerance";
394
395/// [`REFEREED_INTERSECTION`] when no decision within the tolerance fired.
396pub const REFEREED_INTERSECTION_EXACT: &str =
397 "tessellated from the exact compiler's intersection, the exact intersection of the given operands";
398
399/// The [`DeviationContribution::detail`] of [`DeviationPath::Placement`]:
400/// the rounding of applying an instance's transform to its source's
401/// vertices in `f64` (#295), [`PLACEMENT_EPSILONS`] `f64::EPSILON` of the
402/// largest coordinate magnitude the placement sums (see the module's
403/// "Placement rounding"). Every other contribution of the instance is
404/// grown by it too.
405pub const PLACEMENT_ROUNDING: &str = "rounding of applying the transform in f64";
406
407/// The input family a compilation is refused with when its chord budget,
408/// in an instance's frame, is at or below the rounding of placing the
409/// instance's vertices (#295): at the translation's scale before the
410/// source is meshed, at the placed vertices' scale after. No mesh placed
411/// in `f64` there is within the budget, so none is reported as meeting it.
412pub const BUDGET_BELOW_PLACEMENT_ROUNDING: &str =
413 "chord budget at or below the rounding of placing an instance's vertices in f64";
414
415/// The construction path a part of the mesh came from.
416#[non_exhaustive]
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
418pub enum DeviationPath {
419 /// An authored triangle or polygon mesh.
420 AuthoredMesh,
421 /// A linear extrusion of a profile.
422 Extrusion,
423 /// A revolution of a profile.
424 Revolution,
425 /// An extrusion between two profiles.
426 TaperedExtrusion,
427 /// A revolution between two profiles.
428 TaperedRevolution,
429 /// A disk swept along a directrix.
430 SweptDisk,
431 /// A profile swept with a fixed reference direction.
432 FixedReferenceSweep,
433 /// A profile swept along a curve on a surface.
434 SurfaceCurveSweep,
435 /// Sections placed along a spine.
436 SectionedSpine,
437 /// A half-space bounded by a polygon or a profile.
438 BoundedHalfSpace,
439 /// A CSG primitive.
440 Primitive,
441 /// A face of a B-rep.
442 BRepFace,
443 /// A plane trimmed by boundary curves.
444 CurveBoundedPlane,
445 /// A boolean of two meshes, measured against its exact result (#235).
446 ///
447 /// An operand's own bound does not compose into it as a one-sided
448 /// bound: a boolean is measured, not derived. A warped operand (#266)
449 /// enters as a two-sided perturbation: the exact result is the boolean
450 /// of the operands flattened to their triangles, each within its slab
451 /// width `w` of every reading, and the contribution is the measured
452 /// distance plus the largest `w` (see the module's "Warped operands").
453 ///
454 /// A measured boolean whose exact result is all planar has two
455 /// contributions under this path (#329): the exact result to the mesh,
456 /// and the mesh to the exact result ([`MESH_TO_EXACT`]), so extra
457 /// material in the mesh is reported.
458 Boolean,
459 /// A path that does not report its deviation.
460 Unreported,
461 /// Sections standing at stations along a directrix (#241).
462 StationedSpine,
463 /// Open sections at stations joined by tag (#241).
464 SectionedSurface,
465 /// A node placed in the frame of a curve station (#264), on a basis
466 /// whose frame is not exact: [`DeviationBound::Certified`] by how far
467 /// the placement may lie from the exact one (#405), or
468 /// [`DeviationBound::Unbounded`] by name.
469 StationPlacement,
470 /// A surface trimmed by a parameter rectangle (#311).
471 TrimmedSurface,
472 /// The rounding of moving an instance's vertices with its `f64`
473 /// transform (#295), an `Instance` or an `InstanceAtStation`: an
474 /// absolute term every other contribution of the instance also
475 /// carries, named under [`PLACEMENT_ROUNDING`] so the report shows it.
476 Placement,
477 /// A surface ruled between two sides (#402).
478 RuledSurface,
479}
480
481/// A bound on how far the exact surface may lie from the mesh.
482#[non_exhaustive]
483#[derive(Debug, Clone, Copy, PartialEq)]
484pub enum DeviationBound {
485 /// Guaranteed by the construction's own proof, without inspecting the
486 /// mesh (#231): typically the chord budget itself.
487 Proven(Scalar),
488 /// Computed for this mesh from certified derivative bounds and exact
489 /// distances (#232).
490 Certified(Scalar),
491 /// No certified bound exists for this path; the reason names it.
492 Unbounded(&'static str),
493}
494
495impl DeviationBound {
496 /// The bound's value, `None` when unbounded.
497 #[must_use]
498 pub const fn value(self) -> Option<Scalar> {
499 match self {
500 Self::Proven(value) | Self::Certified(value) => Some(value),
501 Self::Unbounded(_) => None,
502 }
503 }
504
505 /// The worse of two bounds: unbounded wins, else the larger value
506 /// (keeping its kind).
507 #[must_use]
508 pub fn worst(self, other: Self) -> Self {
509 match (self.value(), other.value()) {
510 (None, _) => self,
511 (_, None) => other,
512 (Some(a), Some(b)) => {
513 if b > a {
514 other
515 } else {
516 self
517 }
518 }
519 }
520 }
521
522 /// The bound after a placement: grown by the stretch `factor`, plus the
523 /// placement's absolute `rounding` ([`grown`]), keeping its kind.
524 fn placed(self, factor: Scalar, rounding: Scalar) -> Self {
525 match self {
526 Self::Proven(value) => Self::Proven(grown(value, factor, rounding)),
527 Self::Certified(value) => Self::Certified(grown(value, factor, rounding)),
528 unbounded => unbounded,
529 }
530 }
531}
532
533/// One path's share of a report.
534#[non_exhaustive]
535#[derive(Debug, Clone, Copy, PartialEq)]
536pub struct DeviationContribution {
537 /// The construction path.
538 pub path: DeviationPath,
539 /// What within the path: a surface family, a directrix family, a
540 /// primitive kind. Empty when the path says it all.
541 pub detail: &'static str,
542 /// The worst bound over every part of the mesh this path made.
543 pub bound: DeviationBound,
544}
545
546/// A certified bound on the distance from the exact surface to a compiled
547/// mesh (#232).
548///
549/// The bound is one-sided: it limits how far the exact surface lies from
550/// the mesh, and does not detect extra material in the mesh. Only a
551/// measured all-planar boolean also bounds the mesh's distance to the exact
552/// result ([`MESH_TO_EXACT`], #329).
553#[non_exhaustive]
554#[derive(Debug, Clone, PartialEq)]
555pub struct DeviationReport {
556 /// The chord budget the compilation was asked for, in world units:
557 /// `ExecutionOptions::chord_error`, else the linear tolerance.
558 pub requested: Scalar,
559 /// An upper bound on the distance from every point of the exact
560 /// surface to the mesh's triangles, in world units; `None` when any
561 /// contribution is unbounded. It does not bound material the mesh has
562 /// beyond the exact surface, except for a measured all-planar
563 /// boolean's ([`MESH_TO_EXACT`]).
564 pub bound: Option<Scalar>,
565 /// Each path that contributed, with the worst bound it reached, in
566 /// first-seen order.
567 pub contributions: Vec<DeviationContribution>,
568}
569
570impl DeviationReport {
571 /// Whether the mesh is certified within the requested chord budget.
572 ///
573 /// `false` means "not certified", not "missed": an unbounded path, or a
574 /// bound above the budget whose true deviation may still be below it.
575 /// `true` is about the exact surface's distance to the mesh: a mesh
576 /// with extra material can still meet the request, except where a
577 /// measured all-planar boolean bounds that too ([`MESH_TO_EXACT`]).
578 #[must_use]
579 pub fn meets_requested(&self) -> bool {
580 self.bound.is_some_and(|bound| bound <= self.requested)
581 }
582
583 /// The contributions with no certified bound.
584 pub fn unbounded(&self) -> impl Iterator<Item = &DeviationContribution> {
585 self.contributions
586 .iter()
587 .filter(|c| matches!(c.bound, DeviationBound::Unbounded(_)))
588 }
589}
590
591/// A node's deviation while the graph is compiled: its contributions,
592/// merged by path and detail.
593#[derive(Debug, Clone, PartialEq)]
594pub(crate) struct Deviation(Vec<DeviationContribution>);
595
596impl Default for Deviation {
597 /// Unreported: a path that never states its deviation must not read as
598 /// exact.
599 fn default() -> Self {
600 Self::one(
601 DeviationPath::Unreported,
602 "",
603 DeviationBound::Unbounded("this path does not report its deviation"),
604 )
605 }
606}
607
608impl Deviation {
609 pub(crate) fn one(path: DeviationPath, detail: &'static str, bound: DeviationBound) -> Self {
610 Self(vec![DeviationContribution {
611 path,
612 detail,
613 bound,
614 }])
615 }
616
617 /// No contribution yet; [`Self::add`] fills it.
618 pub(crate) const fn empty() -> Self {
619 Self(Vec::new())
620 }
621
622 /// Fold one contribution in, keeping the worst per path and detail.
623 pub(crate) fn add(&mut self, path: DeviationPath, detail: &'static str, bound: DeviationBound) {
624 match self
625 .0
626 .iter_mut()
627 .find(|c| c.path == path && c.detail == detail)
628 {
629 Some(existing) => existing.bound = existing.bound.worst(bound),
630 None => self.0.push(DeviationContribution {
631 path,
632 detail,
633 bound,
634 }),
635 }
636 }
637
638 /// Fold another node's contributions in.
639 pub(crate) fn absorb(&mut self, other: &Self) {
640 for c in &other.0 {
641 self.add(c.path, c.detail, c.bound);
642 }
643 }
644
645 /// Fold in `other`'s operand perturbations: its [`SNAPPED_OPERANDS`]
646 /// contributions (#276) and its warped faces' slab widths
647 /// ([`WARPED_AUTHORED_FACE`], [`WARPED_BREP_FACE`], #266), and the slab
648 /// widths of keyhole faces' Steiner points off an axis plane
649 /// ([`KEYHOLE_STEINER_POINT`], #283). They stay
650 /// reported when a boolean replaces the rest of an operand's
651 /// deviation, so the report names every operand that was moved or
652 /// flattened, through any chain of booleans.
653 pub(crate) fn carry_perturbations(&mut self, other: &Self) {
654 for c in &other.0 {
655 let carried = matches!(
656 (c.path, c.detail),
657 (DeviationPath::Boolean, SNAPPED_OPERANDS)
658 | (DeviationPath::AuthoredMesh, WARPED_AUTHORED_FACE)
659 | (DeviationPath::BRepFace, WARPED_BREP_FACE)
660 | (DeviationPath::AuthoredMesh, KEYHOLE_STEINER_POINT)
661 | (DeviationPath::BRepFace, KEYHOLE_STEINER_POINT)
662 );
663 if carried {
664 self.add(c.path, c.detail, c.bound);
665 }
666 }
667 }
668
669 /// The largest slab width of a B-rep's warped faces when its mesh is
670 /// the polyhedron it describes up to them (#266): every contribution
671 /// an exact plane with straight edges, a warped face without a
672 /// surface, or a keyhole face's Steiner points off an axis plane,
673 /// whose bound is a slab width too (#283). `None` when any face is
674 /// curved, chorded or off its plane.
675 pub(crate) fn polyhedral_warp(&self) -> Option<Scalar> {
676 self.0
677 .iter()
678 .try_fold(0.0, |widest: Scalar, c| match (c.path, c.detail, c.bound) {
679 (DeviationPath::BRepFace, "plane", DeviationBound::Proven(0.0)) => Some(widest),
680 (
681 DeviationPath::BRepFace,
682 WARPED_BREP_FACE | KEYHOLE_STEINER_POINT,
683 DeviationBound::Certified(width),
684 ) => Some(widest.max(width)),
685 _ => None,
686 })
687 }
688
689 /// The same contributions after a transform whose application rounded
690 /// each vertex by at most `rounding` ([`placement_rounding`], #295):
691 /// lengths grow by at most its largest stretch, and every bound by the
692 /// rounding on top, which is also named under [`PLACEMENT_ROUNDING`]
693 /// when it is not zero.
694 pub(crate) fn transformed(&self, transform: Transform3, rounding: Scalar) -> Self {
695 self.moved(transform, rounding, 0.0)
696 }
697
698 /// [`Self::transformed`] for a transform that places the source at a
699 /// station whose frame lies within `frame` of the exact one over the
700 /// whole source (#405): every contribution grows by it as it grows by
701 /// the rounding, and it is named once under
702 /// [`DeviationPath::StationPlacement`].
703 pub(crate) fn transformed_within(
704 &self,
705 transform: Transform3,
706 rounding: Scalar,
707 frame: Scalar,
708 ) -> Self {
709 let mut placed = self.moved(transform, rounding, frame);
710 placed.add(
711 DeviationPath::StationPlacement,
712 NUMERICAL_STATION_FRAME,
713 DeviationBound::Certified(frame),
714 );
715 placed
716 }
717
718 /// Every contribution after the transform: grown by its stretch, and
719 /// by `rounding + frame` on top; the rounding named under
720 /// [`PLACEMENT_ROUNDING`] when it is not zero.
721 fn moved(&self, transform: Transform3, rounding: Scalar, frame: Scalar) -> Self {
722 let factor = stretch(transform);
723 let mut placed = Self(
724 self.0
725 .iter()
726 .map(|c| DeviationContribution {
727 bound: c.bound.placed(factor, rounding + frame),
728 ..*c
729 })
730 .collect(),
731 );
732 if rounding > 0.0 {
733 placed.add(
734 DeviationPath::Placement,
735 PLACEMENT_ROUNDING,
736 DeviationBound::Certified(rounding),
737 );
738 }
739 placed
740 }
741
742 /// The worst contribution's bound ([`DeviationBound::worst`]): unbounded
743 /// wins, so a reason is never dropped. Unbounded when there is none.
744 pub(crate) fn worst_bound(&self) -> DeviationBound {
745 self.0
746 .iter()
747 .map(|c| c.bound)
748 .reduce(DeviationBound::worst)
749 .unwrap_or(DeviationBound::Unbounded(
750 "this path does not report its deviation",
751 ))
752 }
753
754 /// The worst bound over every contribution, `None` when any is
755 /// unbounded or there is none.
756 pub(crate) fn bound(&self) -> Option<Scalar> {
757 if self.0.is_empty() {
758 return None;
759 }
760 self.0.iter().try_fold(0.0, |worst: Scalar, c| {
761 c.bound.value().map(|value| worst.max(value))
762 })
763 }
764
765 /// The worst certified bound above `budget`, if any: what a mesh
766 /// rebuilt finer could still bring within it. A proven bound is the
767 /// construction's own and does not move with a finer mesh.
768 pub(crate) fn certified_over(&self, budget: Scalar) -> Option<Scalar> {
769 self.0
770 .iter()
771 .filter_map(|c| match c.bound {
772 DeviationBound::Certified(value) if value > budget => Some(value),
773 _ => None,
774 })
775 .reduce(Scalar::max)
776 }
777
778 pub(crate) fn report(&self, requested: Scalar) -> DeviationReport {
779 DeviationReport {
780 requested,
781 bound: self.bound(),
782 contributions: self.0.clone(),
783 }
784 }
785}
786
787/// An upper bound on the largest factor a transform's linear part stretches
788/// a length by: the largest column length when the columns are orthogonal,
789/// else the Frobenius norm. The same factor the compiler shrinks a chord
790/// budget by into an instance's local space ([`shrunk_budget`]).
791///
792/// The `1 + 96 eps` margin on the orthogonal branch covers what the column
793/// lengths do not: columns orthogonal only to `32 eps` (an `f64` rotation
794/// matrix is never exactly orthonormal) raise the largest singular value
795/// above the largest column length by at most about `32 eps` of it, and
796/// each computed length is itself rounded. It is why a rigid transform's
797/// factor is a hair above one, never exactly one: no `f64` rotation can be
798/// shown to stretch nothing, so none is read as stretch 1 (#258). The
799/// instance's source is instead meshed to a budget shrunk by this factor,
800/// so the grown bound still fits the request.
801pub(crate) fn stretch(transform: Transform3) -> Scalar {
802 let m = transform.matrix3;
803 let (sx, sy, sz) = (m.x_axis.length(), m.y_axis.length(), m.z_axis.length());
804 let eps = 32.0 * f64::EPSILON;
805 let orthogonal = m.x_axis.dot(m.y_axis).abs() <= eps * sx * sy
806 && m.x_axis.dot(m.z_axis).abs() <= eps * sx * sz
807 && m.y_axis.dot(m.z_axis).abs() <= eps * sy * sz;
808 if orthogonal {
809 sx.max(sy).max(sz) * (1.0 + 3.0 * eps)
810 } else {
811 (sx * sx + sy * sy + sz * sz).sqrt()
812 }
813}
814
815/// A bound `value` after a placement that stretches by at most `factor`
816/// and rounds each vertex by at most `rounding` (#295), rounded as
817/// [`Deviation::transformed`] rounds it: every point of a source triangle
818/// moves with its corners' barycentric combination, so a vertex rounding
819/// of `r` moves no point of the mesh further than `r`.
820pub(crate) fn grown(value: Scalar, factor: Scalar, rounding: Scalar) -> Scalar {
821 value * factor + rounding
822}
823
824/// How many `f64::EPSILON` of the largest summed coordinate magnitude
825/// bound the rounding of applying a transform to a point (#295): a
826/// coordinate sums four terms, each rounded at most four times, so it is
827/// off by at most `gamma_4 < 2.000_001 eps` of its summed magnitude, and a
828/// point by `sqrt 3` of that, below `3.47 eps`; `4` also covers computing
829/// the magnitude itself in `f64` (see the module's "Placement rounding").
830pub const PLACEMENT_EPSILONS: Scalar = 4.0;
831
832/// An upper bound on how far applying `transform` to `positions` in `f64`
833/// moves any of them from its exact image (#295), the absolute term
834/// [`Deviation::transformed`] adds to an instance's bounds.
835///
836/// Each image coordinate `q_i = a_i1 p_1 + a_i2 p_2 + a_i3 p_3 + t_i` is
837/// four terms, each rounded at most four times (one product, at most three
838/// sums, in any order, with or without a fused multiply-add), so it is off
839/// by at most `gamma_4 S_i`, `S_i = sum_j |a_ij| |p_j| + |t_i|` and
840/// `gamma_4 = 4u / (1 - 4u) < 2.000_001 eps` (`u = eps / 2`). Over three
841/// coordinates the point is off by at most `sqrt 3 gamma_4 max S_i <
842/// 3.47 eps max S_i`, and `S_i` computed in `f64` is low by at most a
843/// factor `1 + gamma_4`, so [`PLACEMENT_EPSILONS`] `eps M`, `M` the
844/// largest computed `S_i` over every position, covers it with room (and
845/// multiplying by `4 eps`, a power of two, is exact). `S_i` is the
846/// coordinate's own magnitude where nothing cancels, so the term is
847/// `4 eps max|coordinate|`: about `5.3e-9` at `6e6`. A row that copies one
848/// coordinate (a single coefficient `+-1` and no translation) moves
849/// nothing and adds nothing, so an identity or a mirror is exact.
850///
851/// At the origin (`positions = [0]`) it is the floor every placement by
852/// `transform` reaches at least: `4 eps` times its largest translation
853/// component in a row that rounds.
854pub(crate) fn placement_rounding(positions: &[Point3], transform: Transform3) -> Scalar {
855 let m = transform.matrix3;
856 let t = transform.translation;
857 let rows = [
858 ([m.x_axis.x, m.y_axis.x, m.z_axis.x], t.x),
859 ([m.x_axis.y, m.y_axis.y, m.z_axis.y], t.y),
860 ([m.x_axis.z, m.y_axis.z, m.z_axis.z], t.z),
861 ];
862 let mut largest: Scalar = 0.0;
863 for (coefficients, offset) in rows {
864 let nonzero = coefficients.iter().filter(|&&a| a != 0.0).count();
865 let copies = offset == 0.0
866 && nonzero <= 1
867 && coefficients.iter().all(|&a| a == 0.0 || a.abs() == 1.0);
868 if copies {
869 continue;
870 }
871 let a = coefficients.map(Scalar::abs);
872 for p in positions {
873 let sum = a[0] * p.x.abs() + a[1] * p.y.abs() + a[2] * p.z.abs() + offset.abs();
874 if sum.is_nan() {
875 return Scalar::INFINITY;
876 }
877 largest = largest.max(sum);
878 }
879 }
880 PLACEMENT_EPSILONS * Scalar::EPSILON * largest
881}
882
883/// The largest budget `b` from `budget / stretch` down whose product with
884/// `stretch`, rounded as [`Deviation::transformed`] rounds it, is within
885/// `budget` (#255, #258).
886///
887/// `budget / stretch` alone is not enough: at some factors (a scale of
888/// 0.33 or 1.19 at a budget of `1e-4`) the quotient multiplied back
889/// rounds one ulp above `budget`, and a source meshed to exactly its local
890/// budget would report just over the request. Rounded multiplication is
891/// monotone, so any source bound `d <= b` then reports `d * stretch <=
892/// budget`, and nested instances keep that through every level.
893/// `budget` itself when `stretch` is not a positive finite factor (the
894/// callers refuse those first), and the plain quotient when no positive
895/// value fits (a quotient in the subnormal range, which no mesh reaches).
896pub(crate) fn shrunk_budget(budget: Scalar, stretch: Scalar) -> Scalar {
897 shrunk_budget_within(budget, stretch, 0.0)
898}
899
900/// [`shrunk_budget`] leaving `room` for a placement's rounding (#295): the
901/// largest `b` from `(budget - room) / stretch` down with
902/// [`grown`]`(b, stretch, room) <= budget`, so a source bound `d <= b`
903/// placed with a rounding `r <= room` reports within `budget` (rounded
904/// addition and multiplication are monotone). The callers refuse a `room`
905/// at or above `budget` first ([`BUDGET_BELOW_PLACEMENT_ROUNDING`]).
906pub(crate) fn shrunk_budget_within(budget: Scalar, stretch: Scalar, room: Scalar) -> Scalar {
907 if !(stretch.is_finite() && stretch > 0.0) {
908 return budget;
909 }
910 let mut shrunk = (budget - room) / stretch;
911 while shrunk > 0.0 && grown(shrunk, stretch, room) > budget {
912 shrunk = shrunk.next_down();
913 }
914 if shrunk > 0.0 {
915 shrunk
916 } else {
917 (budget - room) / stretch
918 }
919}
920
921#[cfg(test)]
922mod tests {
923 use axiolid_core::{Mat3, Vec3};
924
925 use super::*;
926
927 /// Columns orthogonal only to `30 eps` pass as orthogonal, and the
928 /// largest singular value then exceeds the largest column length: the
929 /// margin must cover it (#258).
930 #[test]
931 fn the_stretch_covers_columns_orthogonal_only_to_rounding() {
932 let skew = 30.0 * f64::EPSILON;
933 let m = Mat3::from_cols(Vec3::X, Vec3::new(skew, 1.0, 0.0), Vec3::Z);
934 // Both columns have length one in f64; the largest singular value
935 // of [[1, skew], [0, 1]] is above 1 + skew / 2.
936 assert_eq!(m.y_axis.length(), 1.0);
937 let factor = stretch(Transform3::from_mat3(m));
938 assert!(factor >= 1.0 + 0.5 * skew, "{factor}");
939 assert!(factor < 1.0 + 200.0 * f64::EPSILON, "{factor}");
940 assert!(stretch(Transform3::IDENTITY) > 1.0);
941 }
942
943 /// The placement's rounding is `4 eps` of the largest summed magnitude
944 /// of a row that rounds, nothing for a row that copies a coordinate,
945 /// and covers the rounding `transform_point3` actually makes (#295),
946 /// measured against an error-free evaluation of the same sums.
947 #[test]
948 fn the_placement_rounding_covers_applying_the_transform() {
949 let eps = Scalar::EPSILON;
950 let site = Transform3::from_translation(Vec3::new(6e6, -6e6, 100.0));
951 assert_eq!(placement_rounding(&[Point3::ZERO], site), 4.0 * eps * 6e6);
952 let p = Point3::new(0.25, -0.5, 2.0);
953 assert_eq!(placement_rounding(&[p], site), 4.0 * eps * (6e6 + 0.5));
954 let mirror = Transform3::from_scale(Vec3::new(-1.0, 1.0, 1.0));
955 assert_eq!(placement_rounding(&[p], Transform3::IDENTITY), 0.0);
956 assert_eq!(placement_rounding(&[p], mirror), 0.0);
957 // A turn about z rounds x and y, not z.
958 let turn = Transform3::from_rotation_z(0.3);
959 let (c, s) = (0.3_f64.cos().abs(), 0.3_f64.sin().abs());
960 let sums = [c * 0.25 + s * 0.5, s * 0.25 + c * 0.5];
961 let largest = sums[0].max(sums[1]);
962 let r = placement_rounding(&[p], turn);
963 assert!((r - 4.0 * eps * largest).abs() <= 4.0 * eps * largest * 4.0 * eps);
964
965 // The actual rounding, from exact products (fused) and exact sums.
966 let exact = |t: Transform3, p: Point3| -> [Scalar; 3] {
967 let m = t.matrix3;
968 let rows = [
969 ([m.x_axis.x, m.y_axis.x, m.z_axis.x], t.translation.x),
970 ([m.x_axis.y, m.y_axis.y, m.z_axis.y], t.translation.y),
971 ([m.x_axis.z, m.y_axis.z, m.z_axis.z], t.translation.z),
972 ];
973 rows.map(|(a, offset)| {
974 // Each term as a high and a low part, summed from the
975 // smallest magnitude up in a running double-double.
976 let mut parts = Vec::new();
977 for (coefficient, x) in a.into_iter().zip([p.x, p.y, p.z]) {
978 let high = coefficient * x;
979 parts.push(high);
980 parts.push(coefficient.mul_add(x, -high));
981 }
982 parts.push(offset);
983 parts.sort_by(|u, v| u.abs().total_cmp(&v.abs()));
984 let (mut high, mut low) = (0.0, 0.0);
985 for part in parts {
986 let sum = high + part;
987 let bb = sum - high;
988 low += (high - (sum - bb)) + (part - bb);
989 high = sum;
990 }
991 high + low
992 })
993 };
994 let placements = [
995 site,
996 Transform3::from_translation(Vec3::new(5.61e6, 3.3e5, -71.25))
997 * Transform3::from_rotation_z(1.1),
998 Transform3::from_axis_angle(Vec3::new(1.0, 2.0, 3.0).normalize(), 0.7)
999 * Transform3::from_scale(Vec3::new(1.5, 0.75, 2.0)),
1000 ];
1001 for t in placements {
1002 let points: Vec<Point3> = (0..200)
1003 .map(|i| {
1004 let k = i as Scalar;
1005 Point3::new((k * 0.37).sin() * 3e3, (k * 0.91).cos() * 7.0, k * 1.3e-3)
1006 })
1007 .collect();
1008 let bound = placement_rounding(&points, t);
1009 for &p in &points {
1010 let placed = t.transform_point3(p);
1011 let truth = exact(t, p);
1012 let error = Vec3::new(
1013 placed.x - truth[0],
1014 placed.y - truth[1],
1015 placed.z - truth[2],
1016 );
1017 assert!(error.length() <= bound, "{error:?} over {bound:e}");
1018 }
1019 }
1020 }
1021
1022 /// The shrunk budget times the stretch is within the budget, and the
1023 /// next value up would not be (unless the quotient already fit).
1024 #[test]
1025 fn the_shrunk_budget_fits_after_rounding() {
1026 for budget in [1e-4, 1e-3, 3.7e-5, 0.125] {
1027 for factor in [0.33, 1.0, 1.19, 2.38, 2.510_038_280_268_978, 1e6] {
1028 let s = factor * (1.0 + 96.0 * f64::EPSILON);
1029 let b = shrunk_budget(budget, s);
1030 assert!(b * s <= budget, "{budget} {factor}");
1031 assert!(b <= budget / s);
1032 assert!(b == budget / s || b.next_up() * s > budget);
1033 }
1034 }
1035 // With room for a placement's rounding (#295): the grown bound fits.
1036 for room in [5.3e-9, 1e-5, 9.9e-5] {
1037 for factor in [0.33, 1.0, 1.19, 2.38] {
1038 let s = factor * (1.0 + 96.0 * f64::EPSILON);
1039 let b = shrunk_budget_within(1e-4, s, room);
1040 assert!(b > 0.0 && grown(b, s, room) <= 1e-4, "{room} {factor}");
1041 assert!(grown(b.next_up(), s, room) > 1e-4 || b == (1e-4 - room) / s);
1042 }
1043 }
1044 // The quotient alone rounds over at this factor.
1045 let s = 0.33 * (1.0 + 96.0 * f64::EPSILON);
1046 assert!((1e-4 / s) * s > 1e-4);
1047 assert!(shrunk_budget(1e-4, s) < 1e-4 / s);
1048 }
1049}