Compare commits

...

9 Commits

Author SHA1 Message Date
MarkusRe 4ad9bf9102 solidcore: five surface approximation, extrusion and revolution fixes become the only path (#14246 batch B47) (#15228)
* claim: #14246 B47

Co-Authored-By: Claude Code <noreply@anthropic.com>

* solidcore: B47 tests run on the default kernel (test first)

The tests of the five B47 fixes now run on Kernel::default(), without
setting approximate_surface_bezier_starter, approximate_surface_checks_every_row,
extrusion_step_uses_clamped_uv, revolution_frame_from_profile_origin or
revolution_offset_axis_both_sides. On main they fail: the default kernel
returns no surface for the offset wave (WR-573), accepts a pass whose
earlier rows miss the tolerance (WR-726), extrapolates STEP parameters
outside the extrusion's domain (WR-61), refuses the far side of a profile
that crosses the axis (WR-738) and leaves an offset generator past the
axis (WR-343).

Tests that pinned the defect or the opt-in's half state go: the default's
"success without surface", and the 1.62e-3 and 33 x 4 passes that only the
last-row check accepts. goldens surface.surface_approximate/gen0381 leaves
known_failing.json (achieved 0.546 for a requested 1.38e-3, #2507).

Co-Authored-By: Claude Code <noreply@anthropic.com>

* solidcore: retire the five B47 flags (WR-573, WR-726, WR-61, WR-738, WR-343)

ApproximateSurface raises the starter's inner knots to full multiplicity
and accepts a pass only when every row of patches is within the tolerance;
SurfOfExtrusion's STEP evaluators clamp to the analytic domain; a surface
of revolution takes its frame from the profile's origin and inverts on
both sides of the axis; its offset is cut at the axis on both sides of the
frame's origin.

- delete the five UpstreamFixes fields, their off paths, SolidCraft's
  preset lines, the fixes.json entries and every test setting of them.

SolidCraft already ran with all five on; the kernel rig with three
(approximate_surface_checks_every_row and revolution_frame_from_profile_origin
were missing from fixes.json).

Co-Authored-By: Claude Code <noreply@anthropic.com>

* solidcore: the step seam's half-turned revolution is built with upstream's frame

revolution_of_an_unbounded_line_measures_from_its_profile used
create_canonical to get a revolution whose frame (X = -1, from the line's
far start) is turned half a turn from its angles. With WR-738 the default
frame faces the profile's origin (X = +1), so the test builds upstream's
frame itself with SurfOfRevolution::new and keeps every check of closed();
it also asserts the new canonical frame.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* solidcore: bless the ten units the five B47 fixes move; WR addenda

133 golden rows move in ten units; no other unit does (all 283 units at
the fix: 273 pass, these ten changed, added 1 (gen0381), removed and
broken 0). Each flag was switched on alone on main's code to see which:

- approximate_surface_bezier_starter (WR-573): 42 rows of
  surface.surface_approximate (starters with simple inner knots now fit);
- approximate_surface_checks_every_row (WR-726): 31 rows there, plus
  gen0381 (was known failing: success at 0.546 for 1.38e-3);
- extrusion_step_uses_clamped_uv (WR-61): 9 rows of surf_of_extrusion_p1
  (evaluations outside the domain are clamped);
- revolution_frame_from_profile_origin (WR-738): 36 rows in seven units
  (the frame faces the profile's origin; inversion on both sides);
- revolution_offset_axis_both_sides (WR-343): 14 rows in three units
  (the offset generator is cut at the axis below the frame's origin).

Rows bless only rewrote within the goldens' tolerance keep their text.
Each WR record gets an addendum naming the retirement.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* solidcore: findings for the B47 retirement, every moved golden row

Why the 133 rows are right, by flag, and every row with its old and new
values; for surface.surface_approximate each row's requested and achieved
tolerance.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* solidcore: rustfmt the B47 changes

Co-Authored-By: Claude Code <noreply@anthropic.com>

* solidcore: B47 review: the overrun refusal is tested, issue 973's offsets against the closed form

- a starter whose end knots are simple overruns its control net: the new
  SM_ERR (main returned success with no surface) has a test;
- the WR-726 refusal test checks the failure itself;
- issue_973's offset generators: all four end points against the closed
  form (the segment moved by 1 either way, one of them cut at the axis at
  z = -1 - sqrt(2)), not one minimum radius over both;
- ApproximatedSurface::surface and approximate_surface's errors say when
  there is no surface and name the overrun.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* solidcore: B47 findings: a schematic of the offset cut at the axis, the frame's moved rows explained

- VectorCraft schematic (SVG and source) of issue 973's meridian plane:
  the offset towards the axis crosses it below the frame origin, where
  upstream's axis segment does not reach, and is cut there;
- the swap_uv to_nurbs rows and the two fillet_solver_p4 rows: their lines
  start on the axis to rounding, which WR-738's absolute on-axis bound
  does not see, so the frame's X axis turns; the surfaces are the same.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* solidcore: a starter given with simple end knots is clamped and fits; the overrun check is a guard

The full gate showed that a cubic starter with simple end knots is clamped
to its valid span before the patches are built, and is fitted exactly
(achieved 5.6e-17). A clamped starter whose inner knots are raised to full
multiplicity has three control points per span and one more, so no patch
runs past its net: the overrun SM_ERR is a guard. The test now pins the
fit, and approximate_surface's errors say so.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* solidcore: the clamped-starter test binds the surface before borrowing it

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-11-12 10:51:19 +00:00
Brett Lawson 3a105438a9 SolidCraft's errors carry solidcore-ops' offset refusal as data, and its bare SMLib statuses become a shrinking set (#15032) (#15253)
* Claim: SolidCraft's error carries the ops refusal instead of an SMLib status (#15032)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdcgEtwSrdwZxqTQp5MfnU

* SolidCraft's error carries an ops offset refusal as data, one offset routing for ops and SolidCraft, and its bare statuses as a shrinking set (#15032)

- crates/smlib: `Error::Refused(solidcore_ops::Error)` carries a refusal solidcore-ops
  types. An offset refusal that stands for no status (not certified, collapses, loops
  cross, seam opened, faces apart, reach uncertified, unsound, ...) now arrives as ops'
  `OffsetRefused` instead of solidcore's text. The offset adapter takes walls touching,
  folds and a vanishing edge from `solidcore_ops::offset::refusal` instead of routing
  them itself. The 13 kernel areas that still stand for a status share one predicate
  (`stands_for_status`), with the same outcomes.
- `tests::status_sites_only_shrink`: every use of `Error::Status` in the crate's code
  (called or point-free, outside comments, patterns and test modules), per file, a count
  that may only fall (27 today).
- solidcore-ops: `offset::refusal`, the offset refusal for a caller with no Order;
  `status` and `refusal` share one routing (`typed`), and a test checks they agree.
  `OffsetRefusal`'s loops-cross and unsound reasons are worded, not Debug.
- solidcore: `LoopCrossing` and `Unsoundness` get the words `OffsetFailure` used inline
  (its text unchanged; "refused" is added only by `OffsetFailure`).
- xtask/solidcore-paths.txt: crates/smlib 736 -> 727.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdcgEtwSrdwZxqTQp5MfnU

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 10:33:11 +00:00
MarkusRe 60abf29cdf Local operation: a face next to a round's tangent edge builds instead of being refused (#13718) (#15138)
* claim: #13718

Co-Authored-By: Claude Code <noreply@anthropic.com>

* Local operation test: a round given a wider coaxial cylinder meets its sides on the corner side (#13718)

A 10 mm cube's corner round r 2 about (8, 8) given the coaxial cylinder r 2.3
(and r 2.05) should build: the sides x = 10 and y = 10 cut the new cylinder
at 8 ± w, w = sqrt(R² − 4), and only 8 + w lies on the corner's side. The
test asserts the vertices (10, 8 + w) and (8 + w, 10) at both ends, ten
vertices in all, and the closed-form volume 10 (96 + A). On main the
operation is refused (LoopGapUncertified).

Co-Authored-By: Claude Code <noreply@anthropic.com>

* Local operation: a face next to a round's tangent edge is met where the new surfaces cross (#13718)

A local operation that moved a face beside a round refused at the round's
tangent edges (LoopGapUncertified): an edge whose faces were tangent in the
input was always extended and trimmed along its old line, though the new
surfaces now cross there.

- The reach encloses each piece's discriminant by its mean-value form and
  lists the edges where a plane and a quadric, the new surfaces, cross in
  every normal section (LocalReach::crossing).
- edgeuse_offset mates such an edge instead of extending and trimming it.
- mate_edges keeps the cut on the curved face's side: the plane cuts the
  quadric on both sides of the old edge, about sqrt(2 r delta) away, so the
  nearest midpoint decided nothing.
- trim_extended_edge ends an edge beside such a mated tangent edge at that
  edge's new vertex where it lies on the curve, and refuses it by name
  (OffsetFailure::TangentEndOffCurve) where it does not.

The r 2 round given the coaxial cylinder r 2.3 (and r 2.05), the r 5 round
given r 5.1 and the side moved 0.1 into its round now build, with their
vertices where the closed forms put them (within each vertex's tolerance)
and their closed-form volumes (within 1e-7 relative). The two refusal tests
of those cases go; the narrower cylinder r 1.8 and the side moved off its
round are still refused at the loop gap.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* Local operation: the goldens' offset adapter evaluates the binormal too; rustfmt (#13718)

The goldens unit offset.offset_geometry_creation implements OffsetBrep for its
own adapter: it gets edgeuse_middle_binormal, as the executive's does.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* Local operation: the tangent-end refusal's message and docs say where they hold (#13718)

The message of OffsetFailure::TangentEndOffCurve had a run of spaces in it (a
string continuation lost). The docs now say that a lamina edge still ends at
its vertex's image, and that the refusal comes from the mate.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* Local operation test: the tangent-edge cases also build with every fix on, and the new round faces out (#13718)

The wider coaxial cylinder, the round moved out and the side moved into its round run with
the kernel's default fixes and with UpstreamFixes::all() (the set an application turns on).
A built vertex is checked within the input's tolerance, not its own grown one, and the new
round's outer loop must run its surface's way.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* Local operation: a full cylinder given to a round faces out as the round did (#13718)

The new surface was turned by comparing its normal at the middle of its own domain with the
old face's normal at the face's middle. For a full cylinder given to a round that middle is
across the axis, where the normal opposes the round's though both face out, so the cylinder
was reversed and the round came back inside out: refused with every fix on (the face's UV
trims fail the loop orientation check; with exact mate curves the volume is negative). The
new normal is now read where the new surface passes nearest the old face's middle. Recorded
as WR-776.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* Goldens: topology.brep_local_operation gen0030 builds the box whose tangent side is tilted into its round (#13718)

box_with_large_fillet.smb with face 3 (the side x = 10, tangent to the r 8 round) moved
0.47 in and tilted into the round, face 4 moved and tilted, face 0 given its own plane.
main refused it at the loop gap (LoopGapUncertified, face 3); the side and the round now
cross along their tangent edge and the body is built: check clean, manifold, volume
certified in [795.35, 796.67] (an independent integration of the region gives 796.008),
every vertex within 1e-6 of its three faces' new surfaces.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* Local operation: the turned surface keeps upstream's point where no nearest normal exists; the upstream defect is WR-777 (#13718)

WR-776 is taken by #15209. Where the drop of the old face's middle fails, or the new surface
has no normal at the nearest point (a cone's apex), the normal at the domain middle decides,
as upstream. The tangent-edge tests name the fix set that fails.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* Findings: gen0030 of topology.brep_local_operation, the side tilted into its round, checked independently (#13718)

Co-Authored-By: Claude Code <noreply@anthropic.com>

* Offset failures: TangentEndOffCurve is classed like ExactCurveUncut, not a phrase nor a defect (#13718)

Since #15160 every offset failure is listed in is_phrase and is_defect and in the pinned
table of classes.

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-11-12 10:20:48 +00:00
Awais Akbar 977c7c7538 Sheet metal: contour curves in chain order, a lone folded panel's perforations, path flange text (#15364)
Contour flange curves are put in chain order before their segments are
joined, so a chain listed out of order builds. Perforations on a sheet
whose only panel is a folded flange (an arc-first contour) use that
panel's transform. The path flange description lists lines only, since
that command does not take arcs yet.
2026-11-12 10:18:57 +00:00
Awais Akbar a4e0a2c115 Sheet metal: only a bend whose inner edge collapses drops its inner face (#15365)
The sharp-bend limit was 1e-3 mm, far above the linear tolerance, so a
bend of radius 0.0005 dropped its inner face while its end walls kept an
inner arc and the shell could open. The limit is now half the linear
tolerance: the inner arc's chord is below it, so the end walls lose that
edge too. A larger radius keeps its inner cylinder.
2026-11-12 10:18:47 +00:00
Andrzej Błasik ebf6277d08 Agent commands: the agent's pointer stays on the face, edge or point it picked when you turn the view (#15361)
The pointer was placed on screen once, when it reached a pick. Turning, panning or zooming the view afterwards left it at the old pixel, pointing at nothing.

Now the pointer remembers the place in the model it rests on, and while it glides to it, waits there for the command to compute, or rests after it ran, it is drawn where the view shows that place at the moment. A pointer on a toolbar button, or following the agent's own mouse input, stays where it is on the screen. If the place leaves the view the pointer keeps its last position.
2026-11-12 10:01:16 +00:00
Ahmad Khan 7c1f818862 Sheet metal: an Extrude Cut that passes a hair off a flange's corner builds instead of failing with "the faces do not close" (#15346)
* Sheet metal: a cut that passes a hair off a flange's corner builds instead of failing to sew (#15269)

A cut-out passing within the sew's tolerance of a panel's corner, but not
through it, left the panel's flat outline open (SMLib's 2D boolean took the
two points for one, and the outline is read off the edges' curves) or with an
edge shorter than the sew's tolerance, which SMLib refuses. Such a cut-out now
passes through the corner: a corner of it that near moves onto the panel's
corner, and a straight edge that near is split there, as onto_seams already
does for bend lines. The reach is the sew's own tolerance, now public as
kernel::sew_tolerance.

* Sheet metal: a cut-out corner already on a panel corner stays there; keep the sew tolerance out of crates/kernel

A cut-out corner that lies on one panel corner no longer moves to another
corner within reach. The reach is worked out in the sheet code, so
crates/kernel gains no public function. A unit test covers onto_corners with
two corners within reach of each other.

---------

Co-authored-by: Ahmad Khan <ahmad.khan@shiritai.co>
2026-11-12 09:51:32 +00:00
MarkusRe f93890fb0c solidcore: an open profile turned part way is built, and a planar path's sweep frame is on its plane (#14246 batch B43) (#15264)
* claim: #14246 B43

Co-Authored-By: Claude Code <noreply@anthropic.com>

* solidcore: B43 tests run on the default kernel (test first)

The tests of two sweep flags now assert the fixed result on
Kernel::default() and drop their off halves:

- sheet_sweep_joins_profile_wires (WR-409): an open chain of two lines
  revolved half a turn is a joined sheet of two faces; a drilled hole's
  open profile (the goldens cases issue1463_drilled_hole_{pz,mz}_120)
  turned 120 degrees about +z and -z is a joined sheet of two cone faces,
  valid at Level 2, with the areas Pappus gives.
- sweep_frame_on_planar_path_normal (WR-407): a planar path's sweep
  frame keeps the plane's normal as its z. wr_660_planar_path_pose no
  longer switches the frame; both rigid-motion tests take the 1e-5 box
  bound of the test that switched it on.

On main's code the WR-409 and WR-407 tests fail; the next commit makes
the fixed paths the only ones.

Part of #14246 (batch B43).

Co-Authored-By: Claude Code <noreply@anthropic.com>

* solidcore: two sweep fixes become the default (retire their flags, B43)

The fixed paths of two UpstreamFixes flags of the primitive sweeps are
now the only ones, and the fields, the off paths and every preset line
go:

- sheet_sweep_joins_profile_wires (WR-409): before a partial revolve or
  a repeated linear sweep, the wires of an open profile are glued where
  their curves meet (p3/sweeps.rs), so a sheet of more than one curve is
  built instead of refused.
- sweep_frame_on_planar_path_normal (WR-407): a curve sweep along a
  planar path starts its frame with z on the plane's normal
  (p3/geometry.rs), so a flat cap stays flat to the rounding.

Fixture 434 no longer opts in to the frame; it keeps
cover_refuses_folded_continuation, which stays opt-in.

Part of #14246 (batch B43).

Co-Authored-By: Claude Code <noreply@anthropic.com>

* goldens: bless the 3 rows the two sweep fixes move (B43)

sweep.api_brep moves 3 rows; no other unit does. A drilled hole's open
profile revolved 120 degrees about +z and about -z is now built (WR-409)
instead of refused with 1001: a sheet of two faces, 6 edges and 5
vertices, which the new sweeps.rs test checks at Level 2 and against
Pappus' areas. box_face1_curve_path_cap, a box face swept along a
cubic in the plane y = 0, moves its volume by 6.4e-9 relative with the
frame on the plane's normal (WR-407). Every row with its old and new
value: plan/findings/sweep.api_brep--b43-two-flags-retired-2026-11-12.md.
WR-407 and WR-409 get their addenda.

Part of #14246 (batch B43).

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-11-12 09:45:22 +00:00
Bryan d6349b00e4 Engine: body names after the marker stay put in designs saved rolled back before names were pinned (#14008) (#14094)
* Engine: body names after the marker stay put in designs saved rolled back before names were pinned (#14008)

Opening a design pins each feature's counted body names (BodyN), but only
for the features it evaluated. A design saved before names were pinned,
and saved rolled back, left the features after the marker unpinned, so a
body made at the marker took a later body's number, every later counted
body shifted, and combines that named them failed or joined other bodies.
Opening such a design now evaluates the whole timeline once, pins the
names it gives (the names the bodies had when saved), and rolls back again.
Designs already pinned open as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: pinning body names after the marker leaves gear ratios and placements alone (#14008)

Opening an unpinned design saved rolled back refreshed the session on the
whole timeline to read the later bodies' names. That refresh also worked out
what the design reads from the later geometry and kept it: a gear picked on a
face after the marker took the later ratio, and the rolled-back refresh could
not resolve the face, so the changed ratio stayed and was marked saved. The
whole timeline is now evaluated on a scratch copy, and only the body names and
reserved names it pins come back to the design.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: the rolled-back reopen test checks the session is clean (#14008)

Review nitpick: assert the opened session is not dirty and has no undo
history, as the test's comment already claimed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re-run PR checks (#14008)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-11-12 09:03:03 +00:00
75 changed files with 4795 additions and 1143 deletions
+1 -1
View File
@@ -614,7 +614,7 @@ pub(super) fn sheet_eval(
}
FeatureKind::SheetContour { sketch, curves, distance, rule, flip, reverse } => {
let ss = st.sketch(sketch.0).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?.clone();
let segs = chain_segs(&ss, curves)?;
let segs = chain_segs(&ss, &ss.chain_order(curves)?)?;
let w = val(vals, distance, Kind::Length)?;
if !(w > 0.0) {
return Err(DocError::Invalid("the contour flange distance must be positive".into()));
+117 -6
View File
@@ -45,10 +45,12 @@ const TOUCH_AREA: f64 = 1e-6;
const FOLD_TABS_REJOIN: &str = "not supported yet: folding tabs that join again beyond the bend";
pub const FOLD_FIXED_OFF_SHEET: &str = "the fold's fixed point is not on the sheet";
/// A bend radius below this (mm), the sheet model's outline tolerance, is a sharp bend. Its inner
/// face has collapsed to the bend's inner edge, so the folded solid has only the outer cylindrical
/// face there, as a press brake makes it with a sharp punch: no inner cylinder.
const SHARP_BEND: f64 = 1e-3;
/// A bend radius at or below this (mm) is a sharp bend. Its inner face has collapsed to the bend's
/// inner edge, so the folded solid has only the outer cylindrical face there, as a press brake
/// makes it with a sharp punch: no inner cylinder. Half the linear tolerance, so the inner arc's
/// chord (at most √2 · radius) is below it and the end walls' inner edge collapses too; a larger
/// radius keeps its inner cylinder and the shell stays closed.
const SHARP_BEND: f64 = LINEAR_TOL / 2.0;
/// The most bores one sheet keeps.
const MAX_BORES: usize = 1000;
@@ -171,6 +173,76 @@ fn onto_seams(region: Region2, seams: &[(Vec2, Vec2)]) -> Region2 {
Region2 { outer: snap_loop(region.outer), holes: region.holes.into_iter().map(snap_loop).collect() }
}
/// `region` passing through each corner in `corners` (the panels' and bend zones' corners) that it
/// passes within `reach` of, but not through: a corner of its own that near moves onto it, as
/// [`onto_seams`] moves corners onto a bend line, and a straight edge that near is split there.
/// Such a corner is a corner of the cut-out to the folded solid, which is sewn with a tolerance
/// of `reach` (#15269): left apart, SMLib's 2D boolean took the two for one point and handed the
/// panel's outline back open, or left an edge between them shorter than the sew's tolerance,
/// which SMLib refused. The cut moves by under `reach`.
fn onto_corners(region: Region2, corners: &[Vec2], reach: f64) -> Region2 {
if corners.is_empty() {
return region;
}
let snap_loop = |mut lp: Loop2| -> Loop2 {
let n = lp.segs.len();
if n < 2 {
return lp;
}
for j in 0..n {
let prev = (j + n - 1) % n;
let (Some(p), Some(q)) = (lp.segs.get(j).and_then(seg_start), lp.segs.get(prev).and_then(seg_end)) else { continue };
if p.dist(q) > LINEAR_TOL {
continue;
}
// A corner on one of the corners (to rounding) stays there, whatever other corner is
// near.
if corners.iter().any(|c| c.dist(p) <= NOISE) {
continue;
}
let near = corners.iter().filter(|c| c.dist(p) < reach);
let Some(&c) = near.min_by(|a, b| a.dist(p).total_cmp(&b.dist(p))) else { continue };
if let Some(segs) = shifted(&lp.segs, j, c - p) {
lp.segs = segs;
}
}
let mut segs = Vec::with_capacity(lp.segs.len());
for e in lp.segs {
let Seg2::Line { a, b } = e else {
segs.push(e);
continue;
};
let d = b - a;
let mut on: Vec<(f64, Vec2)> = corners
.iter()
.filter(|c| c.dist(a) >= reach && c.dist(b) >= reach)
.filter_map(|&c| {
let t = (c - a).dot(d) / d.len2().max(1e-30);
let off = (a + d * t).dist(c);
((0.0..=1.0).contains(&t) && off > NOISE && off < reach).then_some((t, c))
})
.collect();
on.sort_by(|x, y| x.0.total_cmp(&y.0));
let mut from = a;
for (_, c) in on {
if c.dist(from) >= reach {
segs.push(Seg2::Line { a: from, b: c });
from = c;
}
}
segs.push(Seg2::Line { a: from, b });
}
Loop2 { segs }
};
Region2 { outer: snap_loop(region.outer), holes: region.holes.into_iter().map(snap_loop).collect() }
}
/// The working tolerance (mm) of the record the kernel's sew builds a solid `size` mm across
/// from: 10⁻⁶ of its size, at least 10⁻⁵ mm (`sew_impl` in `crates/kernel/src/sew.rs`).
fn sew_tolerance(size: f64) -> f64 {
(size * 1e-6).max(1e-5)
}
/// `segs` (a closed loop) with the corner where segment `j` starts moved by `delta`: a line
/// meeting there moves that end, an arc moves whole and passes the move on to the corner at its
/// other end. None when a curve that cannot move so meets the move, or the move goes round the
@@ -2177,8 +2249,16 @@ impl SheetBody {
let mut cuts = Vec::new();
let mut cut_points = 0usize;
let seams = self.seams();
// The folded solid is sewn with a tolerance of its size, which the flat pattern's size (and
// the thickness) bounds.
let flat_box = pieces.iter().try_fold(Aabb2::EMPTY, |b, (_, r)| loop_box(&r.outer).map(|r| b.union(&r)))?;
let reach = sew_tolerance(flat_box.diagonal() + self.t);
let corners: Vec<Vec2> =
pieces.iter().flat_map(|(_, r)| std::iter::once(&r.outer).chain(&r.holes)).flat_map(|l| l.segs.iter().map(Seg2::start)).collect();
for hole in self.holes.iter().filter(|h| encloses_area(h)) {
let region = fitted_region(&onto_seams(planar_region(hole)?, &seams))?;
let hole_corners = hole.holes.iter().fold(hole.outer.segs.len(), |n, h| n.saturating_add(h.segs.len()));
work.charge(corners.len().saturating_mul(hole_corners))?;
let region = fitted_region(&onto_corners(onto_seams(planar_region(hole)?, &seams), &corners, reach))?;
cut_points = cut_points.saturating_add(region_complexity(std::slice::from_ref(&region))?.1);
if cut_points > MAX_FLAT_POINTS {
return Err(flat_budget_error());
@@ -3346,8 +3426,12 @@ impl SheetBody {
let transverse = volume * (3.0 * radius2 + self.t * self.t) / 12.0;
let own = [[transverse, 0.0, 0.0], [0.0, transverse, 0.0], [0.0, 0.0, volume * radius2 / 2.0]];
for hole in holes {
let world = if self.flat || panels.len() == 1 {
// One panel on its own is placed by its own transform: an arc-first contour
// has a single flange panel and no base, and that panel is folded.
let world = if self.flat {
&flat
} else if let [(panel_world, _)] = panels.as_slice() {
panel_world
} else {
&panels.iter().find(|(_, region)| region.contains(hole)).ok_or_else(invalid)?.0
};
@@ -8839,6 +8923,33 @@ mod tests {
Vec2::new(x * turn.cos() - y * turn.sin(), x * turn.sin() + y * turn.cos())
}
#[test]
fn a_cut_out_passes_through_a_corner_it_passes_a_hair_off() {
// Two corners 0.00008 apart, both within reach of each other (#15269).
let (a, b, reach) = (Vec2::new(0.0, 0.0), Vec2::new(8e-5, 0.0), 1.6e-4);
let square = |p: Vec2| Region2 {
outer: Loop2::polygon(&[p, p + Vec2::new(0.0, 1.0), p + Vec2::new(-1.0, 1.0), p + Vec2::new(-1.0, 0.0)]),
holes: vec![],
};
// A cut-out's corner on either corner stays there.
for p in [a, b] {
assert_eq!(onto_corners(square(p), &[a, b], reach), square(p));
}
// One a hair off moves onto the nearest corner.
let moved = onto_corners(square(Vec2::new(1.1e-4, 1e-5)), &[a, b], reach);
assert!(moved.outer.segs.iter().any(|e| e.start() == b), "{moved:?}");
// An edge passing a hair off a corner is split there.
let strip = Region2 {
outer: Loop2::polygon(&[Vec2::new(-1.0, 1e-5), Vec2::new(1.0, 1e-5), Vec2::new(1.0, 1.0), Vec2::new(-1.0, 1.0)]),
holes: vec![],
};
let split = onto_corners(strip, &[a], reach);
assert_eq!(split.outer.segs.len(), 5, "{split:?}");
assert!(split.outer.segs.iter().any(|e| e.start() == a), "{split:?}");
// Corners beyond reach move nothing.
assert_eq!(onto_corners(square(Vec2::new(1e-3, 0.0)), &[a, b], reach), square(Vec2::new(1e-3, 0.0)));
}
#[test]
fn a_ledge_beside_an_edge_as_tall_as_the_setback_goes_with_the_strip() {
// A 100 x 60 plate with a ledge 3 wide beside its y = 0 edge, as tall as the setback of a
+38
View File
@@ -1237,6 +1237,44 @@ fn counted_body_names_stay_when_the_timeline_changes() {
assert_eq!(bodies(&s), named(&[("Body2", "B"), ("Body3", "C"), ("Body4", "E")]));
}
/// A design saved before body names were pinned, and saved rolled back, is pinned as it opens
/// for the features after the marker too (#14008): a body made at the marker does not take the
/// number of a later body, so the later bodies keep the names they were saved with and a combine
/// that names them still joins the same bodies.
#[test]
fn counted_body_names_after_the_marker_stay_in_a_design_saved_before_pinning() {
let mut s = Session::default();
for (name, x) in [("A", 0), ("B", 20), ("C", 40), ("D", 50)] {
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [x, 0, 0], "name": name}));
}
run(&mut s, "solid.combine", json!({"target": "Body3", "tools": ["Body4"], "operation": "join", "name": "Join"}));
// As a file written before names were pinned: no stored names, and rolled back to after A.
let mut doc = (*s.doc).clone();
for f in &mut doc.features {
f.body_names.clear();
}
doc.reserved_body_names.clear();
doc.marker = Some(1);
let mut s = Session::opened_at(solidcraft_doc::Document::from_json(&doc.to_json()).unwrap(), None, None);
assert_eq!(s.doc.find_feature("C").map(|f| f.body_names.clone()), Some(vec!["Body3".to_string()]));
assert!(["Body1", "Body2", "Body3", "Body4"].iter().all(|n| s.doc.reserved_body_names.contains(*n)));
// Opening changed nothing the user can see: the design is still rolled back, and not dirty.
assert_eq!(s.doc.marker, Some(1));
assert_eq!(s.model.state().bodies.len(), 1);
assert!(!s.is_dirty());
assert!(s.undo.is_empty());
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [0, 40, 0], "name": "N"}));
run(&mut s, "timeline.roll_to", json!({"position": "end"}));
let st = s.model.state();
let names: Vec<(String, String)> =
st.bodies.iter().map(|b| (b.name.clone(), s.doc.feature(b.feature).map(|f| f.name.clone()).unwrap_or_default())).collect();
let want: Vec<(String, String)> =
[("Body1", "A"), ("Body5", "N"), ("Body2", "B"), ("Body3", "C")].iter().map(|(b, f)| (b.to_string(), f.to_string())).collect();
assert_eq!(names, want, "C and D are joined into Body3, N takes a new number");
assert!(s.doc.features.iter().all(|f| s.model.result(f.id).is_some_and(|r| r.error.is_none())));
}
/// Pinning reads each feature's bodies once and keeps them with the state it read them from;
/// the names it pins and reserves, and the fingerprints it leaves, are those of reading every
/// feature's bodies again: random boxes (named and counted), moves, suppressions, deletions,
+48
View File
@@ -968,11 +968,59 @@ impl Session {
// first undo after opening evaluated the whole timeline again (82-93 s in a
// 5,484-feature design). The pinned names are the ones the file's bodies have, so
// opening is no change: the pinned design is the saved one.
let unpinned = session.doc.reserved_body_names.is_empty() && session.doc.features.iter().all(|f| f.body_names.is_empty());
let before = session.doc.clone();
session.pin_body_names();
if unpinned || !std::sync::Arc::ptr_eq(&before, &session.doc) {
session.pin_rolled_back_body_names();
}
session.saved = session.doc.clone();
session
}
/// Pin the names of the bodies the features after the timeline marker make, in a design
/// saved rolled back before names were pinned (#14008). Those features were never evaluated,
/// so [`Session::pin_body_names`] left them unpinned: a body then made at the marker took the
/// number of a later body, every later counted body shifted, and a combine that names them
/// failed or joined another body. The names are the ones the whole timeline gives, which is
/// what the design called those bodies when it was saved. The design stays rolled back.
///
/// The whole timeline is evaluated on a scratch copy, and only the names it pins come back:
/// refreshing this session there would also work out what the design reads from the later
/// geometry (a gear's ratio from a face after the marker, joint placements) and keep it.
fn pin_rolled_back_body_names(&mut self) {
if !self.doc.marker.is_some_and(|m| m < self.doc.features.len()) {
return;
}
let mut whole = self.scratch();
whole.doc_mut().marker = None;
whole.model.evaluate_shared(&whole.doc);
whole.pin_body_names();
let pins: Vec<(u64, Vec<String>)> = whole
.doc
.features
.iter()
.filter(|f| self.doc.feature(f.id).is_some_and(|mine| mine.body_names != f.body_names))
.map(|f| (f.id, f.body_names.clone()))
.collect();
let reserve: Vec<String> = whole.doc.reserved_body_names.difference(&self.doc.reserved_body_names).cloned().collect();
if pins.is_empty() && reserve.is_empty() {
return;
}
// As `pin_body_names` stores names: the bodies keep the names they have, so no result
// changes and none is evaluated again.
let renamed: Vec<u64> = pins.iter().map(|(id, _)| *id).collect();
let keep = self.model.pinned_prefix(&self.doc, &renamed);
let doc = self.doc_mut();
for (id, names) in pins {
if let Some(f) = doc.feature_mut(id) {
f.body_names = names;
}
}
doc.reserved_body_names.extend(reserve);
self.model.refingerprint_shared(&self.doc, keep);
}
/// Prepare only the file's copy with relative paths; saving never rewrites live history.
pub(crate) fn design_at(&self, path: Option<&str>) -> std::borrow::Cow<'_, Document> {
let mut design = std::borrow::Cow::Borrowed(&*self.doc);
@@ -429,3 +429,34 @@ fn an_editor_accepts_the_ratios_rolling_the_marker_works_out() {
assert_eq!(s.doc.marker, None);
assert!(close_to(ratio(&s), -0.6), "{}", ratio(&s));
}
/// Opening a design saved rolled back before body names were pinned works the later bodies'
/// names out from the whole timeline (#14008), and keeps nothing else that evaluation reads: a
/// gear picked on a face after the marker keeps the ratio the file has, and the parts stay
/// where the file has them.
#[test]
fn pinning_the_names_after_the_marker_leaves_a_gear_on_a_later_face_alone() {
let s = gear_train();
let g1 = s.doc.components.iter().find(|c| c.name == "G1").map(|c| c.id).unwrap_or_else(|| panic!("no G1"));
let disc = s.model.state().bodies.iter().find(|b| b.component == g1).map(|b| b.feature).unwrap_or_else(|| panic!("no G1 disc"));
let at = s.doc.features.iter().position(|f| f.id == disc).unwrap_or_else(|| panic!("no G1 disc feature"));
let mut doc = (*s.doc).clone();
for f in &mut doc.features {
f.body_names.clear();
}
doc.reserved_body_names.clear();
// Rolled back before G1's disc, so its face is not there, with the ratio the file was saved
// with (the whole timeline makes it -0.5).
doc.marker = Some(at);
if let Some(l) = doc.assembly.links.first_mut() {
l.ratio = 0.25;
}
let text = doc.to_json();
let plain = Session::opened(solidcraft_doc::Document::from_json(&text).unwrap(), None);
let opened = Session::opened_at(solidcraft_doc::Document::from_json(&text).unwrap(), None, None);
assert!(opened.doc.feature(disc).is_some_and(|f| !f.body_names.is_empty()), "the disc after the marker has its name");
assert_eq!(opened.doc.marker, Some(at));
assert_eq!(opened.doc.assembly.links.first().map(|l| l.ratio), Some(0.25));
assert_eq!(opened.doc.assembly, plain.doc.assembly);
assert_eq!(opened.doc.occurrences, plain.doc.occurrences);
}
+1 -1
View File
@@ -67,7 +67,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.contract(&PATH_FLANGE_CONTRACT)
.output(output_schema_for::<FeatureResult>)
.params(
"edge: [x,y,z] on a top or bottom edge of the sheet; sketch + curves: an open chain of sketch lines and tangent arcs drawn on a plane across \
"edge: [x,y,z] on a top or bottom edge of the sheet; sketch + curves: an open chain of sketch lines drawn on a plane across \
that edge, from the edge out (each line one flange as long as the line, to the outer face from the virtual sharp; each \
corner one bend, turning the way the lines turn); radius? (default: rule); body?. One feature, one body",
),
+72
View File
@@ -645,6 +645,14 @@ fn contour_flange_takes_tangent_arcs_and_develops_their_bends() {
assert!((bends[0]["radius"].as_f64().unwrap_or(0.0) - 8.0).abs() < 1e-9, "{f}");
assert!((bends[0]["angle_deg"].as_f64().unwrap_or(0.0) - 90.0).abs() < 1e-9, "{f}");
assert!((bends[0]["allowance"].as_f64().unwrap_or(0.0) - FRAC_PI_2 * 9.0).abs() < 1e-9, "{f}");
// The same chain listed out of order builds the same flange: the curves are put in chain
// order before their segments are joined.
run(&mut s, "edit.undo", json!({}));
let r = run(&mut s, "sheet.flange", json!({"sketch": "Arc", "curves": ["l1", "l2", "a1"], "distance": 50, "rule": "R"}));
assert!(r.get("error").is_none(), "{r}");
let f = flat(&mut s);
let got = f["flat_size_mm"][0].as_f64().unwrap_or(f64::NAN);
assert!((got - want).abs() < 1e-6, "{got} vs {want}: {f}");
// A 180° half pipe alone: outer radius 56 and 1.5 thick, with no flat panel before or
// after it.
let mut s = Session::default();
@@ -1365,6 +1373,15 @@ fn a_sharp_bend_has_only_its_outer_cylindrical_face() {
assert_eq!(cylinders, 3, "one outer face per sharp bend, no inner cylinders");
let bends = flat(&mut s)["bends"].clone();
assert_eq!(bends.as_array().map(Vec::len), Some(3), "{bends}");
// A tiny radius above the sharp limit keeps its inner cylinder, and the body stays closed.
let mut s = Session::default();
run(&mut s, "sheet.manage_rules", json!({"name": "Tiny", "thickness": t, "k_factor": k, "bend_radius": 0.0005, "active": true}));
base(&mut s, 100.0, w);
run(&mut s, "sheet.fold", json!({"points": [[80, 0, t], [80, w, t]], "fixed": fixed, "flip": false, "radius": 0.0005}));
let body = &s.model.state().bodies[0].body;
assert!(body.validity().is_empty() && body.shell_count() == 1, "{:?}", body.validity());
let cylinders = body.faces(1e-6).map(|f| f.iter().filter(|f| f.plane_normal.is_none()).count()).unwrap_or(0);
assert_eq!(cylinders, 2, "a tiny radius keeps both cylindrical faces");
}
#[test]
@@ -7750,3 +7767,58 @@ fn flange_segments_stored_bounds_outside_the_partition_are_refused() {
assert!(errors.iter().any(|e| e == want), "{what}: {errors:?}");
}
}
/// A through-all cut that passes a hair off the top corner of a flange where it meets the mitre
/// of the next flange (#15269). A 60 × 100 plate (2 mm sheet, R 2) gets 30 mm flanges on its
/// x = 0 and y = 100 edges; a cut in the YZ plane tapers the first flange from its mitre corner,
/// y = 98 − √2 at z = 30, down to z = 10 at y = 0. With the cut's corner 0.00001 to 0.0001 mm
/// along the top edge from the flange's corner, or its taper passing that close to the corner,
/// SMLib's 2D boolean took the two for one point and the flange's outline came back open ("sew:
/// the faces do not close"), or with an edge that short, which SMLib refused. Every offset builds,
/// and the flange loses its part above the taper.
#[test]
fn a_cut_passing_a_hair_off_a_flange_corner_builds() {
// The flange's flat panel runs 26 from its bend (z = 4) to its top (z = 30) and c along y. The
// taper runs from (y 0, z 10) at slope k; the flange loses what lies above it.
let c = 98.0 - 2f64.sqrt();
let removed = |k: f64, end: f64| {
let y = (20.0 / k).min(end);
20.0 * y - k * y * y / 2.0
};
let mut failed = Vec::new();
for d in [0.0, 1e-5, -1e-5, 3e-5, -3e-5, 1e-4, -1e-4, 1.2e-4, -1.2e-4, 1e-3, -1e-3, 96.5858 - c] {
for along in [true, false] {
let mut s = Session::default();
run(
&mut s,
"sheet.manage_rules",
json!({"name": "Steel 2mm", "thickness": "2 mm", "bend_radius": "2 mm", "k_factor": "0.4", "active": true}),
);
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Base"}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [60, 100]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "sheet.flange", json!({"sketch": "Base", "body_name": "Part"}));
run(&mut s, "sheet.flange", json!({"edges": [[0, 50, 2], [30, 100, 2]], "height": 30}));
let before = volume(&mut s);
// Along: the cut's corner on the flange's top edge, d from its corner. Past: the taper
// runs on 1 mm past the mitre, d above the corner there.
let (corner, k) = if along { ([c + d, 30.0], 20.0 / (c + d)) } else { ([c + 1.0, 10.0 + (c + 1.0) * (20.0 + d) / c], (20.0 + d) / c) };
run(&mut s, "sketch.create", json!({"plane": "YZ", "name": "Cut"}));
run(&mut s, "sketch.line", json!({"points": [corner, [0, 10], [-5, 10], [-5, 35], [corner[0], 35]], "closed": true}));
run(&mut s, "sketch.finish", json!({}));
let what = format!("{d:e}, {}", if along { "corner along the top edge" } else { "taper past the corner" });
let cut = json!({"sketch": "Cut", "profiles": "all", "through_all": true, "direction": "symmetric", "operation": "cut"});
if let Err(e) = s.execute("solid.extrude", &cut) {
failed.push(format!("{what}: {e}"));
continue;
}
// The cut passes through the corner when it passes within the sew's tolerance of it
// (under 0.00016 mm here). The taper then turns about its far end by that much at
// most, which moves the volume by under 2 mm × c × 0.00016 / 2 = 0.016 mm³.
let want = before - 2.0 * removed(k, c.min(corner[0]));
let got = volume(&mut s);
assert!((got - want).abs() < 0.016, "{what}: {got} vs {want}");
}
}
assert!(failed.is_empty(), "{failed:?}");
}
-7
View File
@@ -82,7 +82,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.fillet_chamfer_cone_on_arc = true;
f.offset_excess_faces_after_merge = true;
f.offset_clip_right_side_out = true;
f.revolution_offset_axis_both_sides = true;
f.offset_extended_edges_mated = true;
f.keep_operand_uv_trims = true;
f.proto_vertex_miss_is_false = true;
@@ -123,7 +122,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.edge_reversal_keeps_failed_edge = true;
f.topology_failed_evaluations_propagate = true;
f.heal_stage3_failures_propagate = true;
f.sweep_frame_on_planar_path_normal = true;
f.cover_refuses_folded_continuation = true;
f.boolean_vertex_by_best_crossing = true;
f.boolean_cleanup_removes_slits = true;
@@ -132,7 +130,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.degenerate_proto_edge_in_one_vertex = true;
f.new_edge_vertex_tolerance_covers_gap = true;
f.torus_signed_gen_curve_interval = true;
f.sheet_sweep_joins_profile_wires = true;
f.point_solve_keeps_triangle_guess = true;
f.imprint_trims_edge_to_proto_vertex = true;
f.imprint_extends_edge_to_proto_vertex = true;
@@ -176,7 +173,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.revolution_round_offset_collapses = true;
f.fillet_chamfer_cone_stored_as_cone = true;
f.offset_point_vertex_caps = true;
f.approximate_surface_bezier_starter = true;
f.proto_edge_splits_at_captured_vertices = true;
f.stitch_solid_nests_cavities = true;
f.local_operation_closes_walls_to_nothing = true;
@@ -250,7 +246,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.circumscribe_axis_map_by_basis = true;
f.validation_does_not_make_uv_trims = true;
f.iso_curve_uv_line_both_params = true;
f.extrusion_step_uses_clamped_uv = true;
f.analytic_corner_failure_is_false = true;
f.proto_edge_split_upper_keeps_the_old_max = true;
f.point_on_start_point_squared_tolerance = true;
@@ -307,7 +302,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.fillet_overflow_crease_along_edge = true;
f.plane_first_torus_section_exact = true;
f.keep_exact_pcurves = true;
f.approximate_surface_checks_every_row = true;
f.planar_section_face_tolerance = true;
f.face_swap_uv_transposes_trims = true;
f.closed_smooth_pole_tangent_limit = true;
@@ -316,7 +310,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.fillet_setback_beside_curved_faces = true;
f.fillet_chamfer_corner = true;
f.brep_ray_classifies_current_trims = true;
f.revolution_frame_from_profile_origin = true;
f
}
+59 -86
View File
@@ -74,29 +74,47 @@ type Out<T> = std::result::Result<T, Error>;
const INVALID_INPUT: i32 = 1007;
/// An SMLib status as the capi passes it on (`status`, smlib_shim.h): solidcore's upstream
/// status codes as they are; its refusals of code it has not ported as not supported.
/// status codes as they are; its refusals of code it has not ported as not supported; a refusal
/// solidcore-ops types as that refusal ([`Error::Refused`]); the rest as solidcore's text.
pub(super) fn status(e: SolidError) -> Error {
match e {
SolidError::Upstream { code, .. } => Error::Status(code),
e if is_b_spline_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
e if is_intersect_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
e if is_primitive_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
e if is_mass_heal_or_geom_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
e if is_sweep_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
e if is_query_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
e if is_topology_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
e if is_curve_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
e if is_surface_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
e if is_offset_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
e if is_blend_failure(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
e if stands_for_status(&e) => Error::Status(e.sm_status().unwrap_or(solidcore::error::SM_ERR)),
SolidError::InvalidInput { .. } => Error::Status(INVALID_INPUT),
SolidError::Unsupported { reason, .. } if reason == MIXED_CORNER_RAILS_CROSS => Error::Unported(MIXED_CORNER_UNSUPPORTED),
e if is_boolean_failure(&e) => boolean_status(e),
e if e.is_unported() => Error::Unsupported,
e => Error::Solidcore(e.to_string()),
e => match solidcore_ops::offset::refusal(&e) {
Some(r @ solidcore_ops::Error::OffsetRefused { .. }) => Error::Refused(r),
_ => Error::Solidcore(e.to_string()),
},
}
}
/// The kernel failures SolidCraft still passes on as the status they stand for ([`Error::Status`]),
/// because solidcore-ops gives them no type of their own yet: each replaced a status its routine
/// returned (`Upstream` before solidcore described it), whether or not it names an entity. A
/// B-spline method's (`NurbsFailure::BSpline`); an intersection or classification, primitive
/// (extrusion, revolution, skin, taper), mass-property, healing, geometry-utility, query,
/// topology or surface routine's; and a sweep, curve, offset or blend routine's that replaced a
/// status (a railed loft's, a length bracket's and an offset's refusals replaced none and pass on
/// otherwise). `tests::status_sites_only_shrink` counts the sites that make a status, a set that only shrinks.
fn stands_for_status(e: &SolidError) -> bool {
matches!(e.nurbs_failure(), Some(NurbsFailure::BSpline(_)))
|| e.intersect_failure().is_some()
|| e.primitive_failure().is_some()
|| e.mass_failure().is_some()
|| e.heal_failure().is_some()
|| e.geom_failure().is_some()
|| e.sweep_failure().is_some_and(|w| w.replaces_upstream_status())
|| e.query_failure().is_some()
|| e.topology_failure().is_some()
|| e.curve_failure().is_some_and(|c| c.replaces_upstream_status())
|| e.surface_failure().is_some()
|| e.offset_failure().is_some_and(|o| o.replaces_upstream_status())
|| e.blend_failure().is_some_and(|b| b.replaces_upstream_status())
}
/// A failure of the Boolean engine (which slice, sweep and the other users of the merge engine
/// run), which solidcore names in its own terms as a [`SolidError::Failed`] from a
/// `boolean::` stage where it once returned [`SolidError::Upstream`].
@@ -116,79 +134,6 @@ fn boolean_status(e: SolidError) -> Error {
}
}
/// A B-spline method's failure (solidcore's `BSplineFailure`): it stands for the status the
/// method returned (`Upstream` before solidcore described it), and passes on as that status.
fn is_b_spline_failure(e: &SolidError) -> bool {
matches!(e.nurbs_failure(), Some(NurbsFailure::BSpline(_)))
}
/// An intersection or classification failure (solidcore's `IntersectFailure`): it stands for the
/// status the routine returned (`Upstream` before solidcore described it), and passes on as that
/// status.
fn is_intersect_failure(e: &SolidError) -> bool {
e.intersect_failure().is_some()
}
/// A primitive, extrusion, revolution, skin or taper failure (solidcore's `PrimitiveFailure`): it
/// stands for the status the routine returned (`Upstream` before solidcore described it), and
/// passes on as that status.
fn is_primitive_failure(e: &SolidError) -> bool {
e.primitive_failure().is_some()
}
/// A mass-property, healing or geometry-utility failure (solidcore's `MassFailure`,
/// `HealFailure`, `GeomFailure`): it stands for the status the routine returned (`Upstream`
/// before solidcore described it), and passes on as that status.
fn is_mass_heal_or_geom_failure(e: &SolidError) -> bool {
e.mass_failure().is_some() || e.heal_failure().is_some() || e.geom_failure().is_some()
}
/// A sweep routine's failure (solidcore's `SweepFailure`) that replaced a status the routine
/// returned (`Upstream` before solidcore described it): it passes on as that status. A railed
/// loft's refusal replaced none and passes on as solidcore's text, as before.
fn is_sweep_failure(e: &SolidError) -> bool {
e.sweep_failure().is_some_and(|w| w.replaces_upstream_status())
}
/// A query's failure (solidcore's `QueryFailure`): it stands for the status the query returned
/// (`Upstream` before solidcore described it), and passes on as that status.
fn is_query_failure(e: &SolidError) -> bool {
e.query_failure().is_some()
}
/// A topology routine's failure (solidcore's `TopologyFailure`): it stands for the status the
/// routine returned (`Upstream` before solidcore described it), and passes on as that status.
fn is_topology_failure(e: &SolidError) -> bool {
e.topology_failure().is_some()
}
/// A curve routine's failure (solidcore's `CurveFailure`) that replaced a status the routine
/// returned (`Upstream` before solidcore described it): it passes on as that status. A length
/// bracket's refusal replaced none and passes on as solidcore's text, as before.
fn is_curve_failure(e: &SolidError) -> bool {
e.curve_failure().is_some_and(|c| c.replaces_upstream_status())
}
/// A surface routine's failure (solidcore's `SurfaceFailure`): it stands for the status the
/// routine returned (`Upstream` before solidcore described it), and passes on as that status.
fn is_surface_failure(e: &SolidError) -> bool {
e.surface_failure().is_some()
}
/// An offset routine's failure (solidcore's `OffsetFailure`) that replaced a status the routine
/// returned (`Upstream` before solidcore described it): it passes on as that status. An offset's
/// refusals replaced none and pass on as before.
fn is_offset_failure(e: &SolidError) -> bool {
e.offset_failure().is_some_and(|o| o.replaces_upstream_status())
}
/// A blend routine's failure (solidcore's `BlendFailure`) that replaced a status the routine
/// returned (`Upstream` before solidcore described it): it passes on as that status, whether or
/// not it names an entity.
fn is_blend_failure(e: &SolidError) -> bool {
e.blend_failure().is_some_and(|b| b.replaces_upstream_status())
}
/// `size_ok`.
fn size_ok(x: f64) -> bool {
x.is_finite() && x > 0.0 && x <= 1e6
@@ -1550,6 +1495,34 @@ mod tests {
assert!(!matches!(status(folds), Error::Status(_)));
}
/// An offset's refusal that stands for no status reaches SolidCraft as ops' typed refusal
/// ([`Error::Refused`]), its reason and sizes as data, not as solidcore's text (#15032); the
/// refusals with kinds of their own keep their errors.
#[test]
fn offset_refusals_pass_on_as_ops_refusals() {
use solidcore::error::Failure;
use solidcore::error::offset::{LoopCrossing, OffsetFailure};
use solidcore_ops::{Error as Ops, OffsetRefusal};
let face = solidcore::topology::FaceId::from_index(3);
let offset = |o| Failure::new("offset::offset_executive::crossing_loops", o).into_error();
let refused = |why| Error::Refused(Ops::OffsetRefused { face: None, why });
assert_eq!(
status(offset(OffsetFailure::Collapses { face, distance: -2.0, radius: 1.5 })),
refused(OffsetRefusal::Collapses { distance: -2.0, radius: 1.5 })
);
assert_eq!(status(offset(OffsetFailure::NotCertified { face, distance: -1.0 })), refused(OffsetRefusal::NotCertified { distance: -1.0 }));
let crossing = LoopCrossing::ThreeAtOnePoint;
let e = status(offset(OffsetFailure::LoopsCross { face: Some(face), crossing }));
assert_eq!(e, refused(OffsetRefusal::LoopsCross { crossing }));
assert_eq!(
e.to_string(),
"the offset is refused: its offset's loops cross in a way the offset cannot resolve: three loops cross at one point"
);
assert_eq!(status(offset(OffsetFailure::SeamOpened { face })), refused(OffsetRefusal::SeamOpened));
// A fold is the offset module's own error (`Error::WallFolds`), never ops' general refusal.
assert!(!matches!(status(offset(OffsetFailure::Folds { face, distance: 2.0, largest: 1.0 })), Error::Refused(_) | Error::Status(_)));
}
/// A blend routine's failure passes on as the status it replaced, as the `Upstream` it
/// replaced did, whether or not it names an edge.
#[test]
+11 -17
View File
@@ -18,8 +18,6 @@
use solidcore::EntityRef;
use solidcore::SolidError;
use solidcore::error::offset::OffsetFailure;
use solidcore::error::{ErrorKind, Reason};
use solidcore::geom::attribute::{Attribute, Behavior, ids};
use solidcore::geom::axis2_placement::Axis2Placement;
use solidcore::geom::object::Shape;
@@ -51,22 +49,18 @@ type Out<T> = std::result::Result<T, Error>;
/// `SM_ERR_INVALID_INPUT`.
const INVALID_INPUT: i32 = 1007;
/// `status` (smlib_shim.h): an offset whose walls touch is [`Error::WallsTouch`]; a vertex moved
/// past an edge's other end (`SM_ERR_OFFSET_EDGE_VANISHES`, fork 34318f4) is
/// `SMLIB_ERR_UNSUPPORTED`; a wall whose face's offset folds, which solidcore's one construction
/// refuses (solidcore#2367), is [`Error::WallFolds`] with the
/// largest wall it names; any other status as the blends pass it on. solidcore names an offset's
/// refusals by their kind and reason (its errors in its own terms, solidcore#2654), whichever
/// status the pinned C++ gives them.
/// `status` (smlib_shim.h): an offset's refusal as solidcore-ops routes it
/// ([`solidcore_ops::offset::refusal`], the routing of ops' own offsets): walls that touch
/// ([`Error::WallsTouch`]), a wall whose face's offset folds ([`Error::WallFolds`] with the largest
/// wall it names, solidcore#2367), a vertex moved past an edge's other end
/// (`SM_ERR_OFFSET_EDGE_VANISHES`, fork 34318f4: `SMLIB_ERR_UNSUPPORTED`); anything else as the
/// blends pass it on, which carries ops' other offset refusals as they are.
fn status(e: SolidError) -> Error {
if let SolidError::Failed(f) = &e
&& let Reason::Offset(OffsetFailure::Folds { largest, .. }) = f.reason
{
return Error::WallFolds { largest: Some(largest) };
}
match e.kind() {
ErrorKind::OffsetWallsTouch => Error::WallsTouch,
ErrorKind::OffsetEdgeVanishes => Error::Unsupported,
use solidcore_ops::Error as Ops;
match solidcore_ops::offset::refusal(&e) {
Some(Ops::WallsTouch) => Error::WallsTouch,
Some(Ops::WallFolds { largest }) => Error::WallFolds { largest },
Some(Ops::Unsupported(_)) => Error::Unsupported,
_ => blend_status(e),
}
}
+4
View File
@@ -92,6 +92,9 @@ pub enum Error {
Record(import::Unsupported),
/// solidcore reported an error (the call, then its message).
Solidcore(String),
/// A refusal solidcore-ops types, carried as it is: the app words it from its fields
/// (an offset refused with its reason, [`solidcore_ops::Error::OffsetRefused`]).
Refused(solidcore_ops::Error),
/// solidcore panicked; the guard caught it (the call, then the panic's message).
Internal(String),
/// A step of SMLib solidcore has not ported yet, refused rather than built another way: what
@@ -175,6 +178,7 @@ impl std::fmt::Display for Error {
Error::Exception => write!(f, "SMLib raised an internal exception"),
Error::Record(u) => write!(f, "{u}"),
Error::Solidcore(e) => write!(f, "solidcore: {e}"),
Error::Refused(e) => write!(f, "{e}"),
Error::Internal(e) => write!(f, "solidcore failed internally in {e}"),
Error::Unported(what) => write!(f, "not supported yet: {what}"),
Error::Cancelled(call) => write!(f, "{call}: cancelled"),
+83
View File
@@ -638,3 +638,86 @@ fn every_call_starts_clean_on_any_kernel() {
assert_eq!(core::run("draw", draw).unwrap(), first, "another kernel");
drop(held);
}
/// The sites in this crate that still make a bare status ([`Error::Status`], "SMLib failed (status
/// n)") for a failure that has no type yet: a set that only shrinks. A failure solidcore-ops types
/// travels as its refusal ([`Error::Refused`]), never as a new status; a site that goes lowers its
/// file's count here (a file that reaches none drops its line). Counted in the crate's code: every
/// use of `Error::Status`, called or point-free (`.map_err(Error::Status)`), but not in comments,
/// match patterns, `*tests.rs` files or the top-level `#[cfg(test)] mod name { .. }` blocks (a
/// `#[cfg(test)]` item of another kind, or a `mod name;` declaration, hides no code).
#[test]
fn status_sites_only_shrink() {
const SITES: &[(&str, usize)] =
&[("core/blend.rs", 9), ("core/measure.rs", 1), ("core/offset.rs", 7), ("core/split.rs", 3), ("core/ssi.rs", 1), ("core/sweep.rs", 6)];
/// The lines of `text` outside its top-level inline test modules (rustfmt closes one with a
/// `}` alone at the start of a line).
fn code_lines(text: &str) -> Vec<&str> {
let lines: Vec<&str> = text.lines().collect();
let (mut out, mut i) = (Vec::new(), 0);
while i < lines.len() {
let next = lines.get(i + 1).copied().unwrap_or("");
let inline_mod = ["mod ", "pub mod ", "pub(crate) mod "].iter().any(|p| next.starts_with(p)) && next.ends_with(" {");
if lines[i] == "#[cfg(test)]" && inline_mod {
i += 2;
while i < lines.len() && lines[i] != "}" {
i += 1;
}
i += 1;
continue;
}
out.push(lines[i]);
i += 1;
}
out
}
/// The uses of `Error::Status` in `line` that make a status (not a match pattern).
fn uses(line: &str) -> usize {
const NAME: &str = "Error::Status";
line.match_indices(NAME)
.filter(|(i, _)| {
let rest = &line[i + NAME.len()..];
if rest.starts_with(|c: char| c.is_alphanumeric() || c == '_') {
return false;
}
let Some(args) = rest.strip_prefix('(') else { return true };
let after = args.find(')').map_or("", |j| args[j + 1..].trim_start());
!(after.starts_with("=>") || after.starts_with("if ") || after.starts_with('|'))
})
.count()
}
fn walk(dir: &std::path::Path, root: &std::path::Path, out: &mut Vec<(String, usize)>) {
let mut entries: Vec<_> = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().path()).collect();
entries.sort();
for p in entries {
if p.is_dir() {
walk(&p, root, out);
continue;
}
let name = p.file_name().unwrap().to_string_lossy().into_owned();
if !name.ends_with(".rs") || name.ends_with("tests.rs") {
continue;
}
let text = std::fs::read_to_string(&p).unwrap();
let n: usize = code_lines(&text).into_iter().filter(|l| !l.trim_start().starts_with("//")).map(uses).sum();
if n > 0 {
out.push((p.strip_prefix(root).unwrap().to_string_lossy().replace('\\', "/"), n));
}
}
}
// the counter itself: a point-free use counts, a pattern does not, nor a test module
assert_eq!(uses(" .map_err(Error::Status)?;"), 1);
assert_eq!(uses(" Error::Status(s) => s,"), 0);
assert_eq!(uses(" Err(Error::Status(1001))"), 1);
let module = "fn a() {}\n#[cfg(test)]\nmod tests {\n fn b() { Error::Status(1) }\n}\nfn c() {}\n#[cfg(test)]\nmod more_tests;\n#[cfg(test)]\nconst X: u8 = 0;";
assert_eq!(code_lines(module), ["fn a() {}", "fn c() {}", "#[cfg(test)]", "mod more_tests;", "#[cfg(test)]", "const X: u8 = 0;"]);
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut found = Vec::new();
walk(&root, &root, &mut found);
let expected: Vec<(String, usize)> = SITES.iter().map(|&(f, n)| (f.to_owned(), n)).collect();
for (file, n) in &found {
let listed = SITES.iter().find(|(f, _)| f == file).map_or(0, |&(_, m)| m);
assert!(*n <= listed, "{file} makes {n} bare statuses, {listed} listed: carry the typed refusal (Error::Refused), not a new status");
}
assert_eq!(found, expected, "fewer bare statuses than listed: lower the counts in SITES");
}
+102 -15
View File
@@ -135,8 +135,9 @@ pub struct Job {
method: String,
params: Value,
reply: Sender<ControlResponse>,
/// Screen position and registry label, kept together even when several commands share a panel.
stops: VecDeque<(Pos2, &'static str)>,
/// Screen position, registry label and, for a pick on the model, its place in the world; kept
/// together even when several commands share a panel.
stops: VecDeque<(Pos2, &'static str, Option<Vec3>)>,
chat_turn: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
/// Seconds per glide and per click for this request (within [`TIME_BUDGET`] in all).
glide: f64,
@@ -157,6 +158,8 @@ pub struct Job {
#[derive(Default)]
pub struct AgentCursor {
pub pos: Option<Pos2>,
/// The pick on the model the pointer rests on: it follows the pick when the view turns.
anchor: Option<Vec3>,
job: Option<Job>,
/// Click ripples: where and when.
ripples: Vec<(Pos2, f64)>,
@@ -189,6 +192,7 @@ impl AgentCursor {
return;
}
self.pos = Some(at);
self.anchor = None;
if press {
self.ripples.push((at, now));
}
@@ -252,12 +256,13 @@ fn button(command: &str) -> Option<Pos2> {
}
/// Where the pointer goes for a command: its toolbar button (or its panel), then its picks on
/// screen, at most [`MAX_PICK_STOPS`] of them.
pub fn stops(app: &SolveApp, command: &str, params: &Value) -> Vec<Pos2> {
let mut out: Vec<Pos2> = button(command).into_iter().collect();
/// screen, at most [`MAX_PICK_STOPS`] of them (each with its place in the world).
pub fn stops(app: &SolveApp, command: &str, params: &Value) -> Vec<(Pos2, Option<Vec3>)> {
let mut out: Vec<_> = button(command).into_iter().map(|p| (p, None)).collect();
let Some(rect) = app.viewport.rect else { return out };
let proj = crate::viewport::projection(app, rect);
let on_screen: Vec<Pos2> = picks(app, command, params).into_iter().filter_map(|w| proj.to_screen(w).filter(|p| rect.contains(*p))).collect();
let on_screen: Vec<_> =
picks(app, command, params).into_iter().filter_map(|w| proj.to_screen(w).filter(|p| rect.contains(*p)).map(|p| (p, Some(w)))).collect();
out.extend(summarise(&on_screen, MAX_PICK_STOPS));
out
}
@@ -356,19 +361,22 @@ pub fn intercept(app: &mut SolveApp, req: &crate::control::ControlRequest) -> bo
let Some(cmd) = params.get("command").or(params.get("id")).and_then(Value::as_str) else { return false };
let inner = params.get("params").cloned().unwrap_or(json!({}));
let label = solidcraft_engine::find_command(cmd).map_or("", |c| c.label);
stops(app, cmd, &inner).into_iter().map(|at| (at, label)).collect()
stops(app, cmd, &inner).into_iter().map(|(at, world)| (at, label, world)).collect()
}
// A batch is acted out as one request; one the app refuses is answered at once.
"engine.batch" => match params.get("commands").and_then(Value::as_array) {
Some(steps) if crate::control::batch_refusal(steps).is_none() => batch_targets(steps).into(),
Some(steps) if crate::control::batch_refusal(steps).is_none() => {
batch_targets(steps).into_iter().map(|(at, label)| (at, label, None)).collect()
}
_ => return false,
},
_ => return false,
};
if s.speed == Speed::Instant {
// No animation: the command runs now; the pointer jumps to where it acted.
if let Some((last, _)) = stops.back() {
if let Some((last, _, world)) = stops.back() {
app.agent.pos = Some(*last);
app.agent.anchor = *world;
app.agent.ripples.push((*last, app.now));
}
return false;
@@ -410,6 +418,12 @@ fn ease(t: f64) -> f64 {
t * t * (3.0 - 2.0 * t)
}
/// Where the viewport draws a world point now, if it is projectable and inside the viewport.
fn on_screen(app: &SolveApp, w: Vec3) -> Option<Pos2> {
let rect = app.viewport.rect?;
crate::viewport::projection(app, rect).to_screen(w).filter(|p| rect.contains(*p))
}
/// Move the pointer along; when the last stop is clicked, run the held request and reply.
pub fn step(app: &mut SolveApp, ctx: &egui::Context) {
let now = app.now;
@@ -417,7 +431,22 @@ pub fn step(app: &mut SolveApp, ctx: &egui::Context) {
if !app.agent.ripples.is_empty() {
ctx.request_repaint();
}
let Some(job) = app.agent.job.as_mut() else { return };
// The pick the pointer rests on or glides to, where the view draws it now (else it keeps its
// last place).
let held = match app.agent.job.as_ref().and_then(|j| j.stops.front()) {
Some(stop) => stop.2,
None => app.agent.anchor,
};
let held = held.and_then(|w| on_screen(app, w));
let Some(job) = app.agent.job.as_mut() else {
if let Some(p) = held {
app.agent.pos = Some(p);
}
return;
};
if let (Some(p), Some(stop)) = (held, job.stops.front_mut()) {
stop.0 = p;
}
if crate::control::chat_turn_cancelled(job.chat_turn.as_deref()) {
if let Some(job) = app.agent.job.take() {
let _ = job.reply.send(crate::control::cancelled_chat_response());
@@ -434,7 +463,11 @@ pub fn step(app: &mut SolveApp, ctx: &egui::Context) {
}
let (glide, click) = (job.glide, job.click);
ctx.request_repaint();
let Some(&(to, _)) = job.stops.front() else {
let Some(&(to, _, world)) = job.stops.front() else {
// Waiting for the command to compute: the pointer stays on its pick.
if let Some(p) = held {
app.agent.pos = Some(p);
}
if ahead_is_in(app) {
finish(app, ctx);
}
@@ -444,6 +477,7 @@ pub fn step(app: &mut SolveApp, ctx: &egui::Context) {
if !job.clicked {
let e = ease(t) as f32;
app.agent.pos = Some(job.from + (to - job.from) * e);
app.agent.anchor = None;
if t >= 1.0 {
job.clicked = true;
job.started = now;
@@ -451,8 +485,11 @@ pub fn step(app: &mut SolveApp, ctx: &egui::Context) {
}
return;
}
app.agent.pos = Some(to);
if now - job.started >= click {
job.stops.pop_front();
// The pointer rests here once the request is done.
app.agent.anchor = world;
job.from = to;
job.started = now;
job.clicked = false;
@@ -543,7 +580,7 @@ pub fn paint(app: &SolveApp, ctx: &egui::Context) {
painter.circle_stroke(*at, 6.0 + 22.0 * k, Stroke::new(3.0 * (1.0 - k) + 0.5, agent().gamma_multiply(1.0 - k)));
}
solidcraft_look::pointer::paint(&painter, p);
if let Some((_, label)) = app.agent.job.as_ref().and_then(|job| job.stops.front()).filter(|(_, label)| !label.is_empty()) {
if let Some((_, label, _)) = app.agent.job.as_ref().and_then(|job| job.stops.front()).filter(|(_, label, _)| !label.is_empty()) {
use crate::ui_kit::{Container, Surface};
let t = solidcraft_theme::Tokens::get();
Container::new(ctx, Surface::Popup)
@@ -597,10 +634,10 @@ mod tests {
let at = Vec3::new(20.0, 15.0, 20.0);
let s = stops(&a, "solid.hole", &json!({"position": [20, 15, 20], "diameter": 5}));
let want = proj.to_screen(at).unwrap();
assert!(s.iter().any(|p| p.distance(want) < 0.01), "{s:?} vs {want:?}");
assert!(s.iter().any(|(p, _)| p.distance(want) < 0.01), "{s:?} vs {want:?}");
assert!(rect.contains(want));
// Off-screen places are skipped rather than pointed at outside the view.
assert!(stops(&a, "solid.hole", &json!({"position": [1e6, 0, 0]})).iter().all(|p| rect.contains(*p)));
assert!(stops(&a, "solid.hole", &json!({"position": [1e6, 0, 0]})).iter().all(|(p, _)| rect.contains(*p)));
}
/// A ring of points around the box's top, `n` of them, in active-sketch coordinates.
@@ -624,7 +661,7 @@ mod tests {
let s = stops(&a, "sketch.line", &p);
assert!(s.len() >= MAX_PICK_STOPS && s.len() <= 1 + MAX_PICK_STOPS, "{s:?}");
// Few points are all visited.
assert_eq!(stops(&a, "sketch.line", &json!({"points": ring(2)})).iter().filter(|p| a.viewport.rect.unwrap().contains(**p)).count(), 2);
assert_eq!(stops(&a, "sketch.line", &json!({"points": ring(2)})).iter().filter(|(p, _)| a.viewport.rect.unwrap().contains(*p)).count(), 2);
assert_eq!(summarise(&[1, 2, 3, 4, 5, 6, 7], 3), vec![1, 4, 7]);
assert_eq!(summarise(&[1, 2], 3), vec![1, 2]);
assert_eq!(summarise(&[1, 2, 3], 1), vec![1]);
@@ -744,6 +781,56 @@ mod tests {
assert_eq!(h.app.agent.pos, Some(pos2(700.0, 360.0)));
}
/// Acts out a command, then turns the view and draws a frame.
fn rest_then_turn_view(h: &mut crate::scenario::Harness, params: Value) -> (Pos2, Pos2) {
let (req, rx) = crate::control::ControlRequest::new("engine.execute", params);
assert!(intercept(&mut h.app, &req));
h.frames(120);
assert_eq!(rx.try_recv().unwrap()["ok"], true);
let before = h.app.agent.pos.unwrap();
h.app.cam.orbit(150.0, 40.0);
h.frames(2);
(before, h.app.agent.pos.unwrap())
}
#[test]
fn the_pointer_stays_on_its_pick_when_the_view_turns() {
let mut h = crate::scenario::Harness::new();
h.call("ui.agent_cursor", json!({"show": true, "speed": 1}));
let at = Vec3::new(10.0, 10.0, 10.0);
let sphere = json!({"command": "solid.sphere", "params": {"center": [10, 10, 10], "radius": 3}});
let (before, after) = rest_then_turn_view(&mut h, sphere);
let rect = h.app.viewport.rect.unwrap();
let want = crate::viewport::projection(&h.app, rect).to_screen(at).unwrap();
assert!(after.distance(want) < 0.01, "{after:?} vs {want:?}");
assert!(after.distance(before) > 1.0, "the view turned, so the pick moved: {before:?} -> {after:?}");
// Turned while the pointer is still on its way, it arrives where the view now draws the pick.
let at = Vec3::new(12.0, 8.0, 10.0);
let (req, rx) =
crate::control::ControlRequest::new("engine.execute", json!({"command": "solid.sphere", "params": {"center": [12, 8, 10], "radius": 3}}));
assert!(intercept(&mut h.app, &req));
h.frames(3);
assert!(rx.try_recv().is_err(), "still on its way");
h.app.cam.orbit(40.0, 10.0);
h.frames(120);
assert_eq!(rx.try_recv().unwrap()["ok"], true);
let want = crate::viewport::projection(&h.app, rect).to_screen(at).unwrap();
assert!(rect.contains(want), "the pick is still in the view: {want:?}");
let arrived = h.app.agent.pos.unwrap();
assert!(arrived.distance(want) < 0.01, "{arrived:?} vs {want:?}");
// On a toolbar button (no pick) it stays at the button.
let box_params = json!({"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10}});
let (before, after) = rest_then_turn_view(&mut h, box_params);
assert_eq!(before, after);
// Pointer input the agent sends itself clears the anchor.
h.call("ui.move", json!({"x": 420, "y": 380}));
h.app.cam.orbit(50.0, 0.0);
h.frames(2);
assert_eq!(h.app.agent.pos, Some(pos2(420.0, 380.0)));
}
#[test]
fn speeds_read_old_and_new_settings() {
// Saved settings from before #79: slow and normal are 1×, instant stays.
+1 -1
View File
@@ -332,7 +332,7 @@ nice -n 10 cargo run -p goldens -- --solidcraft-cases '3526-*' # ids (<fi
writes the body `id`; the last writes `result`.
* **Fixes** (`fixes`, optional): `Kernel::fixes` (`UpstreamFixes`) fields the case turns
on, by name, for a case that needs an opt-in fix of an upstream defect (for example
`434-*`: `sweep_frame_on_planar_path_normal`, `cover_refuses_folded_continuation`).
`434-*`: `cover_refuses_folded_continuation`).
* **Known refusal** (`refused_by`, optional): for a case the kernel refuses on purpose,
the `issue` that tracks it, the `step` id that fails and text its `error` contains. Only
that step failing with that error defers the case on the issue; any other error stays
@@ -237,7 +237,7 @@
{"case":"gen0186","deferred":null,"error":null,"ints":{"extend":[0,0],"solver_status":[0],"status":[1006]},"topology":null,"values":{"fillet_point_step":[0.00001],"g1":[0.5,0.8058836008053156],"knots_in":[0.5,0.8058836008053156]}}
{"case":"gen0187","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-436.50242195439597,-436.50242195439597],"point":{"blocks":[[-1529.9378731663937,19707.66440960015,-170724.05628405436]],"n":80}}}
{"case":"gen0188","deferred":null,"error":null,"ints":{"extend":[1,1],"solver_status":[0],"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"g1":[0.8086086830965546],"knots_in":[0.8086086830965546]}}
{"case":"gen0189","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-63589.196620918556,63589.196620918556],"point":{"blocks":[[-5197450.252884022,20283996.55232335,-359227109.5068444]],"n":80}}}
{"case":"gen0189","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-63589.196620918556,63589.196620918556],"point":{"blocks":[[-5409431.087460066,20736700.68635911,-369507220.65923965]],"n":80}}}
{"case":"gen0190","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[468891.5071672227,468891.5071672227],"point":{"blocks":[[1313572.8468287329,2461924.6583010578,36903680.949544035]],"n":80}}}
{"case":"gen0191","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.13462148002526228,0.13462148002526228],"point":{"blocks":[[18.133665847189107,45.6919903299749,584.97632626476]],"n":80}}}
{"case":"gen0192","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.000023028127890592776,-0.000023028127890592776],"point":{"blocks":[[8.499931116909373,19.25328375002503,-43.690276481991106]],"n":80}}}
@@ -329,7 +329,7 @@
{"case":"gen0278","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.29988251018983964,-0.29988251018983964],"point":{"blocks":[[27.672845126445942,35.970021969260515,589.6735825955082]],"n":80}}}
{"case":"gen0279","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.45551895555168714,-0.45551895555168714],"point":{"blocks":[[26.62903800169477,39.71592910293169,442.53328304135624]],"n":80}}}
{"case":"gen0280","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[0.4475236202029802,-0.4475236202029802],"point":{"blocks":[[25.999944164972142,50.99682934233869,879.1933223273726]],"n":80}}}
{"case":"gen0281","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[123313.9716096632,123313.9716096632],"point":{"blocks":[[-11003044.26053301,33443193.279643424,-227098110.88102585]],"n":80}}}
{"case":"gen0281","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[123313.9716096632,123313.9716096632],"point":{"blocks":[[-78544753.7741623,147542361.66173863,-3606130597.4620395]],"n":80}}}
{"case":"gen0282","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.001],"offsets":[-0.49647997551037515,-0.24823998775518757],"point":{"blocks":[[20.04228658366335,60.41982795366944,225.83891739598712]],"n":80}}}
{"case":"gen0283","deferred":null,"error":null,"ints":{"status":[0]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.4085014933706985,0.4085014933706985],"point":{"blocks":[[2.1715672491342284,54.69636389649634,-56.974130468373176]],"n":80}}}
{"case":"gen0284","deferred":null,"error":null,"ints":{"status":[1001]},"topology":null,"values":{"fillet_point_step":[0.00001],"offsets":[-0.6442329828030816,0.6442329828030816],"point":{"blocks":[[-2.6088013041253846,144.08407232430747,-807.6284619822886]],"n":80}}}
-3
View File
@@ -244,9 +244,6 @@
"gen2966": "tangency-labs/solidcore#2717: step 0 names D(3,0), overwritten by D(1,2) at out[11]",
"upper_half_slot_overwrite": "tangency-labs/solidcore#2717: step 1 names D(2,0), overwritten by D(1,2) at out[26]"
},
"surface.surface_approximate": {
"gen0381": "tangency-labs/solidcore#2507: approximation reports success and a returned surface although achieved tolerance 0.5459967665063701 exceeds requested tolerance 0.0013812901040403118"
},
"topology.brep_local_operation": {
"gen0014": "tangency-labs/solidcore#2981: tilted disk returns three non-collinear straight boundary carriers; an affine image of the semicircle-and-diameter boundary has only one",
"gen0019": "tangency-labs/solidcore#2981: tilted disk returns three non-collinear straight boundary carriers; an affine image of the semicircle-and-diameter boundary has only one",
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -15057,7 +15057,7 @@
{"case":"review2_std_bnd_neg_h_oob_es_-10_-1_00","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":[1.4772116295183115,-0.2604722665003954,-1.0],"status":[1000.0]}}
{"case":"review2_std_bnd_neg_h_oob_es_0_0_31","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":{"blocks":[[1.5171433585232228,2.5177632265166636,5.135559982163637]],"n":24},"status":[1000.0]}}
{"case":"review2_std_bnd_neg_h_oob_ev_0.5_0_21","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":{"blocks":[[7.044155877284281,78.86753236814712,79.9562773986042]],"n":18},"status":[1000.0]}}
{"case":"review2_std_bnd_neg_h_oob_icb8","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":[],"status":[-1002.0]}}
{"case":"review2_std_bnd_neg_h_oob_icb8","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":{"blocks":[[14420.828427124747,14430.48528137424,30606.24011537018]],"n":56},"status":[1000.0]}}
{"case":"review2_std_bnd_neg_h_oob_inv9_0.5","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":[],"status":[1001.0]}}
{"case":"review2_std_bnd_neg_h_pos2","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":[1.0,0.0,-1.0,2.5000000000000036,2.000000000000001],"status":[1000.0]}}
{"case":"review2_std_bnd_neg_h_pos5","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":[1.0,0.0,0.0,7.0000000000000036,2.000000000000001],"status":[1000.0]}}
@@ -15450,7 +15450,7 @@
{"case":"review2_std_semi_unb_120_iso1_-0.2","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":{"blocks":[[14422.047702388183,14428.359324197401,45074.06272469183]],"n":58},"status":[1000.0]}}
{"case":"review2_std_semi_unb_120_oob_es_360_3_11","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":[-4.196152422706635,1.0277609266404183e-15,3.0,-1.7320508075688783,4.24230095489963e-16,1.0,-1.7937812093223203e-17,-0.07323667569176764,-0.0,0.0,0.0,0.0],"status":[1000.0]}}
{"case":"review2_std_semi_unb_120_oob_ev_-0.1_1.2_21","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":{"blocks":[[80.5629051598358,177.9657591937833,1085.5124521766468]],"n":18},"status":[1000.0]}}
{"case":"review2_std_semi_unb_120_oob_ic9","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":[],"status":[-1002.0]}}
{"case":"review2_std_semi_unb_120_oob_ic9","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":[],"status":[1001.0]}}
{"case":"review2_std_semi_unb_120_tr_id","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":248},"status":[1000.0]}}
{"case":"review2_std_semi_unb_120_tr_idsc","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":248},"status":[1000.0]}}
{"case":"review2_std_semi_unb_120_tr_near","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[1000.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":248},"status":[1000.0]}}
@@ -1359,7 +1359,7 @@
{"case":"gen1264","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":[0.0,0.0,0.0,0.0,0.0,1007.0,1001.0]}}
{"case":"gen1266","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[30432.215458758114,30458.29526403351,472900.47055325436]],"n":54}}}
{"case":"gen1267","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[91560.57207046088,91688.73159676377,3628080.1360532586]],"n":129}}}
{"case":"gen1268","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[91749.73271093576,92177.3819392097,5016135.0212976895]],"n":154}}}
{"case":"gen1268","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[91752.27760903546,92178.97939810494,5016255.163182151]],"n":154}}}
{"case":"gen1269","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[91740.00274331491,93693.74118795947,4101885.839646249]],"n":135}}}
{"case":"gen1270","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[30408.411450051834,30430.694684738628,472101.32902111945]],"n":54}}}
{"case":"gen1271","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[31411.089322527532,31413.044595465522,562740.2444015625]],"n":76}}}
@@ -1810,7 +1810,7 @@
{"case":"gen1737","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[29847.345690061786,31831.810669040908,1312406.4322319615]],"n":83}}}
{"case":"gen1738","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[30433.937507023224,30444.565114143283,625001.0417647376]],"n":59}}}
{"case":"gen1739","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[7504.891638353645,136823.3640135733,-369085.334516311]],"n":186}}}
{"case":"gen1740","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[76061.50659359912,76063.12907658475,4980583.8086414505]],"n":155}}}
{"case":"gen1740","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[76067.900498269,76069.52298125463,4981082.972090172]],"n":155}}}
{"case":"gen1741","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[31486.77928250074,31520.6960683572,577093.7907183073]],"n":73}}}
{"case":"gen1742","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[92380.19460693261,92529.97926873079,3663639.543715459]],"n":147}}}
{"case":"gen1743","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[27806.873231672107,38436.394755773166,225937.0909352196]],"n":106}}}
@@ -441,7 +441,7 @@
{"case":"gen0401","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.324891261185069,-1.4252116561401635,4.0],"status":[0.0]}}
{"case":"gen0402","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[3.97449082495523,-0.15037924885983664],"status":[0.0]}}
{"case":"gen0403","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0],"status":[0.0]}}
{"case":"gen0404","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[0.11400890177583278,33.35665975483444,4.372775325034457]],"n":48},"status":[0.0]}}
{"case":"gen0404","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[0.5967790265467282,34.40203365640425,4.618347812116479]],"n":48},"status":[0.0]}}
{"case":"gen0405","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0],"status":[0.0]}}
{"case":"gen0406","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[3.859075871277968,-2.4348664284036032,-7.393403557723783,-0.7982887284021153,-2.765441031429951,3.103725443844048,-1.9389550672891787,10.027163664075838,8.435566973983684,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0407","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":20},"status":[0.0]}}
@@ -1503,7 +1503,7 @@
{"case":"review4_circ_oob0_drop1","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,2.0,1.0,1.0,6.944271909999158,0.3260283839939158,-7.0],"status":[0.0]}}
{"case":"review4_circ_oob0_drop2","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,2.0,1.0,1.0,0.23606797749978978,0.6739716160060842,-3.0],"status":[0.0]}}
{"case":"review4_circ_oob0_drop3","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,2.0,1.0,1.0,0.37697286480094266,0.6857145143307319,-2.699999999953434],"status":[0.0]}}
{"case":"review4_circ_oob0_eval0","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[10.239707418972689,10.240879663236907,27.116319977689674]],"n":48},"status":[0.0]}}
{"case":"review4_circ_oob0_eval0","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[11.034286717046445,11.035526453033327,28.47305725972208]],"n":48},"status":[0.0]}}
{"case":"review4_circ_oob0_eval1","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[11.13431251238852,11.139268058150607,28.654454125352014]],"n":48},"status":[0.0]}}
{"case":"review4_circ_oob0_eval2","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[11.034286717080732,11.035526453068854,28.473057259783705]],"n":48},"status":[0.0]}}
{"case":"review4_circ_oob0_eval3","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[11.812504631512276,11.867714911366875,30.294375552879156]],"n":48},"status":[0.0]}}
@@ -1607,10 +1607,10 @@
{"case":"review4_line_oob0_drop1","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"review4_line_oob0_drop2","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"review4_line_oob0_drop3","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"review4_line_oob0_eval0","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[-111.0,125.0,-182.0]],"n":48},"status":[0.0]}}
{"case":"review4_line_oob0_eval1","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[27.0,27.0,140.0]],"n":48},"status":[0.0]}}
{"case":"review4_line_oob0_eval0","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[9.0,9.0,98.0]],"n":48},"status":[0.0]}}
{"case":"review4_line_oob0_eval1","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[15.0,15.0,112.0]],"n":48},"status":[0.0]}}
{"case":"review4_line_oob0_eval2","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[9.000000006,9.000000006,98.000000014]],"n":48},"status":[0.0]}}
{"case":"review4_line_oob0_eval3","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[309.0,309.0,798.0]],"n":48},"status":[0.0]}}
{"case":"review4_line_oob0_eval3","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[15.0,15.0,112.0]],"n":48},"status":[0.0]}}
{"case":"review4_line_oob0_xf0_0","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":88},"status":[0.0]}}
{"case":"review4_line_oob0_xf0_1","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":88},"status":[0.0]}}
{"case":"review4_line_oob0_xf1_0","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":88},"status":[0.0]}}
@@ -1965,7 +1965,7 @@
{"case":"review_hugeh_drop_min","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,2.0,1.0,1.0,225439.87986352856,-1.0,300000.3999999999],"status":[0.0]}}
{"case":"review_hugeh_drop_sub_sw","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,2.0,1.0,1.0,375589.2071999666,0.4,-0.8],"status":[0.0]}}
{"case":"review_hugeh_eval5","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"review_hugeh_eval_out","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[128284.96390500593,728288.9639050061,929136.4187280894]],"n":27},"status":[0.0]}}
{"case":"review_hugeh_eval_out","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[128286.96390500593,728286.9639050061,929142.4187280894]],"n":27},"status":[0.0]}}
{"case":"review_hugeh_evalpt_out_oob","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[118516.96761778292,191773.5916444934,-299997.0],"status":[0.0]}}
{"case":"review_hugeh_reparam_sw0","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[256024.62973305513,7855830.278079599,10560697.153241202]],"n":104},"status":[0.0]}}
{"case":"review_hugeh_reparam_sw1","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[256041.62973305513,7855837.278079599,10298970.315877281]],"n":104},"status":[0.0]}}
@@ -1984,7 +1984,7 @@
{"case":"review_hyp_drop_min","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,2.0,1.0,1.0,1.0695128947084926,0.17665129149833825,-0.6000000003259629],"status":[0.0]}}
{"case":"review_hyp_drop_sub_sw","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,2.0,1.0,1.0,1.4641918699227021,0.4,0.17665129149833828],"status":[0.0]}}
{"case":"review_hyp_eval5","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"review_hyp_eval_out","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[13.078596390500593,15.078596390500593,135.91364187280894]],"n":27},"status":[0.0]}}
{"case":"review_hyp_eval_out","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[15.078596390500593,15.078596390500593,141.91364187280894]],"n":27},"status":[0.0]}}
{"case":"review_hyp_evalpt_out_oob","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[2.3516967617782925,-1.072640835550661,4.0],"status":[0.0]}}
{"case":"review_hyp_reparam_sw0","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29190.849362721303,29865.05687181977,616753.6709253237]],"n":104},"status":[0.0]}}
{"case":"review_hyp_reparam_sw1","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29207.849362721303,29872.05687181977,617747.0573620048]],"n":104},"status":[0.0]}}
@@ -2003,7 +2003,7 @@
{"case":"review_par_drop_min","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,2.0,1.0,1.0,0.6483751020349579,0.481089134572278,0.39999999990686774],"status":[0.0]}}
{"case":"review_par_drop_sub_sw","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,2.0,1.0,1.0,0.6483751020349579,0.4,0.481089134572278],"status":[0.0]}}
{"case":"review_par_eval5","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"review_par_eval_out","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[3.5,7.5,42.5]],"n":27},"status":[0.0]}}
{"case":"review_par_eval_out","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[5.5,5.5,48.5]],"n":27},"status":[0.0]}}
{"case":"review_par_evalpt_out_oob","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[2.0,-2.0,3.0],"status":[0.0]}}
{"case":"review_par_reparam_sw0","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":104},"status":[0.0]}}
{"case":"review_par_reparam_sw1","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":104},"status":[0.0]}}
@@ -2025,7 +2025,7 @@
{"case":"review_tinyp_drop_min","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,2.0,1.0,1.0,0.7265455319524028,-2.0,0.3998000000137836],"status":[0.0]}}
{"case":"review_tinyp_drop_sub_sw","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,2.0,1.0,1.0,0.72685503258903,0.4,-1.8],"status":[0.0]}}
{"case":"review_tinyp_eval5","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"review_tinyp_eval_out","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[-0.9952,3.0043999999999995,0.04320000000000053]],"n":27},"status":[0.0]}}
{"case":"review_tinyp_eval_out","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[1.0047999999999997,1.0047999999999997,6.0432]],"n":27},"status":[0.0]}}
{"case":"review_tinyp_evalpt_out_oob","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0021,-0.002,3.0002],"status":[0.0]}}
{"case":"review_tinyp_reparam_sw0","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":104},"status":[0.0]}}
{"case":"review_tinyp_reparam_sw1","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":104},"status":[0.0]}}
@@ -139,7 +139,7 @@
{"case":"gen0049","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.4100115650337681,-3.9887940162857882,-2.561995000785807,0.010492102390852708,0.0,0.015162277039431906,-0.0,0.07935722120521105,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0050","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":20},"status":[0.0]}}
{"case":"gen0051","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203}}}
{"case":"gen0052","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[180.84860962098946,-93.27342223079216],"status":[0.0]}}
{"case":"gen0052","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[171.10481566161275,-93.27342223079216],"status":[0.0]}}
{"case":"gen0053","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[4.184842975578116,2.846079746820902,2.1415537254619523],"status":[0.0]}}
{"case":"gen0054","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30600.602182678995,31306.051145487003,1805291.0217127076],[-65.76976794785696,371.8727564298443,-2467.0575184021236]],"n":415}}}
{"case":"gen0055","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14426.743062189442,14436.028866809054,88425.96812731859]],"n":60},"status":[0.0]}}
@@ -148,7 +148,7 @@
{"case":"gen0058","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0,0.0],"status":[0.0]}}
{"case":"gen0059","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203}}}
{"case":"gen0060","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,0.0],"status":[0.0]}}
{"case":"gen0061","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14386.307534766014,14494.033276441378,87218.62971503517]],"n":60},"status":[0.0]}}
{"case":"gen0061","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14380.63622970681,14494.033276441378,87176.09492709114]],"n":60},"status":[0.0]}}
{"case":"gen0062","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0063","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14421.93482451293,14455.515237530037,88456.46177455962]],"n":60},"status":[0.0]}}
{"case":"gen0064","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203}}}
@@ -271,7 +271,7 @@
{"case":"gen0181","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[["NaN","NaN","NaN"]],"n":24},"status":[0.0]}}
{"case":"gen0182","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14182.03978596602,14475.318444434319,43272.48390922456]],"n":57},"status":[0.0]}}
{"case":"gen0183","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[2.920994723120514,3.1862739688677886,-0.15350931048329683],"status":[0.0]}}
{"case":"gen0184","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14218.225196773094,14554.978047757655,41441.825066495556]],"n":57},"status":[0.0]}}
{"case":"gen0184","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14260.272351171794,14554.978047757655,41631.037261289704]],"n":57},"status":[0.0]}}
{"case":"gen0185","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14421.942747038782,14445.411403527294,45023.97393892388]],"n":57},"status":[0.0]}}
{"case":"gen0186","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,0.0],"status":[0.0]}}
{"case":"gen0187","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[["NaN","NaN","NaN"]],"n":24},"status":[0.0]}}
@@ -346,7 +346,7 @@
{"case":"gen0256","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[4.045861172961799,2.791506923940717,-4.822855168639201,1.0,259.8026108731876,1.0],"status":[0.0]}}
{"case":"gen0257","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0258","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14046.320433328174,14089.589439191814,42378.50876108525]],"n":36},"status":[0.0]}}
{"case":"gen0259","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0648073416517736,1.199218327003228,-0.36554872688804285,1.0,153.91068538477188,1.0],"status":[0.0]}}
{"case":"gen0259","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.056398047914386,1.5499503341938334,-0.36554872688804285,1.0,149.17954822171788,1.0],"status":[0.0]}}
{"case":"gen0260","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":20},"status":[0.0]}}
{"case":"gen0261","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[-2.912786020687666,0.4499537658370061,-2.838147522176005,0.08003582407674935,-0.04207247200146253,2.648153513061051,-0.022978687035656108,-0.04371309945934532,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0262","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[["NaN","NaN","NaN"]],"n":24},"status":[0.0]}}
@@ -416,7 +416,7 @@
{"case":"gen0326","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14404.344713241093,14496.010927645719,87704.31957809815]],"n":60},"status":[0.0]}}
{"case":"gen0327","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[5.115409835591971,1.7651773934142838,-0.31264350134080776],"status":[0.0]}}
{"case":"gen0328","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14364.10072140648,14474.15223777607,86813.10878591676]],"n":60},"status":[0.0]}}
{"case":"gen0329","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14467.442671391475,14497.130340370653,89586.38562767414]],"n":60},"status":[0.0]}}
{"case":"gen0329","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14439.842472336148,14497.130340370653,89379.3841347592]],"n":60},"status":[0.0]}}
{"case":"gen0330","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203}}}
{"case":"gen0331","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[["NaN","NaN","NaN"]],"n":24},"status":[0.0]}}
{"case":"gen0332","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[-1.3605756095314936,-0.151271897488651,-1.7768744522564184],"status":[0.0]}}
@@ -428,7 +428,7 @@
{"case":"gen0338","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0],"status":[0.0]}}
{"case":"gen0339","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[57.42878649638192,-0.005832809397407934],"status":[0.0]}}
{"case":"gen0340","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.3549260809782873,1.3679682326421303,-5.833402107362909],"status":[0.0]}}
{"case":"gen0341","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[-6.700176938643628,-3.357395923882631,2.6819422214491437,1.0,129.95724729768597,1.0],"status":[0.0]}}
{"case":"gen0341","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[-6.654514455177553,-3.3231040753908836,2.7324075544640247,1.0,127.97973993307939,1.0],"status":[0.0]}}
{"case":"gen0342","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[5.032243393776547,-1.749255977392718,0.958126685213412],"status":[0.0]}}
{"case":"gen0343","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0],"status":[0.0]}}
{"case":"gen0344","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14445.674591908042,14568.462638370058,87587.88912933407]],"n":60},"status":[0.0]}}
@@ -523,7 +523,7 @@
{"case":"gen0433","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14035.85928468635,14069.103019922324,42086.18185513708]],"n":26},"status":[0.0]}}
{"case":"gen0434","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[14052.513856075087,14078.834925277404,42380.175819615964]],"n":27},"status":[0.0]}}
{"case":"gen0435","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203}}}
{"case":"gen0436","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[-3.619929122494868e-15,1.797055921200299],"status":[0.0]}}
{"case":"gen0436","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0118009489537107e-15,1.797055921200299],"status":[0.0]}}
{"case":"gen0437","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.3884716884654378,2.923716484354393,-1.1000832412841655],"status":[0.0]}}
{"case":"gen0438","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[["NaN","NaN","NaN"]],"n":24},"status":[0.0]}}
{"case":"gen0439","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[-4.772666013962317,3.377252862387458,-2.1026658300473335,-0.010586252214888452,-0.0029027919921837016,-0.01137212501201693,-0.020779910868203452,-0.01489923915896273,0.023147016876424953,0.0,0.0,0.0],"status":[0.0]}}
@@ -65,7 +65,7 @@
{"case":"gen0039","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30340.433051511613,30932.5606464786,1820927.9232843728],[77.10171151221616,167.8153131819559,3189.371186227484]],"n":331},"status":[0.0]}}
{"case":"gen0040","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[209.22154681086224,0.5911137647154503,4.0],"status":[0.0]}}
{"case":"gen0041","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[271.13769704094375,0.5830082693777043,4.0],"status":[0.0]}}
{"case":"gen0042","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[68.61339786513784,186.22174391286927,2969.994418621325]],"n":333},"status":[0.0]}}
{"case":"gen0042","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[48.11256095218669,71.78574250580726,869.660262323575]],"n":291},"status":[0.0]}}
{"case":"gen0043","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":147},"status":[0.0]}}
{"case":"gen0044","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203},"status":[0.0]}}
{"case":"gen0045","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":[],"status":[0.0]}}
@@ -105,7 +105,7 @@
{"case":"gen0079","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[[14327.155843002227,14360.726643693342,58939.27139935145]],"n":56},"status":[0.0]}}
{"case":"gen0080","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[[14427.161225301501,14502.827257603643,59863.96457008896]],"n":66},"status":[0.0]}}
{"case":"gen0081","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[64.62013677956263,125.14934757639627,3041.7397502204462]],"n":333}}}
{"case":"gen0082","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[-1245.4138918180133,1428.0333726570022,-149278.72547589545],[-948.1309957855681,1090.6344709538264,-118774.52948526443],[-802.9829307565625,934.6216543889982,-101853.81831041723],[-1282.1750845821025,1464.794565421092,-163147.8530471224],[-982.4921961134048,1146.725174353307,-122635.44041772405],[-851.3162033773506,980.4476766553662,-104926.10977774498],[-1139.1204214729742,1302.5176495947371,-168011.56702383497],[-1166.5505751355472,1349.1700559745352,-140598.97556245676],[-1028.5037458798133,1160.9782196303895,-132835.3596426937],[-942.8992147810836,1084.5669398312011,-133584.79105291184],[-1398.808052771028,1581.4275336100186,-170269.65030632075],[-1061.4922736560652,1215.6962504782853,-135503.06411626106],[-904.9914027216902,1034.122875999706,-107979.0115030878],[-1287.1505416541825,1460.5767711936296,-177250.940693365],[-826.636684338983,1267.8344440924898,-69623.5526430057],[73.0654296875,73.0654296875,3724.78955078125]],"n":4701},"status":[0.0]}}
{"case":"gen0082","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[-737.1636360474104,1316.0762581101783,-125163.79180414745],[-1349.5625498106601,1532.1820306496493,-178827.7581919008],[-1086.8409935288435,1265.2817237771294,-128448.30305401902],[-682.1290074940994,811.2604807721148,-93650.95009314532],[-915.5067677697263,1044.6382410477413,-117552.07828647364],[-773.4291701265265,912.5896448222238,-99386.82375005717],[-1007.709639108296,1190.329119947282,-140604.5153008329],[-1287.3384299597553,1469.9579107987447,-164196.7710271496],[-902.1159667891781,1068.8561953835012,-103223.64810484709],[-744.82702135752,873.9584946355357,-105073.32547959087],[-896.988996912772,1026.1204701907868,-113693.57255006001],[-797.1962640691185,948.0572404187775,-109234.79862529425],[-1107.8967642948833,1290.516245133871,-155001.93437208852],[-1321.378891428595,1503.9983722675847,-163441.39094282888],[-853.4602898047845,1008.5000167451449,-98805.39078149953],[-826.2664028994614,955.3978761774765,-115886.3493348148],[-962.7573365540987,1091.8888098321142,-115690.49735835107],[-900.8124520287619,1063.373930032383,-121963.60353870178],[-1228.6912915934581,1411.3107724324454,-172849.4854970503],[-1319.8361119111778,1502.4555927501676,-157187.98757297389],[-791.8055985855146,935.1448238719136,-92120.8662002456],[-915.4477061248416,1044.5791794028567,-128352.52811971057],[-899.8373159661315,1028.9687892441466,-107495.2028428952],[-913.2411845714045,1087.5031642289878,-121227.40328097362],[-1311.5315233974416,1494.1510042364307,-176788.94318542536],[-1167.816010878127,1350.435491717115,-140769.63461484358],[274.40248136536184,358.5796817989656,19762.253779222698]],"n":7893},"status":[0.0]}}
{"case":"gen0083","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30163.331807345396,30656.257355373706,1659886.6203435517]],"n":209},"status":[0.0]}}
{"case":"gen0084","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":52},"status":[0.0]}}
{"case":"gen0085","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[33.81623029329183,74.29002417460812,1385.4413018117118]],"n":315},"status":[0.0]}}
@@ -115,7 +115,7 @@
{"case":"gen0089","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203},"status":[0.0]}}
{"case":"gen0090","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":119},"status":[0.0]}}
{"case":"gen0091","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[285.2930221922122,0.05720722022163192,4.0],"status":[0.0]}}
{"case":"gen0092","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[-1.7976931348623157e+308,1.7976931348623157e+308,"-inf"],[-357.16837009305794,468.65876829775783,-44728.05877865019],[-282.4085174685347,400.55286696302454,-35027.77494004416],[-242.23941758690634,393.19834112068503,-31093.344801238538],[-233.56955446263171,399.97233618821866,-30938.815912681464],[-205.71581723027342,450.80613376827154,-20614.787071552262],[48.600228314914865,49.50739803999082,1727.9638672089109]],"n":1845},"status":[0.0]}}
{"case":"gen0092","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[-251.05469608945535,531.9567838569099,-46628.865548064],[-353.4924752959932,465.34204080744115,-44237.186670104566],[-277.77478653421394,398.1663025719575,-34192.50732303934],[-235.3681940271569,392.1259216106828,-29348.954815771347],[-238.2792548401387,400.62024724660677,-30386.165932495922],[-81.08849178099769,363.67231679818553,2664.198494685567]],"n":1761},"status":[0.0]}}
{"case":"gen0093","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0094","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0095","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":195},"status":[1.0]}}
@@ -124,7 +124,7 @@
{"case":"gen0098","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[21.99934749400926,36.89807091527288,214.0139254345047]],"n":281},"status":[0.0]}}
{"case":"gen0099","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[[29102.152355047543,29107.29243695342,940558.4790331611]],"n":124},"status":[0.0]}}
{"case":"gen0100","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30214.166142169266,30915.77655350364,1806024.3036037374],[43.84487114562552,100.58721747910879,2293.2459888655617]],"n":331},"status":[0.0]}}
{"case":"gen0101","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],[604.524158813029,604.524158813029,68999.01978859231],[658.1777597451761,658.1777597451761,91911.29374234189],[591.3246126618552,591.3246126618552,72336.93167265602],[753.6490536502105,753.6490536502105,101961.14301202864],[595.2998083196048,595.2998083196048,74675.13105462826],[768.4666086710728,768.4666086710728,93818.12407783276],[580.6234527478405,580.6234527478405,77971.93468273387],[617.6343002714236,617.6343002714236,66964.42799149937]],"n":2559},"status":[0.0]}}
{"case":"gen0101","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[544.0354257488368,544.0354257488368,69210.73328546698],[506.77569157224514,506.77569157224514,61094.356793503146]],"n":753},"status":[0.0]}}
{"case":"gen0102","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],[294.1609893234424,325.7459707903748,33462.284086829866],[315.8668388584256,335.9516663479384,36789.08793359075],[385.9583456216646,387.3850667571623,53106.13785089499],[394.2474308783384,423.0018803360589,54694.00950306439],[523.0731617983884,523.0731617983884,62036.02943528389],[26.7724609375,26.7724609375,661.17236328125]],"n":1833},"status":[0.0]}}
{"case":"gen0103","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[3266.9784142035533,12535.636931365654,39649.73051723068]],"n":289},"status":[0.0]}}
{"case":"gen0104","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":245},"status":[0.0]}}
@@ -191,7 +191,7 @@
{"case":"gen0165","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0166","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203},"status":[0.0]}}
{"case":"gen0167","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[33106.70608181323,33222.41982384974,1890250.2108424504]],"n":227},"status":[0.0]}}
{"case":"gen0168","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30350.19325935703,31075.725453605795,1824997.59000365],[61.73234165298125,63.74954779034116,6834.34277734736]],"n":415},"status":[0.0]}}
{"case":"gen0168","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30893.993022628965,30899.40584996718,1839451.4894761096],[59.14511080044788,61.65673953762306,6595.191996883162]],"n":415},"status":[0.0]}}
{"case":"gen0169","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":[],"status":[0.0]}}
{"case":"gen0170","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[43.92062649461591,78.26577226899809,2067.6003075956355]],"n":331},"status":[0.0]}}
{"case":"gen0171","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[99.06199937685278,436.41842743315635,12198.281572798502],[148.46689693219932,548.4712532704684,16684.765755996443],[191.07536906817353,592.2061613697107,21692.99745360325],[218.69911311030063,440.8929170310703,22880.73825965971]],"n":1215},"status":[0.0]}}
@@ -222,7 +222,7 @@
{"case":"gen0196","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":199},"status":[0.0]}}
{"case":"gen0197","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0198","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[27605.173791563233,30065.466113206752,1327274.2951177715]],"n":211},"status":[0.0]}}
{"case":"gen0199","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29909.79441701597,30151.28815455944,1787062.5540406317],[22.418112366612228,101.41156532071561,1883.3816059643618]],"n":331}}}
{"case":"gen0199","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29921.060496156122,30162.455991102324,1787440.0067397063],[22.418112366612228,101.41156532071561,1883.3816059643618]],"n":331}}}
{"case":"gen0200","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":[],"status":[0.0]}}
{"case":"gen0201","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0202","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":175},"status":[0.0]}}
@@ -259,7 +259,7 @@
{"case":"gen0233","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[[14426.18287326237,14454.653666190226,59800.890826561284]],"n":66},"status":[0.0]}}
{"case":"gen0234","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":119},"status":[1.0]}}
{"case":"gen0235","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0236","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30161.045603854614,31138.204065936836,1833123.51430028],[40.82722864105739,50.300476842024075,2211.080025631627]],"n":331},"status":[0.0]}}
{"case":"gen0236","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[28873.884584751126,30226.431949572434,1779986.9779836454],[43.374481917456876,43.90198462866803,2149.580921205964]],"n":331},"status":[0.0]}}
{"case":"gen0237","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[228.51706517202842,231.26749180415766,26348.69394868837],[123.3182428857451,188.0260977764288,13373.816483688634],[-11.995402175047543,187.89544545893907,-3048.150040399075],[56.92911136969772,187.91263477966655,9870.880808681424],[160.26950784847386,160.26950784847386,13772.055172325663]],"n":1425},"status":[0.0]}}
{"case":"gen0238","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[87.60068279087723,0.33035162159402387,4.0],"status":[0.0]}}
{"case":"gen0239","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[152.5307798606708,158.32764609435628,5332.4761063894075]],"n":333},"status":[0.0]}}
@@ -274,11 +274,11 @@
{"case":"gen0248","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[68.90032671276857,131.44015592334304,5924.590857089182]],"n":371},"status":[0.0]}}
{"case":"gen0249","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29300.24341233203,29698.170670179275,1360389.0915883996]],"n":175},"status":[0.0]}}
{"case":"gen0250","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203}}}
{"case":"gen0251","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[31395.903243230136,31901.521833898256,1915222.3545291557],[412.18134604496373,635.257903425118,26932.248818625965]],"n":331},"status":[0.0]}}
{"case":"gen0251","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[31387.138145418256,31892.646443671598,1914927.7321478387],[412.18134604496373,635.257903425118,26932.248818625965]],"n":331},"status":[0.0]}}
{"case":"gen0252","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[29783.869247786137,30490.09975270584,1793829.174678584]],"n":245},"status":[1.0]}}
{"case":"gen0253","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30928.34705294532,31206.093237899506,1841236.0266066224],[114.83113491742695,353.5683631402773,12681.865636163257]],"n":415},"status":[0.0]}}
{"case":"gen0254","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203}}}
{"case":"gen0255","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30503.84519345892,31838.395526608463,1796405.2657401667],[39.33885253866327,263.24150142098654,6925.038599944966]],"n":415},"status":[0.0]}}
{"case":"gen0255","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29883.44119896961,31095.097080374773,1775860.5038163783],[39.619521670416034,263.52217055273934,6963.770940126848]],"n":415},"status":[0.0]}}
{"case":"gen0256","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[27659.770667763034,30367.247058612047,1310819.721686675]],"n":139},"status":[0.0]}}
{"case":"gen0257","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0258","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[44.96034861920769,156.58863861715096,7641.818671714357]],"n":417}}}
@@ -352,7 +352,7 @@
{"case":"gen0326","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[29813.632743275248,30178.862271611724,1774998.5179560848],[22.71853882641926,41.939703414230095,576.9140753039794]],"n":289},"status":[1.0]}}
{"case":"gen0327","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[-100.51550223368639,-54.65196655229613,4.0],"status":[0.0]}}
{"case":"gen0328","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30907.270032492794,30997.081205610983,1851341.7153341782],[118.34851497590905,173.89919819038974,10313.756159526478]],"n":415},"status":[0.0]}}
{"case":"gen0329","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30242.643042477386,30600.330184823913,1808200.518957099]],"n":247},"status":[1.0]}}
{"case":"gen0329","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30242.01894297435,30600.32658832165,1808179.6447237702]],"n":247},"status":[1.0]}}
{"case":"gen0330","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[99.06094937706457,119.66120142149087,6800.210773464624]],"n":375},"status":[0.0]}}
{"case":"gen0331","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30438.774804623896,31208.135776664265,1817043.749499533],[158.41851550063862,368.3143094243644,12345.922554327151]],"n":415},"status":[0.0]}}
{"case":"gen0332","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":64},"status":[0.0]}}
@@ -391,7 +391,7 @@
{"case":"gen0365","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0366","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[304.7283158708193,304.7283158708193,41590.849291943676],[276.829046334285,279.02897954031215,34214.08309671351],[320.23106041231256,320.23106041231256,41095.4353162014],[254.2082157981373,257.3193911744161,31559.833982098007],[319.5153372152682,319.5153372152682,40344.37615571475],[124.25598241044773,285.21535529187446,11856.60618218183],[133.52748953469433,311.2763768291718,16231.596556921737],[-15.293700566797176,269.1148575198247,-625.029321965932],[48.92136730575797,306.4592162964581,3491.4858343549326],[-106.17873577124597,297.7172154735486,-10229.216756693904],[-53.06456482137884,282.70286631040995,-7907.042956323674],[17.936397176183906,297.7131287177723,6718.008936742521],[51.26291524181417,283.6894789181049,6457.987932529542],[124.65537591040061,318.6300124121915,17799.042770942986],[175.87623631668302,260.37233947860756,26630.143005713206],[319.4820040947647,319.4820040947647,41181.85794060652],[300.0668178264023,300.0668178264023,28740.52702785446]],"n":5079},"status":[0.0]}}
{"case":"gen0367","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":253},"status":[0.0]}}
{"case":"gen0368","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[35.694540599634784,842.3289559649277,3417.2912619644194],[-7.813734627850701,829.1709661491457,-4094.021335748565],[87.81055245775717,700.6704252542834,17117.774360256706]],"n":1005},"status":[0.0]}}
{"case":"gen0368","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[10.637404637723618,890.9466711083153,2919.6916388241852],[-37.928198161676974,843.7082774432287,-4663.614310978734],[-45.09554512813883,857.5896659186017,-8443.032264699516],[92.97486959517754,302.4952685134943,7276.140371843474]],"n":1131},"status":[0.0]}}
{"case":"gen0369","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":159}}}
{"case":"gen0370","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30383.89091429167,30840.54151394795,1390642.4393899525]],"n":175},"status":[0.0]}}
{"case":"gen0371","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
@@ -513,9 +513,9 @@
{"case":"gen0487","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203},"status":[0.0]}}
{"case":"gen0488","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[235.96884874395954,0.5572125311975293,4.0],"status":[0.0]}}
{"case":"gen0489","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],[139.5662740107048,153.13724779431374,22499.63847402025],[131.02063165157546,141.21820592101764,14324.238983210931],[142.0679288605742,156.48947669572613,15237.200636786527],[172.39477905731755,186.0463387294453,20056.508834654236],[103.82601324969191,148.81984736986385,15529.725610825788],[92.10291881629135,151.68177913486537,14169.824727441977],[72.80067074252553,146.44369888205364,7288.383236286807],[57.537816779728615,158.2408741028258,6865.10385002448],[115.9509266604279,188.12576163718484,13954.457901704034],[97.81055190176076,140.60033255311092,15110.412064780152],[126.8162591086444,151.37478404637196,19328.78490675373],[237.68745883101602,237.68745883101602,23073.73262668829]],"n":3525},"status":[0.0]}}
{"case":"gen0490","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30400.737445116174,31114.80264765432,1815287.3718729897],[42.11669985687213,140.34797085880848,2284.8378252978664]],"n":331},"status":[0.0]}}
{"case":"gen0490","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30397.200580424655,31114.831020394577,1815168.9049285576],[42.11669985687213,140.34797085880848,2284.8378252978664]],"n":331},"status":[0.0]}}
{"case":"gen0491","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0492","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30101.03752961847,30826.238013458486,1811679.7767134253],[46.26220274981899,47.565526137197836,2348.253309446046]],"n":331},"status":[0.0]}}
{"case":"gen0492","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30585.771268661912,30591.61174726016,1825382.158266039]],"n":247},"status":[0.0]}}
{"case":"gen0493","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30237.419583032686,30621.962511721675,1530851.695521024]],"n":227},"status":[0.0]}}
{"case":"gen0494","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0495","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
@@ -553,7 +553,7 @@
{"case":"gen0527","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30335.019255381267,30496.908196239867,1693847.7336492313]],"n":211},"status":[0.0]}}
{"case":"gen0528","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29853.88931778531,30814.5489395053,1489739.944910098]],"n":175},"status":[0.0]}}
{"case":"gen0529","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":137},"status":[0.0]}}
{"case":"gen0530","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30348.831473692648,30463.652278955255,1860451.8755221956],[288.5757645016082,358.5383167504445,23882.0646710568]],"n":417},"status":[0.0]}}
{"case":"gen0530","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30517.230509204124,30631.98705119334,1866094.1478551142],[288.5757645016082,358.5383167504445,23882.0646710568]],"n":417},"status":[0.0]}}
{"case":"gen0531","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0532","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1007.0]}}
{"case":"gen0533","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],[130.62928350067602,252.07322634273493,5783.645109730609],[75.0666831240045,216.41276680634135,10035.944629756286],[102.3060362606976,240.62197886028113,13772.32833286398],[105.87453678587237,241.33003913510444,14426.818138210154],[122.14803623595526,274.6644258976251,17687.72978218774],[154.6965008785348,342.72516488185494,21676.230378961936],[186.2685466295157,374.7814607244805,24537.47979387822],[203.741807584522,367.8529593056273,25770.382848082663],[203.97480945081492,363.53041751455277,25117.73429714298],[217.1055014828517,385.69189154511696,27893.95352641429],[260.0306562762822,451.06125685002655,34195.138553885256],[297.83471928601364,396.4567848893448,35257.73814303426]],"n":3567},"status":[0.0]}}
@@ -677,7 +677,7 @@
{"case":"gen0651","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[94.49868238883825,0.3770099799655936,4.0],"status":[0.0]}}
{"case":"gen0652","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":87},"status":[0.0]}}
{"case":"gen0653","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[121.45737923596502,0.5431784797987951,4.0],"status":[0.0]}}
{"case":"gen0654","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[31268.550073864502,31707.789445746068,1597286.0950007732]],"n":175},"status":[0.0]}}
{"case":"gen0654","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[31268.30353566186,31707.54505322787,1597277.8366905637]],"n":175},"status":[0.0]}}
{"case":"gen0655","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[606.0056936672436,681.0308791151142,62907.52372308237],[42.604039936527386,366.80280972585115,-1785.6631607138431],[23.473226519788096,318.1122401899592,11773.697644853324],[218.47745044036117,266.814116698237,24982.01874901759],[47.12214307143897,108.03431790365038,6040.886485405295],[389.35658879966786,395.7878975236645,58421.60578398914],[266.336999700802,306.62781703460575,25448.332946596187],[283.15482336180713,308.1676570216348,42718.36840090891],[384.7422038949858,400.70500718603176,46219.63582200169],[211.77568191954953,334.2348908540083,29239.832983274504],[452.40023185307837,664.8586968796423,58142.94657874513],[211.98076188560208,632.1279733182728,21368.014539372234],[253.46924199939738,569.9012640999533,33606.557909085],[132.67704646845874,597.7251790982442,11673.679565436432],[96.34061945273936,515.336266768876,19593.001602643537],[176.33749870558447,622.7213344779652,17142.588804275052],[-67.96216682870487,534.6220376318145,-8070.93097529493],[89.50404635299623,467.8486931244505,11834.49044005179],[-103.80981441362691,458.55150088220637,-16482.833122240587],[40.05153334941358,412.36626731318745,18120.442572837623],[124.0446345146224,392.92616368642723,9213.029796358962],[-51.743469030576726,319.6021516765523,-3275.2425378131693],[484.2108441259873,507.3120575746463,47693.84837878019]],"n":6591},"status":[0.0]}}
{"case":"gen0656","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30850.068890807062,30852.921630797235,1928105.0656166293],[578.4982565987661,579.541458273286,43881.31271895443]],"n":415},"status":[0.0]}}
{"case":"gen0657","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[172.72676459803048,-0.3390649044947641,4.0],"status":[0.0]}}
@@ -807,7 +807,7 @@
{"case":"gen0781","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0782","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[356.6938718977998,0.9196411058523958,4.0],"status":[0.0]}}
{"case":"gen0783","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0784","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29710.904700154657,30884.24037369345,1477965.8371218254]],"n":175},"status":[0.0]}}
{"case":"gen0784","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29674.9491611654,30848.28277993568,1476761.0098037035]],"n":175},"status":[0.0]}}
{"case":"gen0785","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0786","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":173},"status":[0.0]}}
{"case":"gen0787","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
@@ -882,7 +882,7 @@
{"case":"gen0856","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":96},"status":[0.0]}}
{"case":"gen0857","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[142.87067346362636,0.29111472476951983,4.0],"status":[0.0]}}
{"case":"gen0858","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[62.361152756430386,68.13517399482083,701.9717456314567]],"n":287},"status":[0.0]}}
{"case":"gen0859","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[31344.551071816884,31369.253221453346,1567834.9732728351],[28.485359855137915,28.5230366152614,309.0122217697676]],"n":279},"status":[0.0]}}
{"case":"gen0859","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[32033.558650683775,32054.691630224177,1590693.7945885656],[28.27136060194707,28.309037362070555,307.7282262506226]],"n":279},"status":[0.0]}}
{"case":"gen0860","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[[28313.873561658336,28377.60884655703,484767.7631369969]],"n":74},"status":[0.0]}}
{"case":"gen0861","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30616.003773260356,31142.595467800078,1833604.9973041634],[104.11966630073412,295.3335871107082,9473.601497198426]],"n":415}}}
{"case":"gen0862","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[34.41464392268276,86.15463999923445,2092.6088636556537]],"n":333},"status":[0.0]}}
@@ -968,7 +968,7 @@
{"case":"review_axis_x_1e-13_step_inv_axis_point","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"review_axis_x_1e-13_trim_natural","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[48.1,48.5,790.4]],"n":291},"status":[0.0]}}
{"case":"review_circle_touch_axis_is_nurb_revolution","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[31098.819805153355,31136.03300858893,1856410.701399856],[55.77817459305203,165.76093065034902,11588.357177293241]],"n":499},"status":[1.0]}}
{"case":"review_circle_touch_axis_offset","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[31341.562607380638,31399.965378743374,1865211.620103136],[41.127786822073304,109.87957249080739,6035.616011393157]],"n":415},"status":[0.0]}}
{"case":"review_circle_touch_axis_offset","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[31100.17335854395,31149.60407640085,1856580.090117328],[55.17462120245875,182.49621202458763,11511.154751321834]],"n":499},"status":[0.0]}}
{"case":"review_circle_touch_axis_step_inv_axis_point","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"review_circle_touch_axis_trim_natural","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[31098.819805153355,31136.03300858893,1856410.701399856],[55.77817459305203,165.76093065034902,11588.357177293241]],"n":499},"status":[0.0]}}
{"case":"review_cyl_rev_sw0_is_nurb_revolution","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203},"status":[1.0]}}
@@ -1063,7 +1063,7 @@
{"case":"review_near_cyl_sw1_partial_trim_natural","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":147},"status":[0.0]}}
{"case":"review_near_cyl_sw1_step_inv_axis_point","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"review_near_cyl_sw1_trim_natural","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203},"status":[0.0]}}
{"case":"review_offset_axis_crossing_merge","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[156.56343208465285,156.56343308262387,19831.729066786007],[141.9078163247792,163.61669385864928,17488.734461574913],[115.8570757146454,147.45859996546525,15526.617082293214],[163.76067696086562,171.84956665257306,19295.005635961315]],"n":1215},"status":[0.0]}}
{"case":"review_offset_axis_crossing_merge","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[100.83171302386047,102.93172473993012,12772.586364877012],[64.47744944042489,104.6751255748928,7430.565906180596],[71.71948149404463,97.59853211121963,9549.621696824262],[100.95723558439556,102.1873939665268,9170.97118270073]],"n":1173},"status":[0.0]}}
{"case":"review_offset_tiny_radius_sweep_scale","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203},"status":[0.0]}}
{"case":"review_scale_10000_is_nurb_revolution","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[141034.0,145034.0,1059684.5]],"n":291},"status":[1.0]}}
{"case":"review_scale_10000_offset","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[2211453.4360185647,2825174.331619292,228738649.2978719],[-969142.5745911481,2702774.9264564295,-126689359.50161147],[978763.9312549272,1680925.8553829386,107743979.71070899]],"n":963},"status":[0.0]}}
@@ -65,7 +65,7 @@
{"case":"gen0049","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0050","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29554.21787053357,30852.35940534539,1808680.8130553584],[98.6119534123244,116.19651857887592,3838.5082016365113]],"n":332},"status":[0.0]}}
{"case":"gen0051","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30045.606153067096,30158.833255731497,1804769.1518967522],[46.851353674765036,48.62286883363137,954.4859662688735]],"n":289},"status":[0.0]}}
{"case":"gen0052","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.794606939681873,-1.1975216339726882],"status":[0.0]}}
{"case":"gen0052","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.7668146372816801,-1.1975216339726882],"status":[0.0]}}
{"case":"gen0053","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0],"status":[0.0]}}
{"case":"gen0054","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[[14410.266903444328,14458.832819751307,59436.60089952831]],"n":56},"status":[0.0]}}
{"case":"gen0055","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29959.82946506995,30182.618606974484,1800687.4083010652]],"n":247},"status":[0.0]}}
@@ -253,13 +253,13 @@
{"case":"gen0237","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0238","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0239","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[142.80895067727465,165.59772466484293,4690.703436315782]],"n":331},"status":[0.0]}}
{"case":"gen0240","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30316.210873894917,30460.887906734035,1812074.9543548971]],"n":248},"status":[0.0]}}
{"case":"gen0240","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30314.757133748903,30460.89906458618,1812026.2723082958]],"n":248},"status":[0.0]}}
{"case":"gen0241","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0242","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29453.06387615861,31008.80784815877,1802828.300225178]],"n":248},"status":[0.0]}}
{"case":"gen0243","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0],"status":[0.0]}}
{"case":"gen0244","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,1.0,2.0,1.0,0.14447601780852687,0.4304423917148111,0.5006907695661894],"status":[0.0]}}
{"case":"gen0245","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.23631200507922392,1.0],"status":[0.0]}}
{"case":"gen0246","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30358.865011454316,30786.278677049737,1805668.7603054743],[39.19546205960591,112.41350729437352,2090.6807434755506]],"n":331},"status":[0.0]}}
{"case":"gen0246","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30358.636937922674,30786.27877979692,1805661.1104391105],[39.19546205960591,112.41350729437353,2090.6807434755506]],"n":331},"status":[0.0]}}
{"case":"gen0247","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0],"status":[0.0]}}
{"case":"gen0248","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.3097487469306281,0.3447096372396975],"status":[0.0]}}
{"case":"gen0249","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.4900409133542925,0.7338548219875016],"status":[0.0]}}
@@ -361,7 +361,7 @@
{"case":"gen0345","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,1.0,2.0,1.0,29.331944452967726,-1.9275585097178316,0.19095053188369854],"status":[0.0]}}
{"case":"gen0346","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[253778.41052040597,365766.39658361656,30389394.009382848]],"n":247},"status":[0.0]}}
{"case":"gen0347","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":247},"status":[0.0]}}
{"case":"gen0348","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29772.866678170136,29915.126421664987,1800149.5922664076],[37.83504228505366,42.78839330011802,613.3722055439199]],"n":289},"status":[0.0]}}
{"case":"gen0348","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29973.051789697427,30115.419844470955,1805377.485244787],[37.83504228505366,42.78839330011802,613.3722055439199]],"n":289},"status":[0.0]}}
{"case":"gen0349","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203},"status":[0.0]}}
{"case":"gen0350","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29907.915501148127,30738.050472129526,1787737.1632195506],[21.21505111132828,366.97211528327574,3390.2550279913385]],"n":373},"status":[0.0]}}
{"case":"gen0351","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
@@ -435,7 +435,7 @@
{"case":"gen0419","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[28492.32153593868,28818.741732256625,1329961.9057259609]],"n":119},"status":[0.0]}}
{"case":"gen0420","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0421","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203},"status":[0.0]}}
{"case":"gen0422","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.6073151686569089,0.21182382616090017],"status":[0.0]}}
{"case":"gen0422","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.6188740940019568,0.21182382616090017],"status":[0.0]}}
{"case":"gen0423","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0],"status":[0.0]}}
{"case":"gen0424","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203},"status":[0.0]}}
{"case":"gen0425","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
@@ -448,7 +448,7 @@
{"case":"gen0432","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[[14429.776567776646,14465.824227069492,59894.21558147158]],"n":66},"status":[0.0]}}
{"case":"gen0433","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0,1.0],"status":[0.0]}}
{"case":"gen0434","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0435","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0,-1.4938106444378023],"status":[0.0]}}
{"case":"gen0435","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.00022029345919263152,-1.4938106444378023],"status":[0.0]}}
{"case":"gen0436","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[-1.3510812343977525,0.0],"status":[0.0]}}
{"case":"gen0437","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[30289.733646866076,30942.797821768483,1499808.806060453]],"n":227},"status":[0.0]}}
{"case":"gen0438","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[["-inf","inf","-inf"],[41.30780934419762,60.17987370461063,680.9830085109288]],"n":289},"status":[0.0]}}
@@ -457,7 +457,7 @@
{"case":"gen0441","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0442","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0443","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[28853.554995331007,31384.511487638494,1780805.5838226944]],"n":247},"status":[0.0]}}
{"case":"gen0444","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30351.54025242402,30986.374911465093,1804063.4267965115],[116.98853924905205,230.36991827087627,14117.866639986873]],"n":415},"status":[0.0]}}
{"case":"gen0444","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30344.993134201635,30986.428860388773,1803844.1043282754],[116.98853924905205,230.36991827087627,14117.866639986873]],"n":415},"status":[0.0]}}
{"case":"gen0445","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30194.076700353504,30540.715955664997,1821729.3931670333]],"n":203},"status":[0.0]}}
{"case":"gen0446","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30921.058048258903,31035.965684326417,1842104.916598205],[19.83161117351935,74.19801061273189,1724.1902437582753]],"n":331},"status":[0.0]}}
{"case":"gen0447","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0],"status":[0.0]}}
@@ -475,7 +475,7 @@
{"case":"gen0459","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[[29135.451679601534,29209.622708982995,790985.1474340447]],"n":104},"status":[0.0]}}
{"case":"gen0460","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0461","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0462","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0,0.27442760919883785],"status":[0.0]}}
{"case":"gen0462","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0024955948462567473,0.27442760919883785],"status":[0.0]}}
{"case":"gen0463","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0464","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29701.1875702268,30505.668311187193,1766068.5400124874]],"n":247},"status":[0.0]}}
{"case":"gen0465","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30251.139797238608,30600.4906700693,1837145.5874748258]],"n":203},"status":[0.0]}}
@@ -611,7 +611,7 @@
{"case":"gen0595","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[-0.17726420561295764,0.0],"status":[0.0]}}
{"case":"gen0596","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0597","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0598","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30179.86980868189,30769.076589942142,1802175.3711976507]],"n":247},"status":[0.0]}}
{"case":"gen0598","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30177.38083829894,30769.0863115684,1802091.939090581]],"n":247},"status":[0.0]}}
{"case":"gen0599","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29974.610842395952,30249.142091774193,1799148.2722515154]],"n":203},"status":[0.0]}}
{"case":"gen0600","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":119},"status":[0.0]}}
{"case":"gen0601","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30202.597231256357,30847.73023253907,1806213.0060580869],[76.19620023767334,150.2862245733946,8056.161750753592]],"n":415},"status":[0.0]}}
@@ -642,7 +642,7 @@
{"case":"gen0626","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[2.0,1.0,1.0,2.0,1.0,0.952953622675145,0.0,0.0,1.0,1.0,2.0,1.0,0.952953622675145,0.0,1.0],"status":[0.0]}}
{"case":"gen0627","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30022.93381199988,30185.633736860986,1801699.19302469],[57.42799328541519,74.31672052999164,1651.447980268409]],"n":291},"status":[0.0]}}
{"case":"gen0628","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0],"status":[0.0]}}
{"case":"gen0629","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.8721515637142568,0.0],"status":[0.0]}}
{"case":"gen0629","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.8848737795653722,0.0],"status":[0.0]}}
{"case":"gen0630","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[2430.501448797089,21836.7227678789,25012.954686640933]],"n":288},"status":[0.0]}}
{"case":"gen0631","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0,3.528847403518176],"status":[0.0]}}
{"case":"gen0632","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0],"status":[0.0]}}
@@ -650,7 +650,7 @@
{"case":"gen0634","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,1.0,2.0,1.0,0.16175165120677595,0.7880918502254108,1.8238150874752517],"status":[0.0]}}
{"case":"gen0635","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0],"status":[0.0]}}
{"case":"gen0636","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203},"status":[0.0]}}
{"case":"gen0637","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.16476778247020873,0.8433855990942796],"status":[0.0]}}
{"case":"gen0637","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.16476778247020873,0.8544575028947936],"status":[0.0]}}
{"case":"gen0638","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30017.964658522662,31672.232341764644,1804875.151565422],[66.04196473217209,77.46338561961504,1190.4913105250562]],"n":287},"status":[0.0]}}
{"case":"gen0639","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29982.02558354491,30361.977859283015,1798707.7268753045],[37.41484419377072,65.91795691407908,696.5207612712591]],"n":291},"status":[0.0]}}
{"case":"gen0640","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30951.559772726465,31068.93188729233,1880364.5373067344]],"n":247},"status":[0.0]}}
@@ -663,7 +663,7 @@
{"case":"gen0647","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0],"status":[0.0]}}
{"case":"gen0648","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0649","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[45.13045445159678,159.41969918174524,1760.5329981483399]],"n":330},"status":[0.0]}}
{"case":"gen0650","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.2856814046429315,1.0],"status":[0.0]}}
{"case":"gen0650","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.2856814046429315,0.6905218664371997],"status":[0.0]}}
{"case":"gen0651","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0],"status":[0.0]}}
{"case":"gen0652","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0],"status":[0.0]}}
{"case":"gen0653","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
@@ -749,7 +749,7 @@
{"case":"gen0733","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[[28816.165170626515,28870.167777632127,781880.4496683324]],"n":104},"status":[0.0]}}
{"case":"gen0734","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":{"blocks":[[-130482.281469519,266376.27440638765,-25694841.806371573],[-40218.85473979955,54851.076253649975,-785720.6985079797]],"n":331},"status":[0.0]}}
{"case":"gen0735","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":[1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],"status":[0.0]}}
{"case":"gen0736","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29635.37827878802,30357.418833230993,1744917.0048814148],[7.204599895064316,58.48269797903537,256.633244157186]],"n":289},"status":[0.0]}}
{"case":"gen0736","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29367.116625728537,30357.164229561684,1735917.756401392],[7.204599895064316,58.48269797903537,256.633244157186]],"n":289},"status":[0.0]}}
{"case":"gen0737","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":204},"status":[0.0]}}
{"case":"gen0738","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30007.188254258148,31293.58059153361,1803910.0245482174]],"n":203},"status":[0.0]}}
{"case":"gen0739","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,1.0,2.0,1.0,1.5700924586837751e-15,0.6511656574952199,9.901609045838779],"status":[0.0]}}
@@ -821,7 +821,7 @@
{"case":"gen0805","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[28937.82043008583,29961.14596894057,1459056.3471806922]],"n":173},"status":[0.0]}}
{"case":"gen0806","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"]],"n":203},"status":[0.0]}}
{"case":"gen0807","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,1.0,2.0,1.0,0.025000582436162,3.705166804956611,0.7529051509566986],"status":[0.0]}}
{"case":"gen0808","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[-2.05301802277461,0.0637711762079021],"status":[0.0]}}
{"case":"gen0808","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[-2.05301802277461,0.0],"status":[0.0]}}
{"case":"gen0809","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[0.0],"status":[0.0]}}
{"case":"gen0810","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29960.849667381237,30195.49742371365,1796857.1779529592],[114.50900055028258,185.8869322991257,8148.808590590794]],"n":417},"status":[0.0]}}
{"case":"gen0811","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[1.0,1.0,1.0,2.0,1.0,0.12751539055718009,0.2131629321994861,0.7906133089715329],"status":[0.0]}}
@@ -909,7 +909,7 @@
{"case":"gen0893","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[[28837.545078764328,29010.667246666402,926983.0702905378]],"n":124},"status":[0.0]}}
{"case":"gen0894","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":[],"status":[1001.0]}}
{"case":"gen0895","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"other_build":[0.0],"out":{"blocks":[[28933.558018714477,28934.114917675408,639871.5868115812]],"n":84},"status":[0.0]}}
{"case":"gen0896","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29861.385470306835,30872.96347042743,1753681.557544187],[-163.98958529079258,281.6862262619511,-8163.128279239296]],"n":405},"status":[0.0]}}
{"case":"gen0896","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[29832.287030691798,30858.537926787627,1752921.3151762423],[-163.98958529079258,281.6862262619511,-8163.128279239296]],"n":405},"status":[0.0]}}
{"case":"gen0897","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30701.43729544399,31599.87183841545,1865654.0546727127],[231.9062391064784,235.4316138690079,20255.226413244432]],"n":416},"status":[0.0]}}
{"case":"gen0898","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[[30926.69356985266,31011.469762135803,1842230.5933075529],[53.29219388275672,72.86016758116487,2709.4781826210906]],"n":332},"status":[0.0]}}
{"case":"gen0899","deferred":null,"error":null,"ints":{},"topology":null,"values":{"build":[0.0],"out":{"blocks":[["-inf","inf","-inf"],[98.67703358431551,156.70222297562128,6862.986474338887]],"n":373},"status":[0.0]}}
File diff suppressed because one or more lines are too long
@@ -1167,7 +1167,7 @@
{"case":"matrix_1140","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":[1.0,1.0,1.0,1.0,-0.37488874900281893,1.5006145573208944,7.542087574180792,1.0,0.0,1.0,1.0],"status":[0.0]}}
{"case":"matrix_1141","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":[0.0,3.0,1.0,1.0,1.7626627987039287,0.4215290322200647,10765.122486287479,1.0,0.0,1.0,1.0],"status":[0.0]}}
{"case":"matrix_1142","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":[1.0,1.0,-0.031911776786110846,1.533287570026964,1.6679061978091494e-11,1.0,2.0,1.0,1.0],"status":[0.0]}}
{"case":"matrix_1143","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":[1.0,1.0,0.3285420301022051,-0.24305095022573142,6.648613582037027e-10,1.0,2.0,1.0,1.0],"status":[0.0]}}
{"case":"matrix_1143","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":[1.0,1.0,0.32854203010220506,-0.24305095022573123,6.648616832786346e-10,1.0,2.0,1.0,1.0],"status":[0.0]}}
{"case":"matrix_1144","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":[1.0,3.0,2.0,1.0,1.7606139675717696,1.8366081337236544,0.9286012471319718,1.0,0.0,1.0,1.0],"status":[0.0]}}
{"case":"matrix_1145","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":[1.0,1.0,0.0,1.0,0.39764804741676013,-1.2566799743119004,0.7501029044168733,1.0,0.0,1.0,1.0],"status":[0.0]}}
{"case":"matrix_1146","deferred":null,"error":null,"ints":{},"topology":null,"values":{"out":[0.0,0.0,3.4397425713945156,-0.3980775071895666,0.0,0.0,0.0,1.0],"status":[0.0]}}
+3 -3
View File
@@ -20,7 +20,7 @@
{"case":"box_face0_line_no_cap","deferred":null,"error":null,"ints":{"volume_status":[0]},"topology":null,"values":{"counts":[4.0,12.0,8.0],"faces":[0.0,4.0,0.0],"has":[1.0],"status":[0.0],"volume":[0.0]}}
{"case":"box_face0_line_null_opts","deferred":null,"error":null,"ints":{"volume_status":[0]},"topology":null,"values":{"counts":[4.0,12.0,8.0],"faces":[0.0,4.0,0.0],"has":[1.0],"status":[0.0],"volume":[0.0]}}
{"case":"box_face0_offset_scaled_cap","deferred":null,"error":null,"ints":{"volume_status":[0]},"topology":null,"values":{"counts":[6.0,12.0,8.0],"faces":[1.0,4.0,1.0],"has":[1.0],"status":[0.0],"volume":[0.00002999999999999518]}}
{"case":"box_face1_curve_path_cap","deferred":null,"error":null,"ints":{"volume_status":[0]},"topology":null,"values":{"counts":[6.0,12.0,8.0],"faces":[1.0,4.0,1.0],"has":[1.0],"status":[0.0],"volume":[30.2282600710325]}}
{"case":"box_face1_curve_path_cap","deferred":null,"error":null,"ints":{"volume_status":[0]},"topology":null,"values":{"counts":[6.0,12.0,8.0],"faces":[1.0,4.0,1.0],"has":[1.0],"status":[0.0],"volume":[30.228259877867142]}}
{"case":"box_face1_curve_path_cap_translational","deferred":null,"error":null,"ints":{"volume_status":[0]},"topology":null,"values":{"counts":[6.0,12.0,8.0],"faces":[1.0,4.0,1.0],"has":[1.0],"status":[0.0],"volume":[30.00000000000004]}}
{"case":"box_face1_line_cap","deferred":null,"error":null,"ints":{"volume_status":[0]},"topology":null,"values":{"counts":[6.0,12.0,8.0],"faces":[1.0,4.0,1.0],"has":[1.0],"status":[0.0],"volume":[29.999999999999993]}}
{"case":"box_face1_line_no_cap","deferred":null,"error":null,"ints":{"volume_status":[0]},"topology":null,"values":{"counts":[4.0,12.0,8.0],"faces":[0.0,4.0,0.0],"has":[1.0],"status":[0.0],"volume":[0.0]}}
@@ -458,9 +458,9 @@
{"case":"hang13_linear_circle_cap1_neg","deferred":null,"error":null,"ints":{},"topology":null,"values":{"counts":[3.0,3.0,2.0],"has":[1.0],"status":[0.0]}}
{"case":"hang13_linear_reps1_circle_cap1_neg","deferred":null,"error":null,"ints":{},"topology":null,"values":{"counts":[3.0,3.0,2.0],"has":[1.0],"status":[0.0]}}
{"case":"hang13_linear_reps2_circle_cap1_neg","deferred":null,"error":null,"ints":{},"topology":null,"values":{"counts":[4.0,5.0,3.0],"has":[1.0],"status":[0.0]}}
{"case":"issue1463_drilled_hole_mz_120","deferred":null,"error":null,"ints":{},"topology":null,"values":{"status":[1001.0]}}
{"case":"issue1463_drilled_hole_mz_120","deferred":null,"error":null,"ints":{},"topology":null,"values":{"counts":[2.0,6.0,5.0],"has":[1.0],"status":[0.0]}}
{"case":"issue1463_drilled_hole_mz_360","deferred":null,"error":null,"ints":{},"topology":null,"values":{"counts":[2.0,4.0,3.0],"has":[1.0],"status":[0.0]}}
{"case":"issue1463_drilled_hole_pz_120","deferred":null,"error":null,"ints":{},"topology":null,"values":{"status":[1001.0]}}
{"case":"issue1463_drilled_hole_pz_120","deferred":null,"error":null,"ints":{},"topology":null,"values":{"counts":[2.0,6.0,5.0],"has":[1.0],"status":[0.0]}}
{"case":"issue1463_drilled_hole_pz_360","deferred":null,"error":null,"ints":{},"topology":null,"values":{"counts":[2.0,4.0,3.0],"has":[1.0],"status":[0.0]}}
{"case":"issue1463_rig4008_tool_ccw_180","deferred":null,"error":null,"ints":{},"topology":null,"values":{"counts":[6.0,12.0,8.0],"has":[1.0],"status":[0.0]}}
{"case":"issue1463_rig4008_tool_cw_180","deferred":null,"error":null,"ints":{},"topology":null,"values":{"counts":[6.0,12.0,8.0],"has":[1.0],"status":[0.0]}}
@@ -88,7 +88,7 @@
{"case":"gen0027","deferred":null,"error":null,"ints":{"graph":{"blocks":[[50,1663]],"n":71}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":[1e+20,1e+20,1e+20,1e+20,0.0,5.0,-0.3239936032947275,0.00001,-10.0,-5.0,0.0,4.999999999999999,0.00001,-30.61467458920718,0.0,0.00001],"record":{"blocks":[[29005.84941496466,29190.15058503534,492369.5409541619]],"n":70},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0]}}
{"case":"gen0028","deferred":null,"error":null,"ints":{"graph":{"blocks":[[682,105286],[29,96]],"n":261}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0],"geometry":{"blocks":[[2.4e+21,2.4e+21,2.9999999999999996e+22]],"n":56},"record":{"blocks":[[142982.44946801648,143164.44946801648,15937837.083377652],[30107.0,30287.0,771201.0]],"n":342},"surface_owners":[1.0,1.0,1.0],"uv_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0]}}
{"case":"gen0029","deferred":null,"error":null,"ints":{"graph":{"blocks":[[103,5364]],"n":97}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0],"geometry":{"blocks":[[8e+20,8e+20,3.6e+21]],"n":27},"record":{"blocks":[[43116.93725845537,43200.53141704122,1292649.522554986]],"n":96},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0]}}
{"case":"gen0030","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1296,157069],[2328,235553],[3587,458155],[6012,680472]],"n":985}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0],"geometry":{"blocks":[[1.1999999999999975e+22,1.1999999999999975e+22,7.260000000000001e+23]],"n":240},"record":{"blocks":[[155889.5,155901.5,19084516.0],[154524.26679056452,154754.26679056452,20294414.16484167],[140727.0,140747.0,17917951.0],[154823.9825193369,154889.9825193369,19101055.99218987],[103941.5,103945.5,11452164.0],[30322.62862549696,30351.572897406964,900460.083544985]],"n":1370},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[405.0,465.0,7625.0]],"n":60}}}
{"case":"gen0030","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1310,157645],[2328,233932],[3582,458604],[6017,676996]],"n":985}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0],"geometry":{"blocks":[[1.1999999999999975e+22,1.1999999999999992e+22,7.260000000000001e+23]],"n":240},"record":{"blocks":[[141057.532489923,141129.532489923,18198497.169491984],[116075.2232256508,116196.71117175766,15361187.0901822],[60709.50164966287,44242008.166222826,267724844.31036526]],"n":710},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-60.0,60.0,-1830.0]],"n":60}}}
{"case":"gen0031","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2508,322157],[2737,381059],[5730,644350],[3544,630392],[6005,593789],[9923,1440534],[12774,1466844]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[615.8846177461533,14815740.328712061,14867121.579591237]],"n":424},"record":{"blocks":[[140987.2736643996,141060.86746726738,18167363.483604513],[127271.2210827841,127432.17815770011,17660390.497585837],[128002.63741952149,128193.04443412332,17391234.07712418],[91053.96848240259,65856740.31616496,77823261.30486597],[60762.71681469282,60811.28318530718,5140971.371759845]],"n":1210},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-108.0,108.0,-5886.0]],"n":108}}}
{"case":"gen0032","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
{"case":"gen0033","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000017e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141253.1029586434,141336.67310083122,18233727.58146279],[910456.8947865181,29741318.114338845,368938141.54290855],[-791133.6058079528,13482279.819274934,-89893885.21058506]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
+2 -3
View File
@@ -1754,7 +1754,6 @@ fn measure(k: &Kernel, b: &mut Brep) -> Result<Measured, SolidError> {
/// resolve the same fields).
fn enable_fix(k: &mut Kernel, name: &str) -> Result<(), String> {
let field = match name {
"sweep_frame_on_planar_path_normal" => &mut k.fixes.sweep_frame_on_planar_path_normal,
"imprint_extends_edge_to_proto_vertex" => &mut k.fixes.imprint_extends_edge_to_proto_vertex,
"delete_vertex_joins_seam_iso_curve" => &mut k.fixes.delete_vertex_joins_seam_iso_curve,
"cover_refuses_folded_continuation" => &mut k.fixes.cover_refuses_folded_continuation,
@@ -2505,10 +2504,10 @@ mod tests {
#[test]
fn fixes_are_enabled_by_name() {
let mut k = Kernel::default();
enable_fix(&mut k, "sweep_frame_on_planar_path_normal").unwrap();
enable_fix(&mut k, "imprint_extends_edge_to_proto_vertex").unwrap();
enable_fix(&mut k, "cover_refuses_folded_continuation").unwrap();
enable_fix(&mut k, "fillet_refuse_non_manifold_result").unwrap();
assert!(k.fixes.sweep_frame_on_planar_path_normal);
assert!(k.fixes.imprint_extends_edge_to_proto_vertex);
assert!(k.fixes.cover_refuses_folded_continuation);
assert!(k.fixes.fillet_refuse_non_manifold_result);
assert!(enable_fix(&mut k, "no_such_fix").is_err());
@@ -1340,6 +1340,13 @@ impl OffsetBrep<H> for Two {
fn edgeuse_is_marked(&self, _k: &Kernel, eu: H, _mark: MarkType) -> solidcore::Result<bool> {
Ok(self.marked.contains(&eu))
}
fn edgeuse_middle_binormal(&mut self, k: &Kernel, eu: H) -> solidcore::Result<Vec3> {
let b = self.brep_m(eu)?;
let e = eu1::edge(b, euid(eu))?.ok_or_else(|| invalid("edge use without edge"))?;
let middle = ed::interval(b, e)?.evaluate(0.5);
let options = eu3::BinormalOptions::default();
Ok(eu3::evaluate_binormal(k, b, euid(eu), middle, false, &options)?.binormal)
}
fn edgeuse_normalized_evaluate(
&mut self,
k: &Kernel,
+5 -4
View File
@@ -401,9 +401,10 @@ impl std::fmt::Display for OffsetRefusal {
f,
"its offset by {distance} collapses (a radius of curvature {radius})"
),
OffsetRefusal::LoopsCross { crossing } => {
write!(f, "its offset's loops cross ({crossing:?})")
}
OffsetRefusal::LoopsCross { crossing } => write!(
f,
"its offset's loops cross in a way the offset cannot resolve: {crossing}"
),
OffsetRefusal::DistanceUnresolved {
distance,
resolution,
@@ -436,7 +437,7 @@ impl std::fmt::Display for OffsetRefusal {
"a gap in its offset's loops cannot be bridged within tolerance"
)
}
OffsetRefusal::Unsound { what } => write!(f, "the body built is unsound ({what:?})"),
OffsetRefusal::Unsound { what } => write!(f, "{what}"),
OffsetRefusal::Phrase { phrase, .. } => f.write_str(phrase),
}
}
+19 -5
View File
@@ -42,14 +42,28 @@ const MAX_DISTANCE: f64 = 1e6;
/// What [`Error::Unsupported`] says for an offset that moves a vertex past an edge's other end.
const EDGE_VANISHES: &str = "a vertex moved past the other end of an edge";
/// An offset's kernel error as the operation's: a refusal the kernel gives a reason for by that
/// reason ([`offset_refused`]); one without by its kind ([`by_kind`]), else as
/// [`crate::error::status`].
/// An offset's kernel error as the operation's: its offset refusal where it is one
/// ([`typed`]), else as [`crate::error::status`].
fn status(e: SolidError, index: &dyn Fn(FaceId) -> Option<usize>) -> Error {
typed(&e, index).unwrap_or_else(|| kernel_status(e))
}
/// A kernel error as ops' offset refusal, for a caller that holds no [`Order`] of the body (any
/// face it names is unnamed): what the offset operations give for it ([`typed`]), `None` where
/// they give the kernel's status instead.
#[must_use]
pub fn refusal(e: &SolidError) -> Option<Error> {
typed(e, &|_| None)
}
/// The offset refusal a kernel error is, the one routing [`status`] and [`refusal`] share: a
/// refusal the kernel gives a reason for by that reason ([`offset_refused`]), its face through
/// `index`; one without by its kind ([`by_kind`]); `None` for any other error.
fn typed(e: &SolidError, index: &dyn Fn(FaceId) -> Option<usize>) -> Option<Error> {
if let Some(Reason::Offset(o)) = e.failure().map(|f| &f.reason) {
return offset_refused(*o, e.entity(), index);
return Some(offset_refused(*o, e.entity(), index));
}
by_kind(e.kind()).unwrap_or_else(|| kernel_status(e))
by_kind(e.kind())
}
/// The operation's error for an offset refusal of `kind` that has one of its own: walls that
+81
View File
@@ -611,6 +611,87 @@ fn an_unnamed_offset_refusal_keeps_the_kernels_words() {
);
}
/// `refusal` is what the offset operations give for a kernel error, with any face unnamed: the
/// same routing as `status`, which agrees with it on every kind of error (an offset refusal is
/// typed, in words from its fields; an error with no offset reason is the kernel's status).
#[test]
fn a_kernel_offset_refusal_is_typed_for_a_caller_without_an_order() {
use solidcore::error::offset::{LoopCrossing, OffsetFailure, Unsoundness};
use solidcore::error::Failure;
let face = solidcore::FaceId::from_index(4);
let failed = |o| Failure::new("offset::offset_executive::crossing_loops", o).into_error();
let crossing = LoopCrossing::ThreeAtOnePoint;
let e = super::refusal(&failed(OffsetFailure::LoopsCross {
face: Some(face),
crossing,
}));
assert_eq!(
e,
Some(Error::OffsetRefused {
face: None,
why: crate::OffsetRefusal::LoopsCross { crossing }
})
);
assert_eq!(
e.map(|e| e.to_string()).as_deref(),
Some("the offset is refused: its offset's loops cross in a way the offset cannot resolve: three loops cross at one point")
);
let what = Unsoundness::NotManifoldSolid;
assert_eq!(
super::refusal(&failed(OffsetFailure::Unsound { what })).map(|e| e.to_string()),
Some(format!("the offset is refused: {what}"))
);
assert_eq!(
what.to_string(),
"the body built is not a manifold solid, as its input was"
);
assert_eq!(
super::refusal(&failed(OffsetFailure::Folds {
face,
distance: 2.0,
largest: 1.0
})),
Some(Error::WallFolds { largest: Some(1.0) })
);
assert_eq!(super::refusal(&solidcore::SolidError::sm_err("x")), None);
// `status` and `refusal` agree: `status` is the refusal, else the kernel's status
let errors = [
failed(OffsetFailure::Failed("x")),
failed(OffsetFailure::InvalidValue("x")),
failed(OffsetFailure::Folds {
face,
distance: 2.0,
largest: 1.0,
}),
failed(OffsetFailure::NotCertified {
face,
distance: -1.0,
}),
failed(OffsetFailure::Collapses {
face,
distance: -2.0,
radius: 1.5,
}),
failed(OffsetFailure::LoopsCross {
face: Some(face),
crossing,
}),
failed(OffsetFailure::SeamOpened { face }),
failed(OffsetFailure::Unsound { what }),
failed(OffsetFailure::ExactCurveUncut),
solidcore::SolidError::sm_err("x"),
solidcore::SolidError::Upstream {
operation: "offset::x",
code: solidcore::error::SM_ERR_OFFSET_WALLS_TOUCH,
entity: None,
},
];
for e in errors {
let expected = super::refusal(&e).unwrap_or_else(|| crate::error::status(e.clone()));
assert_eq!(super::status(e.clone(), &|_| None), expected, "{e:?}");
}
}
/// A face whose surface the kernel's room test cannot measure (an offset surface): the room
/// test's answer maps to `Unsupported`, not to a room or `NoRoom`.
#[test]
+86 -59
View File
@@ -16,6 +16,7 @@
//! | [`OffsetFailure::ToleranceUnresolved`] | `SM_ERR_INVALID_INPUT` (solidcore-native, WR-749) |
//! | [`OffsetFailure::SeamOpened`] | `SM_ERR` (solidcore-native) |
//! | [`OffsetFailure::ExactCurveUncut`] | `SM_ERR` (solidcore-native) |
//! | [`OffsetFailure::TangentEndOffCurve`] | `SM_ERR` (solidcore-native) |
//! | [`OffsetFailure::FacesApart`] | `SM_ERR` (solidcore-native) |
//! | [`OffsetFailure::ReachUncertified`] | `SM_ERR` (solidcore-native) |
//! | [`OffsetFailure::LoopGapUncertified`] | `SM_ERR` (solidcore-native) |
@@ -140,6 +141,11 @@ pub enum OffsetFailure {
/// ([`crate::surface::surface::SurfaceIntersection::clip_exact_curves`]; the failure's
/// cause says why). The edge's offsets cannot be mated on it.
ExactCurveUncut,
/// In a local operation, an edge's new curve ends beside an edge that was tangent in
/// the input and whose new surfaces cross, and that edge, already mated, has no new
/// vertex on the curve: the end is not where the edges meet, and no cut chosen by
/// distance stands in for it.
TangentEndOffCurve,
/// In a local operation, the new surfaces of `face` and of its neighbour across an
/// edge meet nowhere near the edge, so no extension of either makes them meet there
/// and the edge cannot be found again. (The reach,
@@ -272,6 +278,7 @@ impl OffsetFailure {
.or_else(|| face.map(EntityRef::Face)),
Self::LoopGapUncertified { face } => face.map(EntityRef::Face),
Self::ExactCurveUncut
| Self::TangentEndOffCurve
| Self::Unsound { .. }
| Self::DistanceUnresolved { .. }
| Self::ToleranceUnresolved { .. }
@@ -312,6 +319,7 @@ impl OffsetFailure {
| Self::ToleranceUnresolved { .. }
| Self::SeamOpened { .. }
| Self::ExactCurveUncut
| Self::TangentEndOffCurve
| Self::FacesApart { .. }
| Self::ReachUncertified { .. }
| Self::LoopGapUncertified { .. }
@@ -340,6 +348,7 @@ impl OffsetFailure {
| Self::ToleranceUnresolved { .. }
| Self::SeamOpened { .. }
| Self::ExactCurveUncut
| Self::TangentEndOffCurve
| Self::FacesApart { .. }
| Self::ReachUncertified { .. }
| Self::LoopGapUncertified { .. }
@@ -360,6 +369,7 @@ impl OffsetFailure {
| Self::FacesApart { .. }
| Self::ReachUncertified { .. }
| Self::ExactCurveUncut
| Self::TangentEndOffCurve
| Self::LoopGapUncertified { .. }
| Self::Unsound { .. }
| Self::Failed(_) => SM_ERR,
@@ -388,6 +398,7 @@ impl OffsetFailure {
| Self::FacesApart { .. }
| Self::ReachUncertified { .. }
| Self::ExactCurveUncut
| Self::TangentEndOffCurve
| Self::LoopGapUncertified { .. }
| Self::Unsound { .. } => ErrorKind::Unsupported,
Self::DistanceUnresolved { .. } | Self::ToleranceUnresolved { .. } => {
@@ -438,6 +449,60 @@ fn offset_of(face: Option<FaceId>) -> String {
)
}
impl core::fmt::Display for LoopCrossing {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(match self {
LoopCrossing::AtLoopVertex => "they cross at a loop vertex",
LoopCrossing::MeetAtLoopVertex => "they meet at a loop vertex",
LoopCrossing::RunAlong => "they run along one another",
LoopCrossing::SharesEdge => "the face shares an edge with another face",
LoopCrossing::ThreeAtOnePoint => "three loops cross at one point",
LoopCrossing::StretchUnclassified => {
"a stretch of a loop is neither inside nor outside another loop"
}
LoopCrossing::LeaveMoreThanOneWay => "the loops leave a crossing more than one way",
LoopCrossing::LeaveNoWay => "the loops leave a crossing no way",
LoopCrossing::ArriveMoreThanOneWay => {
"the loops arrive at a crossing more than one way"
}
LoopCrossing::ComesApartAroundHole => "the face comes apart around a hole",
})
}
}
impl core::fmt::Display for Unsoundness {
/// What the body built fails, without the refusal [`OffsetFailure`]'s words add.
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match *self {
Unsoundness::Check { failures, first } => {
write!(
f,
"the local operation's body fails its check where its input did not \
({failures} failure lines more"
)?;
if let Some((reporting_type, test_index)) = first {
write!(f, ", the first test #{test_index} of type {reporting_type}")?;
}
f.write_str(")")
}
Unsoundness::GeometryUnchecked => f.write_str(
"the local operation's body fails its pointer check, so its geometry cannot \
be checked",
),
Unsoundness::NotManifoldSolid => {
f.write_str("the body built is not a manifold solid, as its input was")
}
Unsoundness::VolumeNotPositive => f.write_str(
"the local operation's body is a manifold solid whose volume is not positive",
),
Unsoundness::VolumeUndecided => f.write_str(
"the local operation's body is a manifold solid whose volume's sign could \
not be proved",
),
}
}
}
impl core::fmt::Display for OffsetFailure {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match *self {
@@ -482,31 +547,11 @@ impl core::fmt::Display for OffsetFailure {
offset_of(face)
)
}
Self::LoopsCross { face, crossing } => {
let how = match crossing {
LoopCrossing::AtLoopVertex => "they cross at a loop vertex",
LoopCrossing::MeetAtLoopVertex => "they meet at a loop vertex",
LoopCrossing::RunAlong => "they run along one another",
LoopCrossing::SharesEdge => "the face shares an edge with another face",
LoopCrossing::ThreeAtOnePoint => "three loops cross at one point",
LoopCrossing::StretchUnclassified => {
"a stretch of a loop is neither inside nor outside another loop"
}
LoopCrossing::LeaveMoreThanOneWay => {
"the loops leave a crossing more than one way"
}
LoopCrossing::LeaveNoWay => "the loops leave a crossing no way",
LoopCrossing::ArriveMoreThanOneWay => {
"the loops arrive at a crossing more than one way"
}
LoopCrossing::ComesApartAroundHole => "the face comes apart around a hole",
};
write!(
f,
"the loops of {} cross in a way the offset cannot resolve: {how}",
offset_of(face)
)
}
Self::LoopsCross { face, crossing } => write!(
f,
"the loops of {} cross in a way the offset cannot resolve: {crossing}",
offset_of(face)
),
Self::EdgeVanishes { edge, face } => {
let which = match (edge, face) {
(Some(e), _) => format!("edge {}", e.index()),
@@ -548,32 +593,8 @@ impl core::fmt::Display for OffsetFailure {
face.index()
),
Self::Unsound { what } => match what {
Unsoundness::Check { failures, first } => {
write!(
f,
"the local operation's body fails its check where its input did not ({failures} \
failure lines more"
)?;
if let Some((reporting_type, test_index)) = first {
write!(f, ", the first test #{test_index} of type {reporting_type}")?;
}
f.write_str("): refused, the input unchanged")
}
Unsoundness::GeometryUnchecked => f.write_str(
"the local operation's body fails its pointer check, so its geometry \
cannot be checked: refused, the input unchanged",
),
Unsoundness::NotManifoldSolid => {
f.write_str("the body built is not a manifold solid, as its input was: refused")
}
Unsoundness::VolumeNotPositive => f.write_str(
"the local operation's body is a manifold solid whose volume is not \
positive: refused, the input unchanged",
),
Unsoundness::VolumeUndecided => f.write_str(
"the local operation's body is a manifold solid whose volume's sign \
could not be proved: refused, the input unchanged",
),
Unsoundness::NotManifoldSolid => write!(f, "{what}: refused"),
_ => write!(f, "{what}: refused, the input unchanged"),
},
Self::LoopGapUncertified { face } => write!(
f,
@@ -586,6 +607,10 @@ impl core::fmt::Display for OffsetFailure {
cut into its pieces with their UV curves, so the edge's offsets cannot be \
mated",
),
Self::TangentEndOffCurve => f.write_str(
"an edge ends beside a tangent edge whose new faces now cross, and that edge's new \
vertex is not on the edge's new curve, so the edge cannot end there",
),
Self::ToleranceUnresolved { tolerance } => write!(
f,
"invalid input: offset tolerance {tolerance:e} is nonzero but at or below the \
@@ -804,6 +829,7 @@ mod tests {
),
(O::SeamOpened { face }, ErrorKind::Unsupported, false, false),
(O::ExactCurveUncut, ErrorKind::Unsupported, false, false),
(O::TangentEndOffCurve, ErrorKind::Unsupported, false, false),
(O::FacesApart { face }, ErrorKind::Unsupported, false, false),
(
O::ReachUncertified { face },
@@ -843,14 +869,15 @@ mod tests {
O::ToleranceUnresolved { .. } => 7,
O::SeamOpened { .. } => 8,
O::ExactCurveUncut => 9,
O::FacesApart { .. } => 10,
O::ReachUncertified { .. } => 11,
O::LoopGapUncertified { .. } => 12,
O::Unsound { .. } => 13,
O::Failed(_) => 14,
O::Missing(_) => 15,
O::InvalidValue(_) => 16,
O::Inconsistent(_) => 17,
O::TangentEndOffCurve => 10,
O::FacesApart { .. } => 11,
O::ReachUncertified { .. } => 12,
O::LoopGapUncertified { .. } => 13,
O::Unsound { .. } => 14,
O::Failed(_) => 15,
O::Missing(_) => 16,
O::InvalidValue(_) => 17,
O::Inconsistent(_) => 18,
}
};
for (i, (o, class, phrase, defect)) in pinned.iter().enumerate() {
-23
View File
@@ -520,10 +520,6 @@ pub struct UpstreamFixes {
pub offset_clip_right_side_out: bool,
/// `SmOffsetExecutive::MapEdgeuses` raises each offset edge it matches to an original edge to that edge's tolerance, and the offset edge's vertices to the original edge's vertices' largest. Upstream builds the offset faces' edges and vertices afresh at the B-rep's zone tolerance, so the offset of a sheet whose edges and vertices carry their own, larger tolerances (a trimmed B-spline patch: B-rep 2.08e-5, edges 4.15e-5, vertices 8.31e-5) is checked against half the B-rep's tolerance at its vertices, and its offset by 0.75 fails `check_brep` with edge/vertex Bad Gaps of 2.4e-5 to 3.9e-5, inside the original's own tolerances (WR-671, #2111).
pub offset_edges_keep_source_tolerance: bool,
/// `SmSurfOfRevolution::CreateOffsetSurface` cuts the offset generator at the axis on either side of the frame origin (WR-343, #973).
pub revolution_offset_axis_both_sides: bool,
/// `SmSurfOfRevolution::CreateCanonical` points the frame's X axis from the axis to the profile's point at its parameter origin (parameter 0, when the profile's STEP interval holds it; else its start, as upstream), the point the profile is defined from: a STEP line's `pnt`, a conic's point on its reference direction. Upstream takes the start of the STEP interval, which for an unbounded profile (`Line::create_canonical`, as STEP builds them) is `SM_INFINITE_PARAMETER` (1 234 567) away: past the axis for a line that crosses it, so the frame faces the other nappe and `STEPInversion`, which rotates a point into the frame's +X half plane, finds no generator point there and fails (`SM_ERR`) for the points of the nappe the profile's origin lies on. With the flag `STEPInversion` (and the STEP window's unbounded inversion) also turns the point onto the -X half plane, the other point where its circle about the axis meets the generator's plane, and takes the first half plane on which it lies on the generator (+X first, as upstream): a profile that crosses the axis then inverts on both sides of it, whichever way the frame faces; upstream refuses the side the frame does not face. When the point at parameter 0 is on the axis, the frame is taken from the interval's start before upstream's 45% point, so a segment through the axis whose points at 0 and at 45% are both on it has a frame; and a reference point is on the axis by its distance from the axis (`|Z x (P - O)|` below `SM_EFF_ZERO`, the bound the frame's Y axis is checked against), not by upstream's test of its direction within 1 degree of the axis, which takes a point one unit off the axis and 1000 along it for one on it (WR-738, #2628; solidcore-native, no fork commit).
pub revolution_frame_from_profile_origin: bool,
/// With extended convex edges, `piecewise_merge` relates the mated offsets of a manifold edge instead of intersecting them again (WR-344, #469; the fork's tangency-labs/smlib@9c21c9a (was 85bbfaf)), except an edge whose mate failed in `EdgeuseOffset`, which is intersected even where it is a tangent sector (#1795; the fork's tangency-labs/smlib@9d8ace9). The merge then no longer refuses some broken offsets, and the difference hands back a wall that bounds nothing (#1818; the fork's `ShellBrep` returns it, its C API refuses it as `SMLIB_ERR_NOT_SOLID`); the shell's refusal of a result that is no closed manifold solid is unconditional, whatever this flag says. Needs no other flag.
pub offset_extended_edges_mated: bool,
/// `SmOffsetSurface::SwapUV` negates the distance once for the extended surface and
@@ -797,10 +793,6 @@ pub struct UpstreamFixes {
pub offset_collapsed_rounds_mated: bool,
/// `FaceuseOffset` fails on a face whose offset surface is missing without collapsing (`SmSurface::OffsetCollapses`: a cone, cylinder, sphere or torus offset onto its spine), where upstream reads any missing surface as a collapsed round (WR-464, #1072).
pub offset_missing_surface_fails: bool,
/// `ApproximateSurface` raises the starter's inner knots to full multiplicity (exact knot insertion) before each round of cubic Bezier patches, which needs every inner knot of full multiplicity; and a patch that runs past the control points is `SM_ERR`. Upstream uses a starter copied from a root B-spline with simple inner knots (the offset of a fitted bicubic patch) as it is, its patches run past its control points, and it returns success with no surface, so exporting an offset B-spline sheet as B-splines fails (1001) (WR-573, #1501; the fork's tangency-labs/smlib@eae96cb).
pub approximate_surface_bezier_starter: bool,
/// `ApproximateSurface` rejects a pass of cubic Bezier patches when any row (or column) of patches has a sample outside the tolerance, so the next subdivision runs, and fails (`SM_ERR`) when the last allowed one is still outside it: an approximation it returns is within the tolerance it was asked for. Upstream starts the failed flag over with every row and has the break after a failed row commented out, so a pass is accepted when its last row is within the tolerance and returns `SM_SUCCESS` with an achieved tolerance above the requested one (WR-726, #2507; the offset wave of #2476 with `approximate_surface_bezier_starter`: 1.12e-5 for 5e-6).
pub approximate_surface_checks_every_row: bool,
/// A surface of revolution whose generatrix is one circular arc (a fillet's round along a circular edge, a torus in all but its type) collapses when an offset runs towards the arc's centre by its radius or more: `Surface::offset_collapses` says so, as a torus's does, and `create_offset_surface` builds no surface, where upstream says it never collapses and builds the generatrix's offset past the arc's centre as the arc turned inside out, so an inward shell thicker than the round fails with 1001 or leaves a wrong volume (WR-567; the fork's tangency-labs/smlib@8b2090f, smlib#64; SolidCraft `shell_of_a_disc_rounded_smaller_than_the_wall`, solidcraft#319). The faces either side are mated across it with `offset_collapsed_rounds_mated`.
pub revolution_round_offset_collapses: bool,
/// At an Nx1 corner whose end rib cannot be built because a rail missed a side edge ending within the fillet's width, the rib is deleted and the global merge cuts the round off (WR-303, #841).
@@ -963,8 +955,6 @@ pub struct UpstreamFixes {
pub topology_failed_evaluations_propagate: bool,
/// `SetProps_Stage3` returns a failed `ClassifyLoops` status before reading its outputs, and a failed stage drops its in-progress marker so a retry reruns it (WR-268, WR-266, #521).
pub heal_stage3_failures_propagate: bool,
/// `CreateCurveSweep` starts a planar path's sweep frame with its z on the plane's normal, not on `MakeUnitOrthoVectors` of the start tangent, so a flat cap stays flat to the rounding (WR-407, #434; SolidCraft change, tangency-labs/smlib e960ca1 (was 1b6f5fa)).
pub sweep_frame_on_planar_path_normal: bool,
/// `SmDatabaseIO::ReadBoolean` ([`crate::topology::database_io::DatabaseReader::read_boolean`]
/// on a reader configured via [`crate::topology::database_io::DatabaseReader::with_fixes`] or
/// [`crate::topology::database_io::DatabaseReader::set_fixes`]) byte-swaps a 4-byte binary
@@ -1022,17 +1012,6 @@ pub struct UpstreamFixes {
pub internal_pole_row_through_knot: bool,
/// `SmSurfOfExtrusion::AssertValid` test 8 point-solves the origin on the generating curve with an unscaled `SM_EFF_ZERO`, so an origin evaluated on the curve at coordinates of about 10 (a text outline's quadratic Bezier) resolves to a few 1e-12 and is reported missing; with this fix that solve uses the issue's coordinate-aware zero `SM_EFF_ZERO * (1.0 + origin.max_dimension())`, its own factor rather than the factor 100 of the neighbouring point tests (WR-511, #1508; tangency-labs/smlib#31).
pub extrusion_origin_scaled_zero: bool,
/// `SmSurfOfExtrusion::EvaluateSTEP` and `EvaluateSTEPPoint` evaluate the clamped
/// copy of the STEP parameters ("clamp input point to surface domain"), not the
/// caller's unclamped `crUV`: with out-of-bounds evaluation off, a parameter
/// outside `m_vAnalUVDomain` no longer extrapolates along the extrusion or past
/// the generator's own domain (WR-61; see
/// `plan/upstream-issues/WR-61--addendum-2026-10-31-step-evaluators-use-clamped-uv.md`).
/// Off by default for pinned C++ parity. The guard is `ContainsPoint2d` with its
/// default `tol = 0`, so a NaN coordinate (never "outside" by that comparison)
/// stays unclamped, and both bodies clamp in the analytic domain, as upstream does
/// (neither reads `SwapUV`).
pub extrusion_step_uses_clamped_uv: bool,
/// `SmBSplineVolume::CalculateContinuitiesSimple` with `bCalcGeometric == FALSE`
/// sets both end knots (`0` and `knot_cnt - 1`) to `SM_CT_DISCONTINUOUS` (matching
/// its `SmBSplineVolume.h` contract, the `bCalcGeometric == TRUE` branch, and
@@ -1047,8 +1026,6 @@ pub struct UpstreamFixes {
/// `plan/upstream-issues/WR-49--addendum-2026-10-31-representational-end-continuities.md`).
/// Off by default for pinned C++ parity.
pub volume_representational_end_continuities: bool,
/// A sheet sweep's open-profile wires are glued where the curves meet (WR-409, #1113).
pub sheet_sweep_joins_profile_wires: bool,
/// `FindPolygonGuess` keeps a suspicious control-net triangle guess as a candidate against the nearest control point's, and returns the nearer one on the surface, so a rational surface whose control points are farther apart than its neighbouring parts does not lose a good guess (WR-444, #1164).
pub point_solve_keeps_triangle_guess: bool,
/// `SmFilletCorner::MakeAnalyticCornerSurface`'s `SER` / `NER` exits return their failing status as an `SmBoolean`, which `SmFilletCorner::CreateCornerPatch` reads as `TRUE` (an analytic surface was made, though none was added) and so skips the Coons patch. With this fix those exits read as `FALSE` and the caller builds the patch. Only the exits that return an upstream status (`SM_ERR` 1001 / `SM_ERR_NULL_POINTER` 1006) change; the port's `Unported`, `InvalidInput` and `PortInvariant` still propagate (WR-254, `plan/upstream-issues/WR-254--smfilletcorner-makeanalyticcornersurface-reports-its-ser-ner--dup2.md`; off by default for pinned C++ parity).
@@ -31,7 +31,7 @@ use crate::geom::object::ObjectRef;
use crate::geom::solution_array::{ObjectKind, SolutionObject};
use crate::intersect::point_class::{ClassGeometry, ClassObject, ClassObjectKind};
use crate::kernel::Kernel;
use crate::math::{Point2, Point3};
use crate::math::{Point2, Point3, Vec3};
use crate::nurbs::b_spline_surface_n_lib::p2::OrientType;
use crate::offset::offset_geometry_creation::{
EdgeuseOffset, OffsetBrep, OffsetExecutive as CreatorExecutive, OffsetGeometryCreation,
@@ -1145,6 +1145,16 @@ impl OffsetBrep<Handle> for EdgeBrep<'_> {
}
}
fn edgeuse_middle_binormal(&mut self, k: &Kernel, u: Handle) -> Result<Vec3> {
let (arena, u) = u.edgeuse()?;
let b = self.brep_mut(arena);
let e = eu::edge(b, u)?
.ok_or_else(|| SolidError::invalid_input(OP, "edge use without edge"))?;
let middle = edge::interval(b, e)?.evaluate(0.5);
let options = eu3::BinormalOptions::default();
Ok(eu3::evaluate_binormal(k, b, u, middle, false, &options)?.binormal)
}
fn edgeuse_normalized_evaluate(
&mut self,
k: &Kernel,
@@ -1226,6 +1236,15 @@ impl CreatorExecutive<Handle> for Executive<'_> {
}
}
fn crosses_at(&self, original: Handle) -> bool {
match original.object {
Object::Topology(Arena::Original, EntityRef::Edgeuse(u)) => {
self.inner.crosses_at_edgeuse(u)
}
_ => false,
}
}
fn has_collapsed_faces(&self) -> bool {
!self.inner.collapsed_faces.is_empty()
}
@@ -232,6 +232,9 @@ pub struct OffsetExecutive {
/// edge uses ([`Self::keep_unmoved_edges`]).
pub(super) kept_edges: BTreeSet<EdgeId>,
pub(super) kept_edgeuses: BTreeSet<EdgeuseId>,
/// Not upstream: the uses of the original edges of a local operation where the two new
/// surfaces cross ([`super::reach::LocalReach::crossing`]), tangent in the input or not.
crossing_edgeuses: BTreeSet<EdgeuseId>,
/// Not upstream (WR-346): the original faces of an inset whose offset collapses (a
/// convex round thinner than the wall), mated across instead of offset
/// (`m_sCollapsedFaces`; only with `Kernel::fixes.offset_collapsed_rounds_mated`).
@@ -297,6 +300,7 @@ impl OffsetExecutive {
moved_faces: Vec::new(),
kept_edges: BTreeSet::new(),
kept_edgeuses: BTreeSet::new(),
crossing_edgeuses: BTreeSet::new(),
collapsed_faces: Vec::new(),
unmated_edgeuses: BTreeSet::new(),
local_faces: Vec::new(),
@@ -459,6 +463,12 @@ impl OffsetExecutive {
self.kept_edgeuses.contains(&edgeuse)
}
/// Not upstream: whether the new surfaces of a local operation cross along the edge of
/// original edge use `edgeuse` ([`super::reach::LocalReach::crossing`]).
pub(super) fn crosses_at_edgeuse(&self, edgeuse: EdgeuseId) -> bool {
self.crossing_edgeuses.contains(&edgeuse)
}
/// Sets `m_eOffsetOperation`.
pub fn set_operation(&mut self, operation: OffsetOperation) {
self.operation = operation;
@@ -615,6 +625,7 @@ impl OffsetExecutive {
self.extension_reaches.clear();
self.faceuse_extension_reaches.clear();
self.edge_bounds.clear();
self.crossing_edgeuses.clear();
}
/// Records how far the surface of original face `face` must be extended for its
@@ -669,7 +680,7 @@ impl OffsetExecutive {
kept.push((*f, surface));
}
}
super::reach::local_extension_reach(
let found = super::reach::local_extension_reach(
kernel,
original,
faceuse,
@@ -677,14 +688,18 @@ impl OffsetExecutive {
&kept,
self.tangency_tol_radians,
&mut self.edge_bounds,
)?
)?;
for &e in found.iter().flat_map(|r| &r.crossing) {
self.crossing_edgeuses
.extend(edge::edgeuses(original, e, None, None)?);
}
// A new edge must cross each neighbouring surface's boundary, not sit on it:
// the mate and the intersection identify points within the body's tolerance,
// so a crossing shorter than that is indistinguishable from touching the
// boundary, and the extension reaches at least that far. With nothing moved
// beside the face (a face given the plane through its own edges), this floor
// is the only margin (the offset branch's is the offset's size).
.map_or(0.0, |r| (r.reach + r.shift).max(original.tolerance()))
found.map_or(0.0, |r| (r.reach + r.shift).max(original.tolerance()))
};
self.extension_reaches.insert(face, reach);
let (primary, mate) = crate::topology::face::p2::faceuses(original, face)?;
@@ -906,7 +906,7 @@ fn planes_line(p: Vec3, a: Tangent, b: Tangent, tangent_sine: f64) -> Option<Tra
/// `F(x0 + τ u) = A τ² + B τ + C` for the quadric's implicit function `F` (a cone's
/// `|P v|² − (r + t z)²`, `v = x − origin`, `z = v·a`, `P` the projection off the axis; a
/// sphere's `|v|² − r²`), with the cone's radius `r0 + rd τ` along the line (a sphere has
/// no nappe: `cone` false); `None` for any other kind.
/// no nappe: `cone` false) and the discriminant `B² − 4 A C`; `None` for any other kind.
#[derive(Debug, Clone, Copy)]
struct AlongLine {
a: Di,
@@ -915,9 +915,37 @@ struct AlongLine {
r0: Di,
rd: Di,
cone: bool,
disc: Di,
}
impl AlongLine {
fn new(a: Di, b: Di, c: Di, r0: Di, rd: Di, cone: bool) -> Self {
let disc = b * b - Di::constant(4.0) * a * c;
Self {
a,
b,
c,
r0,
rd,
cone,
disc,
}
}
/// The same over a piece, its discriminant's value enclosed by its mean-value form
/// about the piece's middle `mid`, `dt` the piece about its middle ([`mean_value`]):
/// the plain enclosure of `B² − 4 A C` grows with every operation's dependency, so a
/// piece where the discriminant keeps one sign is not decided on it.
fn centred(self, mid: &Self, dt: Ivl) -> Self {
Self {
disc: Di {
v: mean_value(self.disc, mid.disc, dt),
d: self.disc.d,
},
..self
}
}
/// Whether the point at `tau` is certified on the cone's nappe (`Some(true)`), certified
/// off it (`Some(false)`), or neither; a sphere's every point is on it.
fn on_nappe(&self, tau: Di) -> Option<bool> {
@@ -950,26 +978,26 @@ fn along_line(q: &Implicit, line: &Trace) -> Option<AlongLine> {
let (p0, pd) = (v.sub(axis.scale(z0)), line.u.sub(axis.scale(zd)));
let t = Di::constant(tan_angle);
let (r0, rd) = (Di::constant(radius) + t * z0, t * zd);
Some(AlongLine {
a: pd.dot(pd) - rd * rd,
b: two * (p0.dot(pd) - r0 * rd),
c: p0.dot(p0) - r0 * r0,
Some(AlongLine::new(
pd.dot(pd) - rd * rd,
two * (p0.dot(pd) - r0 * rd),
p0.dot(p0) - r0 * r0,
r0,
rd,
cone: true,
})
true,
))
}
Implicit::Sphere { centre, radius } => {
let v = line.x0.sub(Dv::constant(centre));
let r = Di::constant(radius);
Some(AlongLine {
a: line.u.dot(line.u),
b: two * v.dot(line.u),
c: v.dot(v) - r * r,
r0: Di::constant(0.0),
rd: Di::constant(0.0),
cone: false,
})
Some(AlongLine::new(
line.u.dot(line.u),
two * v.dot(line.u),
v.dot(v) - r * r,
Di::constant(0.0),
Di::constant(0.0),
false,
))
}
_ => None,
}
@@ -1029,8 +1057,8 @@ enum Plan {
}
/// The discriminant `B² − 4 A C`.
fn discriminant(q: &AlongLine) -> Di {
q.b * q.b - Di::constant(4.0) * q.a * q.c
const fn discriminant(q: &AlongLine) -> Di {
q.disc
}
/// The two roots of `plan`'s formula (`None` where its divisor is not certified apart from
@@ -1161,8 +1189,13 @@ fn planned(plan: &Plan, line: &Trace, q: &AlongLine) -> Option<Vec<Di>> {
#[derive(Debug, Clone, Copy, PartialEq)]
enum PieceBound {
/// An upper bound over the piece and a value the bounded quantity takes at its middle
/// (a lower bound on its largest, which steers the halving only).
Value { upper: f64, lower: f64 },
/// (a lower bound on its largest, which steers the halving only), and whether the two
/// surfaces are certified to cross in every section of the piece ([`Plan::Roots`]).
Value {
upper: f64,
lower: f64,
crosses: bool,
},
/// No meeting anywhere on the piece.
Apart,
/// Not decided on the piece.
@@ -1201,6 +1234,7 @@ fn plane_quadric_piece(
let (Some((line, q)), Some((mline, mq))) = (trace(over), trace(mid)) else {
return PieceBound::Unknown;
};
let q = q.centred(&mq, dt);
let plan = plan_of(&line, &q, quadric, tolerance);
if plan == Plan::Apart {
return PieceBound::Apart;
@@ -1221,6 +1255,7 @@ fn plane_quadric_piece(
Some(upper) => PieceBound::Value {
upper,
lower: attained(&mline, &mq, quadric, tolerance),
crosses: matches!(plan, Plan::Roots { .. }),
},
None => PieceBound::Unknown,
}
@@ -1274,7 +1309,11 @@ fn plane_net_piece(
_ => 0.0,
};
if upper.is_finite() {
PieceBound::Value { upper, lower }
PieceBound::Value {
upper,
lower,
crosses: false,
}
} else {
PieceBound::Unknown
}
@@ -1359,6 +1398,7 @@ fn quadric_distance_piece(over: Dv, mid: Dv, dt: Ivl, quadric: &Implicit) -> Pie
PieceBound::Value {
upper: round_up(enclosed.mag() / root),
lower: m.v.mig() / root,
crosses: false,
}
}
@@ -1422,7 +1462,7 @@ enum EdgeQuantity {
#[derive(Debug, Clone)]
struct EdgeBound {
on: Vec<u64>,
bound: Option<(f64, bool)>,
bound: Option<(f64, bool, bool)>,
}
/// Everything an edge's bound is a function of besides the quantity, as bits: the edge's
@@ -1572,7 +1612,8 @@ fn spans(cn: &NurbsCurve, interval: crate::geom::extent1d::Extent1d) -> Vec<(f64
/// A certified upper bound on the largest value over edge `e` of what `f` bounds on each
/// piece of its parameters (given the point and tangent over the piece, at its middle, and
/// the piece about its middle), and whether some piece is apart.
/// the piece about its middle), whether some piece is apart, and whether the surfaces cross
/// on every piece ([`PieceBound::Value`]'s `crosses`; none apart).
///
/// Every piece is decided first: an undecided one is halved until each piece is bounded or
/// apart, at most [`MAX_INTERVAL_HALVINGS`] times; an edge with a piece still undecided
@@ -1600,7 +1641,7 @@ fn edge_interval_maximum(
bounds: &mut EdgeBounds,
of: EdgeQuantity,
surfaces: Option<&[u64]>,
) -> Result<Option<(f64, bool)>> {
) -> Result<Option<(f64, bool, bool)>> {
let Some(cn) = edge_nurbs(b, e)? else {
return Ok(None);
};
@@ -1625,7 +1666,7 @@ fn interval_maximum(
interval: crate::geom::extent1d::Extent1d,
f: &dyn Fn((Dv, Dv), (Dv, Dv), Ivl) -> PieceBound,
tolerance: f64,
) -> Result<Option<(f64, bool)>> {
) -> Result<Option<(f64, bool, bool)>> {
let mut undecided = spans(cn, interval);
// the bound on a piece; a bound that is not finite bounds nothing
let bound_of = |t0: f64, t1: f64| -> Result<PieceBound> {
@@ -1645,13 +1686,19 @@ fn interval_maximum(
(depth < MAX_INTERVAL_HALVINGS && t0 < tm && tm < t1).then_some(tm)
};
let (mut best, mut apart, mut seen) = (0.0_f64, false, 0_usize);
let mut crosses = true;
let mut bounded: std::collections::BinaryHeap<Bounded> = std::collections::BinaryHeap::new();
// every piece decided: bounded or apart
while let Some((t0, t1, depth)) = undecided.pop() {
seen += 1;
match bound_of(t0, t1)? {
PieceBound::Apart => apart = true,
PieceBound::Value { upper, lower } => {
PieceBound::Value {
upper,
lower,
crosses: c,
} => {
crosses &= c;
best = best.max(lower);
bounded.push(Bounded {
upper,
@@ -1676,7 +1723,7 @@ fn interval_maximum(
while let Some(top) = bounded.pop() {
let tm = halves(top.t0, top.t1, top.depth).filter(|_| seen + 2 <= MAX_INTERVALS);
let Some(tm) = tm.filter(|_| top.halvable && top.upper > best + tolerance) else {
return Ok(Some((top.upper, apart)));
return Ok(Some((top.upper, apart, crosses && !apart)));
};
seen += 2;
let mut parts = Vec::with_capacity(2);
@@ -1684,7 +1731,7 @@ fn interval_maximum(
for (t0, t1) in [(top.t0, tm), (tm, top.t1)] {
match bound_of(t0, t1)? {
PieceBound::Apart => apart = true,
PieceBound::Value { upper, lower } => {
PieceBound::Value { upper, lower, .. } => {
best = best.max(lower);
parts.push(Bounded {
upper,
@@ -1707,7 +1754,7 @@ fn interval_maximum(
}
}
// every piece apart
Ok(Some((0.0, apart)))
Ok(Some((0.0, apart, false)))
}
/// How the line where two planes meet passes a quadric at a corner
@@ -1993,7 +2040,11 @@ fn kept_straight_edge_corner(
/// root's squared distance over each piece by its mean-value form ([`plane_quadric_piece`],
/// [`Plan`]), and the pieces halved to the B-rep's tolerance ([`edge_interval_maximum`]).
/// Pieces where the trace passes the quadric beyond the tolerance (a plane moved off a round,
/// which bends away from it) are apart: listed, adding nothing.
/// which bends away from it) are apart: listed, adding nothing. Where the discriminant,
/// enclosed by its mean-value form, is positive on every piece and a root lies on the nappe
/// ([`Plan::Roots`]), the trace cuts the quadric in every section: the surfaces cross along
/// the edge, even where its faces
/// were tangent in the input, and the edge is listed in [`LocalReach::crossing`].
///
/// The move is the largest distance of an edge point from a moved surface beside it,
/// bounded the same way (a plane's on control points, a quadric's by the mean-value form of
@@ -2136,7 +2187,7 @@ pub fn local_extension_reach(
bounds: &mut EdgeBounds,
of: EdgeQuantity,
surfaces: Option<&[u64]>|
-> Result<(f64, bool)> {
-> Result<(f64, bool, bool)> {
edge_interval_maximum(b, e, f, tolerance, bounds, of, surfaces)?
.ok_or_else(uncertified)
};
@@ -2176,15 +2227,16 @@ pub fn local_extension_reach(
None => net_moved = true,
}
}
let (reach, apart) = match (&own, &other) {
let (reach, apart, crosses) = match (&own, &other) {
(&Exact::Plane(a), &Exact::Plane(c)) => {
match plane_pair(a, c, tangent_sine, tolerance) {
PlanePair::Line => (
hull(&|x| line_distance(x, a, c, tangent_sine, tolerance))?,
false,
false,
),
PlanePair::One => (hull(&|x| plane_distance(x, a))?, false),
PlanePair::Apart => (0.0, true),
PlanePair::One => (hull(&|x| plane_distance(x, a))?, false, false),
PlanePair::Apart => (0.0, true, false),
}
}
(&Exact::Plane(plane), Exact::Quadric(q))
@@ -2220,6 +2272,9 @@ pub fn local_extension_reach(
if apart && !found.apart.contains(&e) {
found.apart.push(e);
}
if crosses && !found.crossing.contains(&e) {
found.crossing.push(e);
}
found.reach = found.reach.max(reach);
// each meeting lies on the moved net: no farther from the edge point than it
if net_moved {
@@ -2435,6 +2490,10 @@ pub struct LocalReach {
/// The edges whose two new surfaces are parallel planes apart: no extension makes them
/// meet, and they add nothing to the reach.
pub apart: Vec<crate::topology::EdgeId>,
/// The edges where a plane and a quadric, the two new surfaces, are certified to cross
/// in every normal section ([`Plan::Roots`] on every piece): they meet at an angle, even
/// where the faces were tangent in the input.
pub crossing: Vec<crate::topology::EdgeId>,
}
/// Every face at `vertex` other than `own` (each once): its edge neighbours at the vertex
@@ -2837,6 +2896,20 @@ mod tests {
x >= want - 1e-9 && x <= want + b.tolerance() + 1e-6
}
/// The edge of `b` between faces `f` and `g`.
fn edge_between(b: &Brep, f: FaceId, g: FaceId) -> EdgeId {
crate::topology::face::p4::edges(b, f, None, false, None)
.unwrap()
.into_iter()
.find(|&e| {
edge::edgeuses(b, e, None, None)
.unwrap()
.into_iter()
.any(|u| eu::face(b, u).unwrap() == Some(g))
})
.unwrap()
}
fn plane_at_x(x: f64) -> Surface {
Surface::from(crate::surface::plane::Plane::from_normal(
Point3::new(x, 0.0, 0.0),
@@ -2850,11 +2923,13 @@ mod tests {
/// planes, parallel, give nothing; the side's other edges, across right angles, reach
/// 0.1. The round, which stays, is reached as far at that edge, and the bottom as far
/// at its corner there (the moved side's line on the bottom meets the round's cylinder
/// 1 from the vertex). Each moved 0.1.
/// 1 from the vertex). Each moved 0.1. The side and the round cross along their tangent
/// edge, on every piece of it; the bottom, whose edges with them stay, lists nothing.
#[test]
fn a_plane_moved_into_a_round_is_reached_where_it_meets_it() {
let mut k = Kernel::default();
let (b, round, side, bottom) = rounded_cube(&mut k);
let tangent = edge_between(&b, side, round);
let into = plane_at_x(19.9);
let moved = [(side, &into)];
for f in [side, round, bottom] {
@@ -2862,6 +2937,8 @@ mod tests {
assert!(bounds(&b, found.reach, 1.0), "{f:?}: {found:?}");
assert!(bounds(&b, found.shift, 0.1), "{f:?}: {found:?}");
assert!(found.apart.is_empty(), "{f:?}: {found:?}");
let crossing: &[EdgeId] = if f == bottom { &[] } else { &[tangent] };
assert_eq!(found.crossing, crossing, "{f:?}: {found:?}");
}
}
@@ -2889,6 +2966,30 @@ mod tests {
assert!(bounds(&b, found.reach, want), "{f:?}: {found:?}");
assert!(bounds(&b, found.shift, 0.1), "{f:?}: {found:?}");
}
// the round crosses both sides, x = 20 and y = 20, along its tangent edges, on every
// piece of them
let other_side = bp1::faces(&b, None)
.unwrap()
.into_iter()
.find(|&f| {
let face = b.face(f).unwrap();
let surface = face.surface.as_ref().unwrap();
let p = surface
.evaluate_point(&k, face.uv_domain.evaluate(0.5, 0.5))
.unwrap();
matches!(surface.kind(), SurfaceKind::Plane(_)) && (p.y - 20.0).abs() < 1e-9
})
.unwrap();
let tangent = [
edge_between(&b, side, round),
edge_between(&b, other_side, round),
];
assert_eq!(reach_of(&k, &b, side, &moved).crossing, tangent[..1]);
let at_round = reach_of(&k, &b, round, &moved);
assert!(
tangent.iter().all(|e| at_round.crossing.contains(e)),
"{at_round:?}"
);
}
/// A cylinder r 1 from z 0 to 2 (its side one face meeting itself across its seam,
@@ -2939,7 +3040,7 @@ mod tests {
};
let piece =
|over: (Dv, Dv), mid: (Dv, Dv), dt: Ivl| quadric_distance_piece(over.0, mid.0, dt, &q);
let (at_seam, apart) = edge_interval_maximum(
let (at_seam, apart, _) = edge_interval_maximum(
&b,
seam,
&piece,
@@ -3088,6 +3189,7 @@ mod tests {
assert!(at_bottom.apart.is_empty(), "{at_bottom:?}");
for found in [at_side, at_round, at_bottom] {
assert!(bounds(&b, found.shift, 0.1), "{found:?}");
assert!(found.crossing.is_empty(), "{found:?}");
}
}
@@ -3405,7 +3507,7 @@ mod tests {
let piece = |over: (Dv, Dv), mid: (Dv, Dv), dt: Ivl| {
plane_net_piece(over, mid, dt, into, &net, &guess)
};
let (reach, apart) = edge_interval_maximum(
let (reach, apart, _) = edge_interval_maximum(
&b,
tangent,
&piece,
@@ -3491,7 +3593,7 @@ mod tests {
let piece = |over: (Dv, Dv), mid: (Dv, Dv), dt: Ivl| {
plane_net_piece(over, mid, dt, tilted, &net, &guess)
};
let (reach, apart) = edge_interval_maximum(
let (reach, apart, _) = edge_interval_maximum(
&b,
arc,
&piece,
@@ -223,6 +223,7 @@ const OP_VERTEX: &str = "offset::offset_geometry_creation::vertex_offset";
const OP_UPDATE: &str = "offset::offset_geometry_creation::update_face_surface_domain";
const OP_EXTEND: &str = "offset::offset_geometry_creation::extend_trim_edges";
const OP_MATE: &str = "offset::offset_geometry_creation::mate_edges";
const OP_TRIM: &str = "offset::offset_geometry_creation::trim_extended_edge";
const OP_EDGEUSE: &str = "offset::offset_geometry_creation::edgeuse_offset";
const OP_EXACT_PIECE: &str = "offset::offset_geometry_creation::exact_piece";
const OP_ANALYTICS_EXACT: &str = "offset::offset_geometry_creation::make_edges_on_analytics_exact";
@@ -269,6 +270,15 @@ fn ignore_soft<T>(r: Result<T>) -> Result<Option<T>> {
}
}
/// Whether `e` is the refusal of an end beside a crossing tangent edge
/// ([`OffsetFailure::TangentEndOffCurve`]).
fn is_tangent_end_off_curve(e: &SolidError) -> bool {
matches!(
e.failure().map(|f| &f.reason),
Some(Reason::Offset(OffsetFailure::TangentEndOffCurve))
)
}
/// `SmTArray::FindElement`.
fn contains<O: PartialEq>(list: &[O], item: &O) -> bool {
list.iter().any(|x| x == item)
@@ -422,6 +432,14 @@ pub trait OffsetExecutive<O> {
let _ = original;
false
}
/// Not upstream: whether the new surfaces of a local operation cross along original
/// edge use `original`'s edge (certified on every normal section,
/// `reach::LocalReach::crossing`), though its faces may have been tangent in the
/// input; `false` unless the adapter says so.
fn crosses_at(&self, original: O) -> bool {
let _ = original;
false
}
/// Not upstream (WR-346): whether any inset face's offset collapsed (a convex round
/// thinner than the wall); `false` unless `Kernel::fixes.offset_collapsed_rounds_mated`.
fn has_collapsed_faces(&self) -> bool {
@@ -697,6 +715,10 @@ pub trait OffsetBrep<O>: ProtoGeometry<O> {
) -> Result<bool>;
/// `SmEdgeuse::IsMarked(eMarkType)` (`topology::topology::Topology::is_marked`).
fn edgeuse_is_marked(&self, k: &Kernel, eu: O, mark: MarkType) -> Result<bool>;
/// Not upstream: `SmEdgeuse::EvaluateBinormal` at the middle of the edge's interval
/// (`topology::edgeuse::p3::evaluate_binormal`): the unit vector across the edge,
/// perpendicular to the face use normal, pointing into the edge use's face.
fn edgeuse_middle_binormal(&mut self, k: &Kernel, eu: O) -> Result<Vec3>;
/// `SmEdgeuse::NormalizedEvaluate(t, bParameterSpaceEval, rPoint, pOptFirstDeriv,
/// bOKToMakeUVTrimCurve)` (`topology::edgeuse::p3::normalized_evaluate`): the point
/// (`want_first_deriv` is `pOptFirstDeriv != NULL`; the derivative is not returned).
@@ -2478,12 +2500,17 @@ impl OffsetGeometryCreation {
/// extended surfaces of the offsets of non-tangent neighbours (non-grazing ones
/// only); or, without candidates, the drop of the base vertex onto the curve. The
/// candidate closest to the base vertex (and not the start already chosen) wins.
/// Not upstream: beside a neighbour already offset (marked) whose faces were tangent in
/// the input and whose new surfaces cross in a local operation
/// (`OffsetExecutive::crosses_at`), that neighbour's vertices on the curve are the only
/// candidates (a lamina base edge still takes its vertex's image).
/// `None` (`bGoodEdge == FALSE`) when an end has no candidate or the interval is
/// zero length. `offset_edgeuse` and `other_offset_edgeuse` are `NotUsed`.
///
/// # Errors
///
/// `SER` of the point solves, intersections and evaluations;
/// [`OffsetFailure::TangentEndOffCurve`] where such a neighbour has no vertex on the
/// curve; `SER` of the point solves, intersections and evaluations;
/// [`SolidError::InvalidInput`] for null objects upstream dereferences. `SER` of
/// `SmEdgeuse::CreateUVTrimCurve` is included.
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
@@ -2664,6 +2691,8 @@ impl OffsetGeometryCreation {
)?;
solutions.append(&on_curve);
}
// (not upstream) the points of the mated neighbours whose new surfaces cross
let mut crossing_ends: Option<SolutionArray<O>> = None;
// Tangent sector handling - extend parameter space and intersect it for one or two curves.
for (itry, &tan_eu) in tan_eus.iter().enumerate() {
if vertex_image {
@@ -2684,6 +2713,10 @@ impl OffsetGeometryCreation {
let tan_edge = brep
.edgeuse_edge(tan_eu)?
.ok_or(dangling("edge use without edge"))?;
let crossing = executive.crosses_at(tan_eu);
if crossing && crossing_ends.is_none() {
crossing_ends = Some(SolutionArray::with_capacity(2));
}
let all_eus = brep.edge_edgeuses(tan_edge)?;
for eu in all_eus {
// skip
@@ -2712,6 +2745,9 @@ impl OffsetGeometryCreation {
if !sols2.is_empty() {
solutions.append(&sols2);
found_3d_vertex = true;
if let Some(ends) = crossing_ends.as_mut().filter(|_| crossing) {
ends.append(&sols2);
}
} // end found a solution check
// not get point on 3DCurve closest to pOffEd0->EndVertex
@@ -2738,6 +2774,9 @@ impl OffsetGeometryCreation {
if !sols2.is_empty() {
solutions.append(&sols2);
found_3d_vertex = true;
if let Some(ends) = crossing_ends.as_mut().filter(|_| crossing) {
ends.append(&sols2);
}
}
}
} // end pTanEu and its mate are marked checks
@@ -2909,6 +2948,23 @@ impl OffsetGeometryCreation {
}
} // end iter every sOffAdjEU
// Not upstream: beside an edge tangent in the input whose new surfaces cross
// (`OffsetExecutive::crosses_at`), already mated, the end is that edge's new
// vertex where it lies on the curve. The cuts of the neighbours' extended
// surfaces are not weighed against it: the plane beside a round cuts the new
// round's curve on both sides of the old vertex, about as far on each (`√(2 r
// δ)`), and the cut on the round's continuation gives an arc across the curve.
// A mated vertex off the curve is refused, not replaced by a nearest cut.
if let Some(ends) = crossing_ends {
if ends.is_empty() {
return Err(
Failure::new(OP_TRIM, OffsetFailure::TangentEndOffCurve).into_error()
);
}
solutions = ends;
face_solutions.clear();
}
let mut found = false;
let mut found_dist = BIG_DOUBLE;
let mut found_param = 0.0;
@@ -3848,7 +3904,10 @@ impl OffsetGeometryCreation {
/// that `TrimExtendedEdge` can trim, the one whose trimmed midpoint is closest to
/// the original edge use's midpoint (the only one, when alone) is trimmed with its
/// UV curves and becomes the geometry of both offset edge uses
/// (`ReplaceEdgeCurves`, oriented by the original edge uses).
/// (`ReplaceEdgeCurves`, oriented by the original edge uses). Not upstream: at an edge
/// whose faces were tangent in the input and whose new surfaces, a plane and a curved
/// surface, cross in a local operation (`OffsetExecutive::crosses_at`), only the curves
/// on the curved face's side of the edge are candidates.
///
/// With [`crate::kernel::UpstreamFixes::offset_analytic_mate_edges`] (the SolidCraft
/// fork's tangency-labs/smlib@fddc366 and @98ab1b0), each marched intersection
@@ -3858,7 +3917,8 @@ impl OffsetGeometryCreation {
/// # Errors
///
/// `SM_ERR_NULL_POINTER` for a null edge use or a missing UV curve; `SM_ERR`
/// when no intersection curve can be trimmed; `SER` of the geometry calls
/// when no intersection curve can be trimmed ([`OffsetFailure::TangentEndOffCurve`]
/// where a curve was refused for it); `SER` of the geometry calls
/// (`GetCreateExtendedSurface` included); [`SolidError::Unported`] for the callees
/// of `TrimExtendedEdge` / `ReplaceEdgeCurves`.
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
@@ -4048,7 +4108,36 @@ impl OffsetGeometryCreation {
});
}
// Not upstream: where faces tangent in the input cross in a local operation
// (`OffsetExecutive::crosses_at`), the plane cuts the curved surface on both sides of
// the old edge, about as far on each (`√(2 r δ)` for a round of radius `r` moved
// `δ`), so the nearest midpoint decides nothing. The edge is the cut on the curved
// face's own side, where the plane is extended past its edge; the other lies on the
// curved surface's continuation past its old tangent line.
let curved_side = if executive.crosses_at(edgeuse)
&& brep.edgeuse_is_tangent_sector(k, edgeuse, self.tangency_tol_degrees())?
{
let plane = |h: O| -> Result<bool> {
Ok(matches!(surface_of(brep, h)?.kind(), SurfaceKind::Plane(_)))
};
let curved = match (plane(surf1)?, plane(surf2)?) {
(true, false) => Some(other_edgeuse),
(false, true) => Some(edgeuse),
_ => None,
};
match curved {
Some(curved) => Some((
edgeuse_normalized_evaluate(k, brep, edgeuse, 0.5, false, false, true)?,
brep.edgeuse_middle_binormal(k, curved)?,
)),
None => None,
}
} else {
None
};
// Now see if we have an intersection curve that has proper trimming
let mut off_curve: Option<SolidError> = None;
let mut min_dist = BIG_DOUBLE;
let mut min_curve: Option<usize> = None;
let mut trim_ivl = Extent1d::new();
@@ -4085,8 +4174,10 @@ impl OffsetGeometryCreation {
return Err(SolidError::null_pointer(OP_MATE));
}
// Trim the extended Surf/Surf intersection
let my_trim = self.trim_extended_edge(
// Trim the extended Surf/Surf intersection (not upstream: a curve that does not
// end at a crossing tangent neighbour's vertex is no candidate, and is the
// refusal when no curve is one)
let my_trim = match self.trim_extended_edge(
k,
brep,
executive,
@@ -4098,7 +4189,22 @@ impl OffsetGeometryCreation {
Some(uv1),
Some(uv2),
mark,
)?;
) {
Err(e) if is_tangent_end_off_curve(&e) => {
off_curve = Some(e);
None
}
r => r?,
};
// (not upstream) a cut on the curved surface's continuation is no candidate
let my_trim = match (my_trim, curved_side) {
(Some(my_trim), Some((orig_mid, into_curved))) => {
let mid_pnt = curve_of(brep, c3)?.evaluate_point(k, my_trim.evaluate(0.5))?;
((mid_pnt - orig_mid).dot(into_curved) > 0.0).then_some(my_trim)
}
(my_trim, _) => my_trim,
};
// when intersection was successfully trimmed
if let Some(my_trim) = my_trim {
@@ -4126,7 +4232,7 @@ impl OffsetGeometryCreation {
// Error Exit - no XSectCurve found
let Some(min_curve) = min_curve else {
return Err(sm_err(OP_MATE));
return Err(off_curve.unwrap_or_else(|| sm_err(OP_MATE)));
};
// arrive here after finding the XSectCurve result whose midpoint that is closest
@@ -4441,7 +4547,9 @@ impl OffsetGeometryCreation {
/// uses leave gaps is always bridged by a cap below. Otherwise, when the executive
/// extends convex edges, lamina and tangent edges are
/// extended/trimmed (`ExtendTrimEdges`) and the others mated (`MateEdges`),
/// problems only reported. Otherwise a non-convex radial sector gets nothing
/// problems only reported (not upstream: a tangent edge whose new surfaces cross in a
/// local operation is mated, and the mate's [`OffsetFailure::TangentEndOffCurve`] is
/// passed on). Otherwise a non-convex radial sector gets nothing
/// (`SM_ERR` for a tangent sector with gaps between the offsets), a lamina edge
/// nothing, an inset on a shell face mates the offset edges to the other face's
/// surface; else the rails (the offset edges' trimmed curves, or the approximated
@@ -4511,7 +4619,12 @@ impl OffsetGeometryCreation {
// No, we have to call ExtendTrimEdges() for Lamina Edges. [cbi B01; B256 ok]
let tan_tol_deg = self.tangency_tol_degrees();
let tan_sec = brep.edgeuse_is_tangent_sector(k, original_edgeuse, tan_tol_deg)?;
// Not upstream: in a local operation, faces tangent in the input whose new
// surfaces cross (certified in every normal section by the reach) meet at an
// angle: their edge is mated (on the curved face's side, `mate_edges`), not
// extended and trimmed along its old line.
let tan_sec = !executive.crosses_at(original_edgeuse)
&& brep.edgeuse_is_tangent_sector(k, original_edgeuse, tan_tol_deg)?;
// Not upstream (opt-in `Kernel::fixes.local_operation_step_walls`, fork
// tangency-labs/smlib 5d428ae): a local operation that moved only one of two
@@ -4572,7 +4685,7 @@ impl OffsetGeometryCreation {
if matches!(
e.failure().map(|f| &f.reason),
Some(Reason::Offset(OffsetFailure::ExactCurveUncut))
) =>
) || is_tangent_end_off_curve(&e) =>
{
return Err(e)
}
@@ -9,9 +9,8 @@
//! Deviations: owned copies retain the original inputs; matrices and partial
//! outputs are explicit. Scaling sampling is bounded to 1,000,000 points; no
//! recursion. Null/non-B-spline profiles are skipped, as the upstream cast does.
//! **A planar path's frame starts on the plane's normal** (opt-in
//! `Kernel::fixes.sweep_frame_on_planar_path_normal`, WR-407; SolidCraft change,
//! tangency-labs/smlib 1b6f5fa, tangency-labs/solidcore#434; see
//! **A planar path's frame starts on the plane's normal** (WR-407; SolidCraft change,
//! tangency-labs/smlib e960ca1 (was 1b6f5fa), tangency-labs/solidcore#434; see
//! [`prepare_curve_sweep`]).
//! **The profile can be swept where it lies** (`SweepOptions::profile_at_path_start`,
//! the SolidCraft fork's `bProfileAtPathStart`, tangency-labs/smlib 2dd71a0, WR-522,
@@ -278,24 +277,21 @@ pub fn prepare_curve_sweep(
// out with its control points spread about its plane (2.8e-5 mm in the #434 case against a
// 1e-5 zone tolerance), which a boolean with another wall on that plane cannot resolve. So
// a planar path starts the frame with its z on the plane's normal; the top is then flat to
// the rounding. Opt-in (`Kernel::fixes.sweep_frame_on_planar_path_normal`, WR-407);
// upstream keeps the `MakeUnitOrthoVectors` frame.
if k.fixes.sweep_frame_on_planar_path_normal {
let plane = path.is_planar(ZONE_TOL_3D)?;
// `SmCurve::IsPlanar(SM_ZONE_TOL_3D, NULL, &sPlaneNormal)` with
// `sPlaneNormal.Unitize()` in place before the test.
if let Some(plane_normal) = plane.is_planar.then_some(plane.normal) {
if let Ok(normal) = plane_normal.unitize() {
if normal.dot(from_x).abs() < EFF_ZERO_SQRT {
let from_z = normal - normal.dot(from_x) * from_x;
if from_z.unitize().is_ok() {
from_y = from_z.cross(from_x);
} else if let Some([x, y, _]) =
ignored(passed_on(tangent.make_unit_ortho_vectors(None), p)?)?
{
from_x = x;
from_y = y;
}
// the rounding (WR-407); upstream keeps the `MakeUnitOrthoVectors` frame.
let plane = path.is_planar(ZONE_TOL_3D)?;
// `SmCurve::IsPlanar(SM_ZONE_TOL_3D, NULL, &sPlaneNormal)` with
// `sPlaneNormal.Unitize()` in place before the test.
if let Some(plane_normal) = plane.is_planar.then_some(plane.normal) {
if let Ok(normal) = plane_normal.unitize() {
if normal.dot(from_x).abs() < EFF_ZERO_SQRT {
let from_z = normal - normal.dot(from_x) * from_x;
if from_z.unitize().is_ok() {
from_y = from_z.cross(from_x);
} else if let Some([x, y, _]) =
ignored(passed_on(tangent.make_unit_ortho_vectors(None), p)?)?
{
from_x = x;
from_y = y;
}
}
}
@@ -463,13 +459,13 @@ mod tests {
Ok((max_z, end - start))
}
/// With `fixes.sweep_frame_on_planar_path_normal` (WR-407, #434) the frame's z is the
/// path plane's normal: the path is turned into the global XY plane and the wall's
/// height is the frame's fixed z.
/// The frame's z is the path plane's normal (WR-407, #434): the path is turned into the
/// global XY plane and the wall's height is the frame's fixed z. Upstream's
/// `MakeUnitOrthoVectors` of the +Y start tangent put the frame's z in the path's plane
/// instead, so the path stayed off z = 0.
#[test]
fn issue_434_planar_path_frame_keeps_the_plane_normal_as_the_frame_z() -> Result<()> {
let mut k = Kernel::default();
k.fixes.sweep_frame_on_planar_path_normal = true;
let k = Kernel::default();
let (max_z, height) = issue_434_half_circle_frame(&k)?;
assert!(max_z < 1e-12, "path leaves the frame's plane by {max_z}");
// (the frame's handedness may turn the height down; the sweep frame it is carried
@@ -482,21 +478,6 @@ mod tests {
Ok(())
}
/// Default (pinned SMLib `c9c5979`): `MakeUnitOrthoVectors` of the +Y start tangent
/// puts the frame's z in the path's plane, so the path is not turned into the global
/// XY plane (upstream behaviour, WR-407).
#[test]
fn issue_434_default_keeps_the_upstream_sweep_frame() -> Result<()> {
let k = Kernel::default();
assert!(!k.fixes.sweep_frame_on_planar_path_normal);
let (max_z, _) = issue_434_half_circle_frame(&k)?;
assert!(
max_z > 1.0,
"upstream's frame keeps the path off z = 0: {max_z}"
);
Ok(())
}
#[test]
fn preparation_preserves_inputs_and_recovers_the_original_path() -> Result<()> {
let k = Kernel::default();
@@ -14,8 +14,7 @@
//! 4096; the repeated linear sweep checks that bound before its end cap, which moves
//! the profile once per repetition. No additional recursion. Unavailable calls retain
//! their full caller tails.
//! Not upstream (WR-409, tangency-labs/solidcore#1113, opt-in
//! `Kernel::fixes.sheet_sweep_joins_profile_wires`): a profile that bounds no planar
//! Not upstream (WR-409, tangency-labs/solidcore#1113): a profile that bounds no planar
//! face becomes wires joined where its curves meet (`join_profile_wire_ends`).
//! Not upstream (WR-442, tangency-labs/solidcore#478, opt-in
//! `Kernel::fixes.revolve_nested_shells_as_cavities`): after a full turn's
@@ -249,15 +248,14 @@ fn targets(
}
}
}
/// Not upstream (WR-409, tangency-labs/solidcore#1113, opt-in
/// `Kernel::fixes.sheet_sweep_joins_profile_wires`; the SolidCraft fork's
/// Not upstream (WR-409, tangency-labs/solidcore#1113; the SolidCraft fork's
/// `sm_CreateJoinedWireEdges`): the profile's wire edges `edges`, in the curves' order,
/// joined where they meet. An end within the B-rep's tolerance of an earlier wire's end
/// is glued to that earlier vertex (`GlueVertices`), so two curves meeting at a point
/// share one vertex there. Without it the topology sweep's copies of two coincident
/// vertices came out coincident, each with a preimage, and its stitch refused them: a
/// partial revolve or repeated linear sweep of an open profile of more than one curve
/// failed. Nothing is done without the opt-in.
/// failed.
///
/// # Errors
///
@@ -268,9 +266,6 @@ fn join_profile_wire_ends(
edges: &[crate::topology::EdgeId],
) -> Result<()> {
use crate::topology::{edge, vertex, VertexId};
if !a.kernel(brep)?.fixes.sheet_sweep_joins_profile_wires {
return Ok(());
}
a.with_primitive_brep(brep, |k, b| {
let tol = b.tolerance();
// the distinct vertices of the wires so far
@@ -880,8 +875,9 @@ mod tests {
}
/// An open chain of two lines (out from r 6 to 8, then up 2) revolved half a turn into
/// a sheet joins at its meeting vertex with the opt-in (WR-409): two faces, the flat
/// ring sector and the cylinder, of area (8^2 - 6^2) pi / 2 + 8 pi 2.
/// a sheet joins at its meeting vertex (WR-409): two faces, the flat ring sector and
/// the cylinder, of area (8^2 - 6^2) pi / 2 + 8 pi 2. Upstream left the two wires
/// apart and the sweep failed.
#[test]
fn issue_1113_open_chain_revolves_into_a_joined_sheet() {
use crate::mass::integrator::properties::{face_area, face_properties};
@@ -896,7 +892,6 @@ mod tests {
let (o, z) = (Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
let mut k = Kernel::default();
k.fixes.sheet_sweep_joins_profile_wires = true;
let mut sheet = create_rotational_sweep(&mut k, &curves, o, z, 180.0, false).unwrap();
let faces = crate::topology::brep::p1::faces(&sheet, None)
.unwrap()
@@ -944,4 +939,83 @@ mod tests {
);
}
}
/// Goldens `sweep.api_brep` `issue1463_drilled_hole_{pz,mz}_120` (WR-409): a drilled
/// hole's open profile, the wall r 2 from z 5 down to 1 and the cone from there to the
/// axis at the origin, turned 120 degrees about +z and about -z, is a sheet of two
/// cone faces (the wall's of half-angle 0) meeting at the profile's middle vertex:
/// 5 vertices (the apex is one), 6 edges, valid at Level 2.
/// By Pappus the wall is 2 pi / 3 * 2 * 4 = 16 pi / 3 and the cone, slant sqrt 5
/// about radius 1, is 2 pi sqrt 5 / 3. Upstream left the two wires apart and both
/// sweeps failed (1001).
#[test]
fn issue_1463_drilled_hole_profile_revolves_part_way_into_a_joined_sheet() {
use crate::mass::integrator::properties::{face_area, face_properties};
use crate::nurbs::b_spline_surface_n_lib::p2::OrientType;
use crate::surface::surface::SurfaceKind;
use crate::topology::brep::validation::{check_brep, CheckLevel};
use crate::topology::face::p2::PPF_AREA;
let chain = [
segment([2.0, 0.0, 5.0], [2.0, 0.0, 1.0]),
segment([2.0, 0.0, 1.0], [0.0, 0.0, 0.0]),
];
let curves: Vec<Option<&Curve>> = chain.iter().map(Some).collect();
let pi = std::f64::consts::PI;
let want = [2.0 * pi * 5.0_f64.sqrt() / 3.0, 16.0 * pi / 3.0];
for z in [1.0, -1.0] {
let mut k = Kernel::default();
let (o, axis) = (Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, z));
let mut sheet = create_rotational_sweep(&mut k, &curves, o, axis, 120.0, false)
.unwrap_or_else(|e| panic!("axis z {z}: {e}"));
let faces = crate::topology::brep::p1::faces(&sheet, None).unwrap();
assert_eq!(faces.len(), 2, "axis z {z}");
let edges = crate::topology::brep::p1::edges(&sheet, None).unwrap();
assert_eq!(edges.len(), 6, "axis z {z}");
let vertices = crate::topology::brep::p1::vertices(&sheet, None).unwrap();
assert_eq!(vertices.len(), 5, "axis z {z}");
// The profile's top end (2, 0, 5) turned 120 degrees the axis's way round.
let top = Point3::new(-1.0, z * 3.0_f64.sqrt(), 5.0);
let turned = vertices.iter().any(|&v| {
crate::topology::vertex::point(&sheet, v).is_ok_and(|p| p.distance(top) < 1e-9)
});
assert!(turned, "axis z {z}: no vertex at {top:?}");
let check = check_brep(&mut k, &mut sheet, CheckLevel::Level2).unwrap();
assert!(check.is_valid(), "axis z {z}: {:?}", check.lines());
let mut areas = Vec::new();
for &f in &faces {
let surface = crate::topology::face::p2::surface(&sheet, f)
.unwrap()
.expect("face surface");
assert!(
matches!(surface.kind(), SurfaceKind::Cone(_)),
"axis z {z}: face {f:?} is not a cone"
);
// `face_properties` at accuracy 1e-9, as in the test above.
let est = face_area(&k, &mut sheet, f, 1e-4).unwrap();
let a = face_properties(
&k,
&mut sheet,
f,
OrientType::Same,
1e-9,
Point3::new(0.0, 0.0, 0.0),
est,
0.0,
PPF_AREA,
)
.unwrap()
.area
.abs();
areas.push(a);
}
areas.sort_by(f64::total_cmp);
for (got, want) in areas.iter().zip(want) {
assert!(
(got - want).abs() <= 1e-8 * want,
"axis z {z}: face area {got}, want {want}"
);
}
}
}
}
+26 -7
View File
@@ -987,16 +987,35 @@ mod tests {
#[test]
fn revolution_of_an_unbounded_line_measures_from_its_profile() {
// An unbounded line profile through (2, 0, 0): the kernel places the
// revolution from the line's far start, its X axis (-1, 0, 0), while its u = 0
// An unbounded line profile through (2, 0, 0), placed from the line's far
// start (upstream's `CreateCanonical`), its X axis (-1, 0, 0), while its u = 0
// is the profile at +X (u = 90 at +Y): the frame of its angles is turned half a
// turn from its position.
// turn from its position. (The kernel's `create_canonical` places it from the
// profile's origin, X = (1, 0, 0).)
let k = k();
let profile = Curve::from(Line::create_canonical(p(2.0, 0.0, 0.0), v3(1.0, 0.0, 2.0)));
let s = Surface::from(
SurfOfRevolution::create_canonical(&k, profile, p(0.0, 0.0, 0.0), Vec3::Z, false)
.unwrap(),
let profile = || Curve::from(Line::create_canonical(p(2.0, 0.0, 0.0), v3(1.0, 0.0, 2.0)));
let canonical =
SurfOfRevolution::create_canonical(&k, profile(), p(0.0, 0.0, 0.0), Vec3::Z, false)
.unwrap();
assert_eq!(canonical.position().x_axis(), v3(1.0, 0.0, 0.0));
let domain = Extent2d::from_bounds(
0.0,
-crate::math::INFINITE_PARAMETER,
360.0,
crate::math::INFINITE_PARAMETER,
);
let mut far_start = SurfOfRevolution::new(
&k,
profile(),
p(0.0, 0.0, 0.0),
v3(-1.0, 0.0, 0.0),
v3(0.0, -1.0, 0.0),
domain,
false,
)
.unwrap();
far_start.make_nurb(&k).unwrap();
let s = Surface::from(far_start);
assert_eq!(position(&s).x_axis(), v3(-1.0, 0.0, 0.0));
let at90 = s.evaluate_step_point(&k, Point2::new(90.0, 0.0)).unwrap();
assert!(at90.distance(p(0.0, 2.0, 0.0)) <= 1e-15);
@@ -89,8 +89,7 @@
//! turns the generator's flag on (upstream permanently, through the `const`
//! method's pointer); here a copy is used for the solve.
//! - **Clamped STEP parameters.** `EvaluateSTEP` and `EvaluateSTEPPoint` clamp a copy
//! of the STEP parameters and then evaluate the caller's; the port keeps that by
//! default and, with `Kernel::fixes.extrusion_step_uses_clamped_uv`, evaluates the
//! of the STEP parameters and then evaluate the caller's; the port evaluates the
//! clamped copy in the analytic UV domain (both bodies ignore `SwapUV`), as their
//! comment says (WR-61; see
//! `plan/upstream-issues/WR-61--addendum-2026-10-31-step-evaluators-use-clamped-uv.md`).
@@ -1249,16 +1248,13 @@ impl SurfOfExtrusion {
// ---- evaluation -----------------------------------------------------------------
/// The STEP parameters the evaluators use: `uv`, clamped to the analytic UV domain
/// when `Kernel::fixes.extrusion_step_uses_clamped_uv` is set and out-of-bounds
/// evaluation is off, else `uv` itself (the pinned C++ evaluates `crUV`). The guard
/// is `ContainsPoint2d` with its default `tol = 0`, so a NaN coordinate (never
/// outside by that comparison) stays unclamped. Both upstream bodies clamp in
/// `m_vAnalUVDomain` and neither reads `SwapUV`.
fn step_eval_uv(&self, k: &Kernel, uv: Point2) -> Point2 {
if k.fixes.extrusion_step_uses_clamped_uv
&& !self.is_out_of_bounds_enabled()
&& !self.anal_uv_domain.contains_point(uv, 0.0)
{
/// when out-of-bounds evaluation is off, else `uv` itself (WR-61; the pinned C++
/// clamps a copy and evaluates `crUV`). The guard is `ContainsPoint2d` with its
/// default `tol = 0`, so a NaN coordinate (never outside by that comparison) stays
/// unclamped. Both upstream bodies clamp in `m_vAnalUVDomain` and neither reads
/// `SwapUV`.
fn step_eval_uv(&self, uv: Point2) -> Point2 {
if !self.is_out_of_bounds_enabled() && !self.anal_uv_domain.contains_point(uv, 0.0) {
self.anal_uv_domain.clamp_point(uv)
} else {
uv
@@ -1269,10 +1265,9 @@ impl SurfOfExtrusion {
/// `(hu + 1) * (hv + 1)` vectors, entry `i * (hv + 1) + j` the derivative of order
/// `i` in u and `j` in v. Only the position, `Sv = E`, the pure u derivatives (the
/// generator's STEP derivatives at `uv.x`, evaluated with the surface's
/// out-of-bounds flag) are non-zero. `uv` is not clamped by default (upstream
/// clamps a copy it does not use); with
/// `Kernel::fixes.extrusion_step_uses_clamped_uv` and out-of-bounds evaluation
/// off, both coordinates are clamped to the analytic domain first (WR-61).
/// out-of-bounds flag) are non-zero. With out-of-bounds evaluation off, both
/// coordinates are clamped to the analytic domain first (WR-61; upstream clamps a
/// copy it does not use).
///
/// # Errors
///
@@ -1332,7 +1327,7 @@ impl SurfOfExtrusion {
// per SMLib practice clamp the input point to the surface domain (upstream
// clamps a copy it then ignores; WR-61 makes that copy the one evaluated)
let uv = self.step_eval_uv(k, uv);
let uv = self.step_eval_uv(uv);
// the generator with the surface's out-of-bounds flag
let gen_own = self.gen()?;
@@ -1366,17 +1361,16 @@ impl SurfOfExtrusion {
/// The point at the STEP parameters `uv` (`EvaluateSTEPPoint`): the generator's
/// STEP point at `uv.x` (with its own out-of-bounds flag) plus `uv.y` times the
/// extrusion vector. `uv` is not clamped by default (upstream clamps a copy it
/// does not use); with `Kernel::fixes.extrusion_step_uses_clamped_uv` and
/// out-of-bounds evaluation off, both coordinates are clamped to the analytic
/// domain first (WR-61).
/// extrusion vector. With out-of-bounds evaluation off, both coordinates are
/// clamped to the analytic domain first (WR-61; upstream clamps a copy it does not
/// use).
///
/// # Errors
///
/// `SM_ERR_NULL_POINTER` without generator; those of the generator's evaluation.
pub fn evaluate_step_point(&self, k: &Kernel, uv: Point2) -> Result<Point3> {
// per SMLib practice clamp the input point to the surface domain (WR-61)
let uv = self.step_eval_uv(k, uv);
let uv = self.step_eval_uv(uv);
let pv = first(&gen_evaluate_step(k, self.gen()?, uv.x, 0)?)?;
Ok(pv + uv.y * self.extrusion_vec)
}
@@ -2560,22 +2554,23 @@ mod tests {
}
#[test]
fn step_clamped_uv_fix_is_off_by_default() {
fn step_evaluators_clamp_to_the_surface_domain() {
let k = k();
assert!(!k.fixes.extrusion_step_uses_clamped_uv);
// V outside the domain [0, 3] of the unit X line swept along Z: the caller's
// parameters extrapolate, as the pinned C++ does with `crUV`.
// V outside the domain [0, 3] of the unit X line swept along Z: the parameters
// are clamped to the surface's domain (WR-61); the pinned C++ extrapolates with
// the caller's `crUV`, to (0.5, 0, 4).
let s = step_clamp_line_surface(&k, Extent2d::from_bounds(0.0, 0.0, 1.0, 3.0), false);
assert_eq!(s.anal_uv_domain, Extent2d::from_bounds(0.0, 0.0, 1.0, 3.0));
assert_eq!(s.step_uv_domain(), s.anal_uv_domain);
assert_eq!(
s.evaluate_step_point(&k, Point2::new(0.5, 4.0)).unwrap(),
Vec3::new(0.5, 0.0, 4.0)
Vec3::new(0.5, 0.0, 3.0)
);
// A generator whose own STEP interval is [0, 1] under a surface U domain of
// [0.2, 0.8]: u = 0.9 is inside the generator and outside the surface.
// [0.2, 0.8]: u = 0.9 is inside the generator and outside the surface, and the
// clamp is the surface's.
let s = step_clamp_owned_line_surface(&k, Extent2d::from_bounds(0.2, 0.0, 0.8, 3.0));
assert_eq!(s.anal_uv_domain, Extent2d::from_bounds(0.2, 0.0, 0.8, 3.0));
assert_eq!(
@@ -2584,15 +2579,16 @@ mod tests {
);
assert_eq!(
s.evaluate_step_point(&k, Point2::new(0.9, 1.0)).unwrap(),
Vec3::new(0.9, 0.0, 1.0)
Vec3::new(0.8, 0.0, 1.0)
);
// The matrix: position and the pure derivatives; an entry upstream does not
// write keeps the caller's value and a short array is refused.
// The matrix: position and the pure derivatives, both coordinates clamped; an
// entry upstream does not write keeps the caller's value and a short array is
// refused.
let mut d = vec![Vec3::new(9.0, 9.0, 9.0); 4];
s.evaluate_step_into(&k, Point2::new(0.9, 4.0), 1, 1, &mut d)
.unwrap();
assert_eq!(d[0], Vec3::new(0.9, 0.0, 4.0));
assert_eq!(d[0], Vec3::new(0.8, 0.0, 3.0));
assert_eq!(d[1], Vec3::Z);
assert_eq!(d[2], Vec3::new(1.0, 0.0, 0.0));
assert_eq!(d[3], Vec3::new(9.0, 9.0, 9.0), "untouched entry");
@@ -2630,9 +2626,8 @@ mod tests {
}
#[test]
fn step_clamped_uv_fix_clamps_when_enabled_and_out_of_bounds_off() {
let mut k = k();
k.fixes.extrusion_step_uses_clamped_uv = true;
fn step_clamp_reads_the_analytic_domain_with_out_of_bounds_off() {
let k = k();
// V outside [0, 3] on the unit X line swept along Z.
let s = step_clamp_line_surface(&k, Extent2d::from_bounds(0.0, 0.0, 1.0, 3.0), false);
@@ -2678,11 +2673,8 @@ mod tests {
Vec3::new(0.5, 0.0, 3.0)
);
// In-domain and exact-boundary parameters are bit-identical with the fix off
// and on.
let off = Kernel::default();
let inside = step_clamp_owned_line_surface(&off, Extent2d::from_bounds(0.2, 0.0, 0.8, 3.0));
let on = step_clamp_owned_line_surface(&k, Extent2d::from_bounds(0.2, 0.0, 0.8, 3.0));
// In-domain and exact-boundary parameters are evaluated as given: the line's
// point `(u, 0, v)` and its matrix, unclamped.
for uv in [
Point2::new(0.5, 1.5),
Point2::new(0.2, 0.0),
@@ -2691,42 +2683,29 @@ mod tests {
Point2::new(0.8, 0.0),
Point2::new(0.5, 3.0),
] {
let at = Vec3::new(uv.x, 0.0, uv.y);
assert_eq!(
on.evaluate_step_point(&k, uv).unwrap(),
inside.evaluate_step_point(&off, uv).unwrap(),
s.evaluate_step_point(&k, uv).unwrap(),
at,
"point at {uv:?}"
);
assert_eq!(
on.evaluate_step(&k, uv, 2, 2).unwrap(),
inside.evaluate_step(&off, uv, 2, 2).unwrap(),
"matrix at {uv:?}"
);
let d = s.evaluate_step(&k, uv, 2, 2).unwrap();
assert_eq!(d[0], at, "matrix at {uv:?}");
assert_eq!(d[1], Vec3::Z, "matrix at {uv:?}");
assert_eq!(d[3], Vec3::new(1.0, 0.0, 0.0), "matrix at {uv:?}");
}
// Out-of-bounds evaluation stays the old extrapolation with the fix either
// way.
let mut off_oob =
step_clamp_owned_line_surface(&off, Extent2d::from_bounds(0.2, 0.0, 0.8, 3.0));
off_oob.set_out_of_bounds_enabled(true);
let mut on_oob =
step_clamp_owned_line_surface(&k, Extent2d::from_bounds(0.2, 0.0, 0.8, 3.0));
on_oob.set_out_of_bounds_enabled(true);
// Out-of-bounds evaluation extrapolates, unclamped.
let mut oob = step_clamp_owned_line_surface(&k, Extent2d::from_bounds(0.2, 0.0, 0.8, 3.0));
oob.set_out_of_bounds_enabled(true);
assert_eq!(
off_oob
.evaluate_step_point(&off, Point2::new(0.9, 4.0))
.unwrap(),
Vec3::new(0.9, 0.0, 4.0)
);
assert_eq!(
on_oob
.evaluate_step_point(&k, Point2::new(0.9, 4.0))
.unwrap(),
oob.evaluate_step_point(&k, Point2::new(0.9, 4.0)).unwrap(),
Vec3::new(0.9, 0.0, 4.0)
);
}
#[test]
fn step_clamped_uv_fix_keeps_the_generator_flag_policy() {
fn step_clamp_overrides_the_generator_flag() {
// A B-spline generator whose own out-of-bounds flag is on under a surface
// whose flag is off: `EvaluateSTEP` evaluates a copy with the surface's flag,
// `EvaluateSTEPPoint` the generator itself.
@@ -2748,33 +2727,20 @@ mod tests {
.unwrap()
.is_out_of_bounds_enabled());
assert!(!s.is_out_of_bounds_enabled());
// u = 1.5 is outside both domains: the generator extrapolates for the point
// (the quadratic's Bernstein form at 1.5 is (3, -1.5, 0)), the STEP copy
// clamps to u = 1.
// u = 1.5 is outside both domains: both evaluators clamp it to u = 1 first, so
// the generator's flag does not change the point (the pinned C++ extrapolates
// the generator for the point, to the quadratic's Bernstein form at 1.5,
// (3, -1.5, 1)).
assert_eq!(
s.evaluate_step_point(&k, Point2::new(1.5, 0.5)).unwrap(),
Vec3::new(3.0, -1.5, 1.0)
Vec3::new(2.0, 0.0, 1.0)
);
assert_eq!(
s.evaluate_step(&k, Point2::new(1.5, 0.5), 1, 1).unwrap()[0],
Vec3::new(2.0, 0.0, 1.0)
);
// With the fix both clamp the parameters, so the generator's flag no longer
// changes the point.
let mut k_on = Kernel::default();
k_on.fixes.extrusion_step_uses_clamped_uv = true;
assert_eq!(
s.evaluate_step_point(&k_on, Point2::new(1.5, 0.5)).unwrap(),
Vec3::new(2.0, 0.0, 1.0)
);
assert_eq!(
s.evaluate_step(&k_on, Point2::new(1.5, 0.5), 1, 1).unwrap()[0],
Vec3::new(2.0, 0.0, 1.0)
);
// The same generator with its flag off clamps for the point too: only the
// generator's own flag decides `EvaluateSTEPPoint`.
// The same generator with its flag off gives the same point.
let k_off = Kernel::default();
let s_off = SurfOfExtrusion::new(
&k_off,
@@ -511,13 +511,12 @@ impl SurfOfRevolution {
/// The surface of one full revolution of `swept_curve` about the axis through
/// `axis_point` along `axis_direction` (`CreateCanonical`, co-ported from unit
/// `surface.surf_of_revolution_p2`): the X axis points from the axis point to the
/// curve's STEP start point (with
/// [`UpstreamFixes::revolution_frame_from_profile_origin`](crate::kernel::UpstreamFixes::revolution_frame_from_profile_origin),
/// its point at parameter 0 when the STEP interval holds it, WR-738, and its start
/// when that is on the axis; or, when that is on the axis, its point at 45%; a
/// point is on the axis within 1 degree of its direction, with the fix by its
/// distance from it), the
/// domain is `[0, 360]` by the curve's STEP interval; the NURB is made by
/// curve's point at parameter 0 when the STEP interval holds it (WR-738; upstream
/// takes the STEP start point), else to its start; when that point is on the axis,
/// to its start, and when that is on the axis too, to its point at 45%. A point is
/// on the axis by its distance from it (upstream: within 1 degree of its
/// direction). The domain is `[0, 360]` by the curve's STEP interval; the NURB is
/// made by
/// [`Self::make_nurb`]. A zero axis (squared length below `SM_EFF_ZERO_SQ`) is
/// taken as `(0, 0, 1)`, as upstream's unchecked `Unitize`.
///
@@ -538,31 +537,24 @@ impl SurfOfRevolution {
// locals
let crv_ivl = gen_step_interval(&swept_curve)?;
// SweptCurve start point; with the fix (WR-738) the point at its parameter origin
// when the interval holds it: the point the profile is defined from, where an
// unbounded profile's start is SM_INFINITE_PARAMETER away (past the axis for a
// line crossing it, so the frame would face the other nappe)
let fix = k.fixes.revolution_frame_from_profile_origin;
let from_origin = fix && crv_ivl.contains_value(0.0, 0.0);
// the point at the profile's parameter origin when the interval holds it (WR-738;
// upstream: the SweptCurve start point): the point the profile is defined from,
// where an unbounded profile's start is SM_INFINITE_PARAMETER away (past the axis
// for a line crossing it, so the frame would face the other nappe)
let from_origin = crv_ivl.contains_value(0.0, 0.0);
let ref_param = if from_origin { 0.0 } else { crv_ivl.min() };
let mut pnt = first(&gen_evaluate_step(k, &swept_curve, ref_param, 0)?)?;
// Vector from SweptCurve Start to RotAxis point
let mut x_vec = pnt - axis_point;
// on the axis: upstream within 1 degree of its direction (which takes a point
// far along the axis for one on it); with the fix by the point's distance from
// the axis, `|Z x x_vec|`, under the bound the Y axis below is checked against
let on_axis = |x_vec: Vec3| {
if fix {
z_vec.cross(x_vec).length_squared() < crate::math::EFF_ZERO_SQ
} else {
x_vec.length() < EFF_ZERO || x_vec.is_parallel_to(z_vec, 1.0)
}
};
// on the axis by the point's distance from the axis, `|Z x x_vec|`, under the
// bound the Y axis below is checked against (upstream: within 1 degree of its
// direction, which takes a point far along the axis for one on it)
let on_axis = |x_vec: Vec3| z_vec.cross(x_vec).length_squared() < crate::math::EFF_ZERO_SQ;
// with the fix, a profile origin on the axis gives way to the start first, the
// point upstream takes (a segment through the axis whose origin and 45% point
// are both on it, e.g. `s (1, 0, 1)` on [-9, 11])
// a profile origin on the axis gives way to the start first, the point upstream
// takes (a segment through the axis whose origin and 45% point are both on it,
// e.g. `s (1, 0, 1)` on [-9, 11])
if from_origin && on_axis(x_vec) {
pnt = first(&gen_evaluate_step(k, &swept_curve, crv_ivl.min(), 0)?)?;
x_vec = pnt - axis_point;
@@ -2434,15 +2426,9 @@ mod tests {
/// The full revolution about Z through the origin of the unbounded line
/// `point + s * vector` (`Line::create_canonical`, as the STEP reader builds a
/// `LINE` profile), with [`UpstreamFixes::revolution_frame_from_profile_origin`]
/// (`crate::kernel::UpstreamFixes`) set to `fix`.
fn unbounded_line_revolution(
fix: bool,
point: Point3,
vector: Vec3,
) -> (Kernel, SurfOfRevolution) {
let mut k = k();
k.fixes.revolution_frame_from_profile_origin = fix;
/// `LINE` profile).
fn unbounded_line_revolution(point: Point3, vector: Vec3) -> (Kernel, SurfOfRevolution) {
let k = k();
let line = Line::create_canonical(point, vector);
let s =
SurfOfRevolution::create_canonical(&k, Curve::from(line), Vec3::ZERO, Vec3::Z, false)
@@ -2477,31 +2463,18 @@ mod tests {
/// 10303-42 `surface_of_revolution`: `u = 0` is the profile itself), so
/// `λ(1) = (2, 0, 1)` is `(u, v) = (0, 1)`; turned by 90° about Z it is
/// `(0, 2, 1) = (90, 1)`; `λ(-1/2) = (1/2, 0, -1/2)`, between the crossing and the
/// profile's origin, is `(0, -1/2)`. Upstream's frame faces the line's start at
/// `s = -SM_INFINITE_PARAMETER`, past the axis: X = (-1, 0, 0), and these points,
/// on the nappe of the profile's origin, do not invert (`SM_ERR`).
/// profile's origin, is `(0, -1/2)`. The frame faces the profile's origin,
/// X = (1, 0, 0) (WR-738). Upstream's frame faces the line's start at
/// `s = -SM_INFINITE_PARAMETER`, past the axis: X = (-1, 0, 0), and its inversion
/// refuses these points, on the nappe of the profile's origin (`SM_ERR`).
#[test]
fn unbounded_line_crossing_the_axis_inverts_on_its_origin_side() {
let (point, vector) = (Vec3::new(1.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 1.0));
let (k, s) = unbounded_line_revolution(true, point, vector);
let (k, s) = unbounded_line_revolution(point, vector);
assert_eq!(s.position().x_axis(), Vec3::new(1.0, 0.0, 0.0));
assert_inverts(&k, &s, Vec3::new(2.0, 0.0, 1.0), Point2::new(0.0, 1.0));
assert_inverts(&k, &s, Vec3::new(0.0, 2.0, 1.0), Point2::new(90.0, 1.0));
assert_inverts(&k, &s, Vec3::new(0.5, 0.0, -0.5), Point2::new(0.0, -0.5));
let (k, s) = unbounded_line_revolution(false, point, vector);
assert_eq!(s.position().x_axis(), Vec3::new(-1.0, 0.0, 0.0));
let r = s.step_inversion(
&k,
&s.step_uv_domain(),
Vec3::new(2.0, 0.0, 1.0),
1e-9,
None,
);
assert!(
matches!(&r, Err(e) if e.upstream_status() == Some(crate::error::SM_ERR)),
"{r:?}"
);
}
/// The mirror image: `(-1, 0, 0) + s (-1, 0, 1)` lies on the -X side of the axis
@@ -2511,25 +2484,23 @@ mod tests {
#[test]
fn unbounded_line_on_the_other_side_inverts() {
let (point, vector) = (Vec3::new(-1.0, 0.0, 0.0), Vec3::new(-1.0, 0.0, 1.0));
let (k, s) = unbounded_line_revolution(true, point, vector);
let (k, s) = unbounded_line_revolution(point, vector);
assert_eq!(s.position().x_axis(), Vec3::new(-1.0, 0.0, 0.0));
assert_inverts(&k, &s, Vec3::new(-2.0, 0.0, 1.0), Point2::new(0.0, 1.0));
assert_inverts(&k, &s, Vec3::new(0.0, -2.0, 1.0), Point2::new(90.0, 1.0));
}
/// A line parallel to the axis, `(2, 0, 0) + s (0, 0, 1)` (a cylinder of radius 2),
/// never crosses it: upstream's frame (from the line's point at 45% of its
/// interval, its start being within 1° of the axis direction) and the fix's agree,
/// X = (1, 0, 0), and `(2, 0, 1)` is `(0, 1)`, `(0, -2, 5)` is `(270, 5)`.
/// never crosses it: the frame from its origin (WR-738) and upstream's (from the
/// line's point at 45% of its interval, its start being within 1° of the axis
/// direction) agree, X = (1, 0, 0), and `(2, 0, 1)` is `(0, 1)`, `(0, -2, 5)` is
/// `(270, 5)`.
#[test]
fn unbounded_line_parallel_to_the_axis_inverts_either_way() {
for fix in [false, true] {
let (k, s) =
unbounded_line_revolution(fix, Vec3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
assert_eq!(s.position().x_axis(), Vec3::new(1.0, 0.0, 0.0), "fix {fix}");
assert_inverts(&k, &s, Vec3::new(2.0, 0.0, 1.0), Point2::new(0.0, 1.0));
assert_inverts(&k, &s, Vec3::new(0.0, -2.0, 5.0), Point2::new(270.0, 5.0));
}
let (k, s) = unbounded_line_revolution(Vec3::new(2.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0));
assert_eq!(s.position().x_axis(), Vec3::new(1.0, 0.0, 0.0));
assert_inverts(&k, &s, Vec3::new(2.0, 0.0, 1.0), Point2::new(0.0, 1.0));
assert_inverts(&k, &s, Vec3::new(0.0, -2.0, 5.0), Point2::new(270.0, 5.0));
}
/// tangency-labs/solidcore#2628 item 7: a profile that crosses the axis lies on
@@ -2538,12 +2509,11 @@ mod tests {
/// both. The profile `λ(s) = (1, 0, 0) + s (1, 0, 1)` crosses at `s = -1`;
/// `λ(-3) = (-2, 0, -3)` lies on the profile itself, `(u, v) = (0, -3)` (`u = 0` is
/// the profile, ISO 10303-42), turned by 90° about Z it is `(0, -2, -3) = (90, -3)`,
/// and `λ(1) = (2, 0, 1) = (0, 1)` lies on the other side of the axis. With
/// [`UpstreamFixes::revolution_frame_from_profile_origin`] (WR-738) all of them
/// invert, whichever way the frame faces: the frame from the profile's origin
/// (X = (1, 0, 0)) and upstream's from its start (X = (-1, 0, 0), built here with
/// the fix on). Without the fix upstream's frame inverts the side it faces,
/// `(-2, 0, -3)` and `(0, -2, -3)`, and refuses `(2, 0, 1)` (`SM_ERR`).
/// and `λ(1) = (2, 0, 1) = (0, 1)` lies on the other side of the axis. All of them
/// invert (WR-738), whichever way the frame faces: the frame from the profile's
/// origin (X = (1, 0, 0)) and upstream's from its start (X = (-1, 0, 0)). Upstream
/// turns a point onto its frame's +X half plane only, so its frame inverts the side
/// it faces, `(-2, 0, -3)` and `(0, -2, -3)`, and refuses `(2, 0, 1)` (`SM_ERR`).
#[test]
fn profile_crossing_the_axis_inverts_on_both_sides() {
let (point, vector) = (Vec3::new(1.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 1.0));
@@ -2554,7 +2524,7 @@ mod tests {
(Vec3::new(0.0, 2.0, 1.0), Point2::new(90.0, 1.0)),
];
let (k, origin_frame) = unbounded_line_revolution(true, point, vector);
let (k, origin_frame) = unbounded_line_revolution(point, vector);
assert_eq!(origin_frame.position().x_axis(), Vec3::X);
let line = Curve::from(Line::create_canonical(point, vector));
let domain = Extent2d::from_bounds(0.0, -INFINITE_PARAMETER, 360.0, INFINITE_PARAMETER);
@@ -2566,105 +2536,63 @@ mod tests {
assert_inverts(&k, s, p, uv);
}
}
let (k, upstream) = unbounded_line_revolution(false, point, vector);
assert_eq!(upstream.position().x_axis(), -Vec3::X);
for (p, uv) in points.iter().take(2) {
assert_inverts(&k, &upstream, *p, *uv);
}
let r = upstream.step_inversion(
&k,
&upstream.step_uv_domain(),
Vec3::new(2.0, 0.0, 1.0),
1e-9,
None,
);
assert!(
matches!(&r, Err(e) if e.upstream_status() == Some(crate::error::SM_ERR)),
"{r:?}"
);
}
/// tangency-labs/solidcore#2628 item 8: the segment `λ(s) = s (1, 0, 1)` on
/// `[-9, 11]` has its point at parameter 0 and the one at 45 % of its interval
/// (`-9 + 0.45 * 20 = 0`) both at the origin, on the axis. With the fix the frame
/// is taken from the interval's start before the 45 % point:
/// `λ(-9) = (-9, 0, -9)`, X = (-1, 0, 0), and `λ(-5) = (-5, 0, -5)` is `(0, -5)`
/// (with the two-sided inversion `λ(5) = (5, 0, 5)`, across the axis, is
/// `(0, 5)`). Upstream's frame is the start's already; with the fix,
/// `create_canonical` fails today (`SM_ERR`: no Y axis from the 45 % point).
/// (`-9 + 0.45 * 20 = 0`) both at the origin, on the axis. The frame is taken from
/// the interval's start before the 45 % point: `λ(-9) = (-9, 0, -9)`,
/// X = (-1, 0, 0), as upstream's, and `λ(-5) = (-5, 0, -5)` is `(0, -5)`; with the
/// two-sided inversion `λ(5) = (5, 0, 5)`, across the axis, is `(0, 5)`.
#[test]
fn frame_from_the_interval_start_when_the_origin_is_on_the_axis() {
for fix in [false, true] {
let mut k = k();
k.fixes.revolution_frame_from_profile_origin = fix;
let line = Line::bounded(
Vec3::ZERO,
Vec3::new(1.0, 0.0, 1.0),
&Extent1d::from_min_max(-9.0, 11.0),
&BoundedLineOptions {
scale: 2.0_f64.sqrt(),
..BoundedLineOptions::default()
},
);
let s = SurfOfRevolution::create_canonical(
&k,
Curve::from(line),
Vec3::ZERO,
Vec3::Z,
false,
)
.unwrap_or_else(|e| panic!("fix {fix}: {e:?}"));
assert_eq!(s.position().x_axis(), -Vec3::X, "fix {fix}");
assert_inverts(&k, &s, Vec3::new(-5.0, 0.0, -5.0), Point2::new(0.0, -5.0));
if fix {
// the other side of the axis (item 7)
assert_inverts(&k, &s, Vec3::new(5.0, 0.0, 5.0), Point2::new(0.0, 5.0));
}
}
let k = k();
let line = Line::bounded(
Vec3::ZERO,
Vec3::new(1.0, 0.0, 1.0),
&Extent1d::from_min_max(-9.0, 11.0),
&BoundedLineOptions {
scale: 2.0_f64.sqrt(),
..BoundedLineOptions::default()
},
);
let s =
SurfOfRevolution::create_canonical(&k, Curve::from(line), Vec3::ZERO, Vec3::Z, false)
.unwrap();
assert_eq!(s.position().x_axis(), -Vec3::X);
assert_inverts(&k, &s, Vec3::new(-5.0, 0.0, -5.0), Point2::new(0.0, -5.0));
// the other side of the axis (item 7)
assert_inverts(&k, &s, Vec3::new(5.0, 0.0, 5.0), Point2::new(0.0, 5.0));
}
/// tangency-labs/solidcore#2628 item 9: whether the frame's reference point is on
/// the axis is decided by its distance from the axis. The profile's origin
/// `λ(0) = (1, 0, 1000)` of `(1, 0, 1000) + s (1, 0, 1)` is one unit from the axis,
/// but its direction from the axis point is within 1° of the axis (0.057°), the
/// test upstream applies. With the fix it is not on the axis, and the frame faces
/// it: X = (1, 0, 0), and `λ(1) = (2, 0, 1001)` is `(0, 1)`. Upstream's test takes
/// the 45 % point instead, past the crossing: X = (-1, 0, 0).
/// test upstream applies. It is not on the axis, and the frame faces it:
/// X = (1, 0, 0), and `λ(1) = (2, 0, 1001)` is `(0, 1)`. Upstream's test takes the
/// 45 % point instead, past the crossing: X = (-1, 0, 0).
#[test]
fn frame_point_on_the_axis_by_its_distance() {
let (point, vector) = (Vec3::new(1.0, 0.0, 1000.0), Vec3::new(1.0, 0.0, 1.0));
let (k, s) = unbounded_line_revolution(true, point, vector);
let (k, s) = unbounded_line_revolution(point, vector);
assert_eq!(s.position().x_axis(), Vec3::X);
assert_inverts(&k, &s, Vec3::new(2.0, 0.0, 1001.0), Point2::new(0.0, 1.0));
let (_, upstream) = unbounded_line_revolution(false, point, vector);
assert_eq!(upstream.position().x_axis(), -Vec3::X);
}
/// A bounded profile whose interval starts at its parameter origin keeps
/// upstream's frame bit for bit with the fix on: the cubic of `TestSmGeneral.cpp`
/// upstream's frame, from its start `(2, 0, 0)`: the cubic of `TestSmGeneral.cpp`
/// (interval [0, 1]) and the cone's segment.
#[test]
fn bounded_profile_frame_is_upstreams_with_the_fix() {
let mut fixed = k();
fixed.fixes.revolution_frame_from_profile_origin = true;
fn bounded_profile_frame_is_upstreams() {
let a =
SurfOfRevolution::create_canonical(&k(), bezier(), Vec3::ZERO, Vec3::Z, false).unwrap();
let b = SurfOfRevolution::create_canonical(&fixed, bezier(), Vec3::ZERO, Vec3::Z, false)
.unwrap();
assert_eq!(a.position(), b.position());
let mut l = Line::from_points(Vec3::new(2.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 3.0), 3);
l.make_nurb().unwrap();
let c = SurfOfRevolution::create_canonical(
&fixed,
Curve::from(l),
Vec3::ZERO,
Vec3::new(0.0, 0.0, 3.0),
false,
)
.unwrap();
assert_eq!(cone().position(), c.position());
for s in [&a, &cone()] {
assert_eq!(s.position().origin(), Vec3::ZERO);
assert_eq!(s.position().x_axis(), Vec3::X);
assert_eq!(s.position().y_axis(), Vec3::Y);
assert_eq!(s.position().z_axis(), Vec3::Z);
}
}
#[test]
@@ -30,11 +30,10 @@
//! conversions and intersections they run.
//! - **`STEPInversion`** returns the parameter and location only on success: upstream
//! writes them before returning `SM_ERR` for a point outside the sweep interval or
//! not on the generator, and the port returns that error alone. With
//! `Kernel::fixes.revolution_frame_from_profile_origin` (WR-738) a planar
//! not on the generator, and the port returns that error alone. A planar
//! generator's point is also turned onto the start plane's -X half plane when it
//! does not lie on the generator on the +X one, so a profile that crosses the axis
//! inverts on both sides; upstream turns it onto the +X half plane only.
//! inverts on both sides (WR-738); upstream turns it onto the +X half plane only.
//! - **Generator.** As in `p1` the generator is a [`Curve`] derived from
//! `SmBSplineCurve`; `CreateOffsetSurface`'s composite-curve branch for any other
//! generator is unreachable. Upstream's `STEPInversion` switches the generator's
@@ -350,12 +349,11 @@ impl SurfOfRevolution {
/// the sweep angle (the larger of two on a seam when it is below the end of the
/// sweep) gives the U parameter, the point's distance from the axis a pole, and
/// the swung point is intersected with the generator for the V parameter. On a
/// seam, `uv_guess` picks the end nearer to it. With
/// [`UpstreamFixes::revolution_frame_from_profile_origin`](crate::kernel::UpstreamFixes::revolution_frame_from_profile_origin)
/// (WR-738) the point is swung onto both half planes of the generator's plane,
/// and the first on which it lies on the generator (+X, then -X at the angle 180
/// degrees on) gives both parameters, so a generator that crosses the axis inverts
/// on both sides of it.
/// seam, `uv_guess` picks the end nearer to it. The point is swung onto both half
/// planes of the generator's plane, and the first on which it lies on the
/// generator (+X, then -X at the angle 180 degrees on) gives both parameters, so a
/// generator that crosses the axis inverts on both sides of it (WR-738; upstream
/// swings it onto the +X half plane only).
///
/// # Errors
///
@@ -493,7 +491,7 @@ impl SurfOfRevolution {
// (with WR-738 the first half plane of the start plane on which it lies on the
// generator, see `start_plane_sides`)
let mut on_generator = None;
for sp in self.start_plane_sides(k, point_on_surf, tol_3d)? {
for sp in self.start_plane_sides(point_on_surf, tol_3d)? {
let solve =
self.solve_on_generator(k, self.is_out_of_bounds_enabled(), sp.point, tol_3d)?;
if !solve.2.is_empty() {
@@ -633,8 +631,8 @@ impl SurfOfRevolution {
/// the interval's period, the lower angle on a seam), and its generator parameter
/// is the generator's closed-form inversion ([`Curve::step_inversion`]: lines,
/// conics, parabolas, hyperbolas, without a domain) or, for any other generator,
/// its solve over the generator's maximum analytic domain; with WR-738's flag on
/// both half planes, as [`Self::step_inversion`]. A non-planar generator runs
/// its solve over the generator's maximum analytic domain; on both half planes
/// (WR-738), as [`Self::step_inversion`]. A non-planar generator runs
/// [`Self::step_inversion`] over [`Self::max_analytic_domain`].
///
/// # Errors
@@ -653,7 +651,7 @@ impl SurfOfRevolution {
return self.step_inversion(k, &domain, point_on_surf, tol_3d, None);
}
let mut on_generator = None;
for sp in self.start_plane_sides(k, point_on_surf, tol_3d)? {
for sp in self.start_plane_sides(point_on_surf, tol_3d)? {
if let Some(v) = self.generator_param_unbounded(k, sp.point, tol_3d)? {
on_generator = Some((sp, v));
break;
@@ -713,9 +711,8 @@ impl SurfOfRevolution {
/// The point turned about the axis onto the start (X/Z) plane, as
/// [`Self::transform_point_to_start_plane`] turns it onto the +X half plane, with
/// its angles in the sweep interval. With
/// [`UpstreamFixes::revolution_frame_from_profile_origin`](crate::kernel::UpstreamFixes::revolution_frame_from_profile_origin)
/// (WR-738) also onto the -X half plane, at the angles 180 degrees on: the point's
/// its angles in the sweep interval, and also onto the -X half plane (WR-738), at
/// the angles 180 degrees on: the point's
/// circle about the axis meets the generator's plane in these two points, and a
/// generator that crosses the axis reaches either (the surface's points on its
/// other side are `Rz(u) λ(v)` with `λ(v)` on the -X half plane). The +X one comes
@@ -726,16 +723,8 @@ impl SurfOfRevolution {
/// # Errors
///
/// Those of [`Self::transform_point_to_start_plane`].
fn start_plane_sides(
&self,
k: &Kernel,
point: Point3,
tol_3d: f64,
) -> Result<Vec<StartPlanePoint>> {
fn start_plane_sides(&self, point: Point3, tol_3d: f64) -> Result<Vec<StartPlanePoint>> {
let near = self.transform_point_to_start_plane(point, tol_3d, false)?;
if !k.fixes.revolution_frame_from_profile_origin {
return Ok(vec![near]);
}
// `p` turned by 180 degrees about the axis: mirrored through its foot on it
let origin = self.position.origin();
let z_axis = self.position.z_axis();
@@ -848,26 +837,19 @@ impl SurfOfRevolution {
let bbox = boxes.and_then(|b| b.normal).unwrap_or_default();
let height = bbox.max_dimension() * 1.5; // multiply max dimension by some factor to be sure
let origin = self.position.origin();
// Not upstream (WR-343, tangency-labs/solidcore#973), opt-in
// `Kernel::fixes.revolution_offset_axis_both_sides`: the axis segment spans the offset
// curve's whole extent along the axis, on both sides of the frame's origin (it ran
// from the origin along +z only and missed an offset crossing the axis below it),
// and the part kept is the one on the generatrix's side (positive radius).
let both_sides = k.fixes.revolution_offset_axis_both_sides;
let (axis_start, axis_end) = if both_sides {
let (mut axial_min, mut axial_max) = (BIG_DOUBLE, -BIG_DOUBLE);
for corner in bbox.corners() {
let axial = (corner - origin).dot(z_axis);
axial_min = axial_min.min(axial);
axial_max = axial_max.max(axial);
}
(
origin + (axial_min - height) * z_axis,
origin + (axial_max + height) * z_axis,
)
} else {
(origin, origin + height * z_axis)
};
// Not upstream (WR-343, tangency-labs/solidcore#973): the axis segment spans the
// offset curve's whole extent along the axis, on both sides of the frame's origin
// (upstream's runs from the origin along +z only and misses an offset crossing the
// axis below it), and the part kept is the one on the generatrix's side (positive
// radius).
let (mut axial_min, mut axial_max) = (BIG_DOUBLE, -BIG_DOUBLE);
for corner in bbox.corners() {
let axial = (corner - origin).dot(z_axis);
axial_min = axial_min.min(axial);
axial_max = axial_max.max(axial);
}
let axis_start = origin + (axial_min - height) * z_axis;
let axis_end = origin + (axial_max + height) * z_axis;
let axis_line = Curve::from(Line::create_line_segment(3, axis_start, axis_end, None)?);
let axis_ivl = axis_line.natural_interval();
let opts = CurveIntersectOptions {
@@ -889,24 +871,16 @@ impl SurfOfRevolution {
.as_slice()
.first()
.map_or(Ok(0.0), |s| s.start.param(0))?;
let trim = if both_sides {
domain.min() != param && domain.max() != param
} else {
domain.min() != param || domain.max() != param
};
if trim {
let keep_high = if both_sides {
// (WR-343) keep the part on the generatrix's side of the axis
// (positive radius along the frame's x axis), not the longer part
let low_mid = off_curve.evaluate_point(k, 0.5 * (domain.min() + param))?;
let high_mid = off_curve.evaluate_point(k, 0.5 * (param + domain.max()))?;
let x_axis = self.position.x_axis();
(high_mid - origin).dot(x_axis) > (low_mid - origin).dot(x_axis)
} else {
// Take the largest portion for now
// Eventually use a better algorithm
param < domain.mid()
};
// (WR-343: at neither end; upstream's `||` trims unless the interval is a
// point)
if domain.min() != param && domain.max() != param {
// (WR-343) keep the part on the generatrix's side of the axis (positive
// radius along the frame's x axis); upstream keeps the longer part
let low_mid = off_curve.evaluate_point(k, 0.5 * (domain.min() + param))?;
let high_mid = off_curve.evaluate_point(k, 0.5 * (param + domain.max()))?;
let x_axis = self.position.x_axis();
let keep_high =
(high_mid - origin).dot(x_axis) > (low_mid - origin).dot(x_axis);
if keep_high {
let _ = domain.set_min(param);
} else {
@@ -1305,20 +1279,20 @@ mod tests {
use crate::nurbs::crv_basic::Curve as NurbCurve;
use crate::nurbs::math::{CPoint, NOW};
/// The generator's radius (along the frame's x axis) at both ends of the offset
/// surface's generator.
fn end_radii(k: &Kernel, offset: &SurfOfRevolution) -> [f64; 2] {
/// The end points of the offset surface's generator.
fn end_points(k: &Kernel, offset: &SurfOfRevolution) -> [Point3; 2] {
let gen = offset.gen().unwrap();
let ivl = gen.natural_interval();
[ivl.min(), ivl.max()].map(|t| gen.evaluate_point(k, t).unwrap().x)
[ivl.min(), ivl.max()].map(|t| gen.evaluate_point(k, t).unwrap())
}
/// tangency-labs/solidcore#973: a surface of revolution about +z through the origin
/// whose generator (in the x-z plane, from the axis at z = -1 out to (4, 0, -5)) lies
/// below the frame's origin. Offset to one side, the generator crosses the axis below
/// the origin too: the axis segment, which ran from the origin along +z only, missed
/// the crossing, and the offset generator was left reaching past the axis (a negative
/// radius). With the opt-in it is cut at the axis.
/// the origin too: upstream's axis segment, which runs from the origin along +z only,
/// misses the crossing, and the offset generator is left reaching past the axis (a
/// negative radius). The axis segment spans the offset on both sides of the origin, so
/// it is cut at the axis (WR-343).
#[test]
fn issue_973_offset_cut_at_axis_below_origin() {
let gen = Curve::from(BSplineCurve::from_nurb(
@@ -1346,18 +1320,27 @@ mod tests {
)
.unwrap();
surface.make_nurb(&k).unwrap();
let min_radius = |both_sides: bool| -> f64 {
let mut k = Kernel::default();
k.fixes.revolution_offset_axis_both_sides = both_sides;
[1.0, -1.0]
.into_iter()
.flat_map(|d| end_radii(&k, &surface.create_offset_surface(&k, d, 1e-6).unwrap()))
.fold(f64::INFINITY, f64::min)
};
// upstream: one of the two offsets keeps a stretch past the axis
assert!(min_radius(false) < -0.1);
// the opt-in cuts it at the axis
assert!(min_radius(true) > -1e-6);
// the generator (0, 0, -1)-(4, 0, -5) moved by 1 along (1, 0, 1)/sqrt(2) either
// way: the one moved towards -x crosses the axis at z = -1 - sqrt(2) and is cut
// there, its end on the axis (upstream keeps it reaching to x = -1/sqrt(2))
let h = std::f64::consts::FRAC_1_SQRT_2;
let ends: Vec<Point3> = [1.0, -1.0]
.into_iter()
.flat_map(|d| end_points(&k, &surface.create_offset_surface(&k, d, 1e-6).unwrap()))
.collect();
let want = [
Vec3::new(h, 0.0, -1.0 + h),
Vec3::new(4.0 + h, 0.0, -5.0 + h),
Vec3::new(0.0, 0.0, -1.0 - 2.0 * h),
Vec3::new(4.0 - h, 0.0, -5.0 - h),
];
assert_eq!(ends.len(), 4);
for w in want {
assert!(
ends.iter().any(|e| e.distance(w) <= 1e-6),
"{w:?} is no end of {ends:?}"
);
}
}
/// #13790: a typed `SM_ERR` that replaced the inversion's off-generator `Upstream` is
+99 -112
View File
@@ -35,17 +35,14 @@
//! upstream caller does, uses [`Surface::approximate_surface_ignoring_status`].
//! - **Patch index check.** When a starter surface's knots are not all of full
//! multiplicity, a patch's control points lie outside the net; upstream signals
//! `SM_ERR` but returns `SM_SUCCESS` without surface, as the port does (`Ok` with
//! `surface: None`). With `Kernel::fixes.approximate_surface_bezier_starter`
//! (WR-573, the fork's tangency-labs/smlib@eae96cb) the starter's inner knots are
//! raised to full multiplicity before each round of patches, and an overrun is
//! `SM_ERR`.
//! `SM_ERR` but returns `SM_SUCCESS` without surface. Here the starter's inner knots
//! are raised to full multiplicity before each round of patches (WR-573, the fork's
//! tangency-labs/smlib@eae96cb), and an overrun is `SM_ERR`.
//! - **Accepted pass.** Upstream accepts a pass of patches when its last row (or
//! column) is within the tolerance (`bFailed` starts over with every row), and
//! returns success with an achieved tolerance above the requested one; the port
//! does the same. With `Kernel::fixes.approximate_surface_checks_every_row`
//! (WR-726, #2507) a pass with any row outside the tolerance is subdivided further,
//! and the search fails (`SM_ERR`) when the last subdivision is still outside it.
//! returns success with an achieved tolerance above the requested one. Here a pass
//! with any row outside the tolerance is subdivided further, and the search fails
//! (`SM_ERR`) when the last subdivision is still outside it (WR-726, #2507).
//! - **Root surface.** `GetRootSurface` of an offset, curve bounded surface or surface
//! in a volume follows its base; the chain is walked in a loop bounded by
//! [`MAX_GEOMETRY_NESTING`] (`InvalidInput` beyond). The root of a plane, cone,
@@ -116,8 +113,10 @@ impl Default for CubicBezierSurface {
#[derive(Debug, Clone)]
pub struct ApproximatedSurface {
/// `rpNewBSplineSurface`: the approximating surface with the fewest subdivisions
/// within the tolerance; `None` when the finest allowed one is still out of it
/// (and in the cases the module deviations list).
/// within the tolerance; `None` after a failure, which only
/// [`Surface::approximate_surface_ignoring_status`] returns as a result (the
/// finest allowed subdivision still out of the tolerance, among others), and when
/// the copy of `just_copy_b_splines` fails with `query_statuses_pass_on` off.
pub surface: Option<Surface>,
/// `dAchievedTol`: the largest sampled distance of the last test (`0` for a copied
/// B-spline, `-1` when nothing was tested).
@@ -139,7 +138,10 @@ impl Surface {
/// # Errors
///
/// `SM_ERR` when no subdivision level is within the tolerance, or the starter
/// surface's degree is above 3; those of the evaluations, of the starter surface's
/// surface's degree is above 3 (a Bezier patch past the starter's control net is
/// `SM_ERR` too, a guard: the clamped starter with full inner knots has three
/// control points per span and one more); those of the evaluations, of the
/// starter surface's
/// construction and of knot insertion; `InvalidInput` for a root surface chain
/// deeper than [`MAX_GEOMETRY_NESTING`].
pub fn approximate_surface(
@@ -321,29 +323,26 @@ impl Surface {
// Update pApproxSurf using cubic Bezier approximations to each patch.
// `Kernel::fixes.approximate_surface_bezier_starter` (WR-573): the patches
// below are the cubic Bezier pieces between knots, which needs every inner
// knot of full multiplicity (control points 3 apart per span). A starter
// copied from a root B-spline with simple inner knots is put in that form
// first, exactly, by knot insertion.
if k.fixes.approximate_surface_bezier_starter {
for (param, deg) in [(SurfParamType::U, u_deg), (SurfParamType::V, v_deg)] {
let (knots, mult) = approx_surf.knots(param, None)?;
more_knots.clear();
for (knot, &m) in knots
.iter()
.zip(&mult)
.take(knots.len().saturating_sub(1))
.skip(1)
{
for _ in m..deg {
more_knots.push(*knot);
}
}
if !more_knots.is_empty() {
approx_surf.insert_knots(param, &more_knots)?;
// WR-573: the patches below are the cubic Bezier pieces between knots, which
// needs every inner knot of full multiplicity (control points 3 apart per
// span). A starter copied from a root B-spline with simple inner knots is put
// in that form first, exactly, by knot insertion; upstream uses it as it is.
for (param, deg) in [(SurfParamType::U, u_deg), (SurfParamType::V, v_deg)] {
let (knots, mult) = approx_surf.knots(param, None)?;
more_knots.clear();
for (knot, &m) in knots
.iter()
.zip(&mult)
.take(knots.len().saturating_sub(1))
.skip(1)
{
for _ in m..deg {
more_knots.push(*knot);
}
}
if !more_knots.is_empty() {
approx_surf.insert_knots(param, &more_knots)?;
}
}
// get knots of the subdivided surface (statuses ignored: without NURB the
@@ -379,17 +378,12 @@ impl Surface {
for iub in 0..=3 {
for ivb in 0..=3 {
if u_min + iub >= num_ctrl_u || v_min + ivb >= num_ctrl_v {
// `Kernel::fixes.approximate_surface_bezier_starter`
// (WR-573): an error, not success with no surface
if k.fixes.approximate_surface_bezier_starter {
return Err(SolidError::surface_failed(
OP,
"a Bezier starter index is past the control net",
));
}
// SE(SM_ERR): some problem here with knots; upstream
// returns SM_SUCCESS without surface
return Ok(None);
// WR-573: an error, where upstream returns SM_SUCCESS
// without surface
return Err(SolidError::surface_failed(
OP,
"a Bezier starter index is past the control net",
));
}
// (the point read is overwritten; statuses ignored)
let _ = ignored_or_pass_on(
@@ -429,9 +423,7 @@ impl Surface {
// We'll test points at 1/4, 2/4, 3/4:
let num_between: usize = 3;
let fraction = (num_between + 1) as f64;
let mut failed = false;
// `Kernel::fixes.approximate_surface_checks_every_row` (WR-726): whether any
// row or column of this pass has failed
// WR-726: whether any row or column of this pass has failed
let mut pass_failed = false;
if let Some((knots, _)) =
@@ -461,7 +453,7 @@ impl Surface {
// for every surface patch
for outer_span in outer_knots.windows(2) {
failed = false; // Starting over on a new row.
let mut failed = false; // Starting over on a new row.
let &[o0, o1] = outer_span else {
continue;
};
@@ -530,15 +522,10 @@ impl Surface {
// ... continue on to the next 'outer' dir row/column of patches
} // end iter every 'outer' dir patch
// when the approx surface is good enough: upstream tests the flag of the
// last row or column only; `Kernel::fixes.approximate_surface_checks_every_row`
// (WR-726) tests every row and column of the pass
let rejected = if k.fixes.approximate_surface_checks_every_row {
pass_failed
} else {
failed
};
if !rejected {
// when the approx surface is good enough: every row and column of the pass
// is within the tolerance (WR-726; upstream tests the flag of the last row
// or column only)
if !pass_failed {
// Remove excess knots: the result does not have to be piecewise Bezier.
ignored_or_pass_on(
approx_surf.remove_knots(approx_tol_3d, true, true, None, None),
@@ -986,59 +973,14 @@ mod tests {
Surface::from(OffsetSurface::new(&k, 0.5, Surface::from(wave), true, 0).unwrap())
}
#[test]
fn wr573_default_gives_success_without_surface() {
// upstream: the starter's simple knots run its patches past its control points
let k = kernel();
let r = offset_wave()
.approximate_surface(&k, 1e-3, 3, false)
.unwrap();
assert!(r.surface.is_none());
}
#[test]
fn wr573_bezier_starter_approximates_the_offset_wave() {
let mut k = kernel();
k.fixes.approximate_surface_bezier_starter = true;
let off = offset_wave();
let tol = 1e-3;
let r = off.approximate_surface(&k, tol, 3, false).unwrap();
// the flag's path ran: the starter's patches no longer overrun its control
// points, so a pass was sampled and a surface returned (without the flag
// there is no surface and no sample)
let s = r.surface.unwrap();
// Upstream accepts a pass when its last row (or column) of patches is within
// `tol`: `bFailed` starts over with every row (SmSurface.cpp, the `outer`
// loop), so a pass can succeed with an earlier row outside `tol`. Its
// achieved tolerance is the largest deviation over the whole pass, above `tol`
// here. Pinned (the kernel is deterministic): any change in the fit shows.
let want = 0.001_620_828_043_730_338_3;
assert!(
(r.achieved_tol - want).abs() <= 1e-12 * want,
"{}",
r.achieved_tol
);
// the surface is that pass's, its excess knots removed within `tol`
let dom = off.natural_uv_domain_with_nurb(&k).unwrap();
for (fu, fv) in [(0.1, 0.2), (0.5, 0.5), (0.83, 0.9)] {
let uv = dom.evaluate(fu, fv);
let p = s.evaluate_point(&k, uv).unwrap();
let q = off.evaluate_point(&k, uv).unwrap();
assert!(
p.distance(q) <= r.achieved_tol + tol,
"{fu} {fv}: {}",
p.distance(q)
);
}
}
#[test]
fn wr726_every_row_checked_keeps_the_fit_within_the_tolerance() {
// #2507: the pass above is accepted at 1.62e-3 for 1e-3 because only its last
// row is within `tol`; with every row checked it is subdivided further
let mut k = kernel();
k.fixes.approximate_surface_bezier_starter = true;
k.fixes.approximate_surface_checks_every_row = true;
// WR-573: the starter's simple inner knots are raised to full multiplicity, so
// its patches stay on its control net and a surface comes back (upstream:
// success with no surface). #2507 (WR-726): upstream accepts the pass at
// 1.62e-3 for 1e-3 because only its last row is within `tol`; with every row
// checked it is subdivided further
let k = kernel();
let off = offset_wave();
let tol = 1e-3;
let r = off.approximate_surface(&k, tol, 3, false).unwrap();
@@ -1070,18 +1012,63 @@ mod tests {
// subdivision level 0 (one pass in each direction) leaves the fit outside
// `tol`: a failure the caller sees, with the achieved tolerance of the last
// pass, above `tol`
let mut k = kernel();
k.fixes.approximate_surface_bezier_starter = true;
k.fixes.approximate_surface_checks_every_row = true;
let k = kernel();
let tol = 1e-3;
let r = offset_wave()
.approximate_surface_ignoring_status(&k, tol, 0, false)
.unwrap();
assert!(r.surface.is_none());
assert!(r.failure.is_some());
assert_eq!(
r.failure,
Some(SolidError::surface_failed(
OP_APPROX,
"the approximation did not converge"
))
);
assert!(r.achieved_tol > tol, "{}", r.achieved_tol);
}
#[test]
fn a_starter_given_with_simple_end_knots_is_clamped_and_fits() {
// a cubic whose end knots are simple too (0 1 .. 7, 4 control points): its NURB
// is clamped to the valid span [3, 4], so with its inner knots raised to full
// multiplicity its net has three points per span and one more, and no Bezier
// patch runs past it (the overrun check is a guard)
let pts: Vec<Point3> = (0..4)
.flat_map(|i| {
let x = f64::from(i);
[
Point3::new(x, 0.0, 0.1 * x * x),
Point3::new(x, 1.0, 0.1 * x * x),
]
})
.collect();
let s = BSplineSurface::create_canonical(&CanonicalSurfaceData {
u_degree: 3,
v_degree: 1,
control_points: &pts,
form: BSplineSurfaceForm::Unspecified,
u_multiplicities: &[1; 8],
v_multiplicities: &[2, 2],
u_knots: &[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
v_knots: &[0.0, 1.0],
knot_type: KnotType::Unspecified,
weights: None,
uv_domain: None,
})
.unwrap();
let r = Surface::from(s)
.approximate_surface(&kernel(), 1e-3, 3, false)
.unwrap();
// the cubic is reproduced by its Hermite data
assert!(r.achieved_tol < 1e-12, "{}", r.achieved_tol);
let s = r.surface.unwrap();
let SurfaceKind::BSpline(b) = s.kind() else {
panic!("not a B-spline");
};
assert_eq!(b.number_control_points(SurfParamType::U), 4);
}
#[test]
fn degree_elevate_and_insert_keep_the_shape() {
let mut s = bilinear();
@@ -33,7 +33,9 @@
//! pointwise normal translation the move is, instead of turning the boundary by the
//! given plane's frame. This holds for bare surfaces only (neither carries a procedural
//! definition): a definition's net alone does not prove its plane, and moving the net
//! would leave the definition behind.
//! would leave the definition behind. (not upstream, WR-777) A given surface is turned by
//! its normal where it passes nearest the old face's middle, not at its own domain's
//! middle, which for a full cylinder given to a round lies across the axis.
use std::collections::{BTreeMap, BTreeSet};
@@ -53,6 +55,8 @@ use crate::offset::offset_executive::p2::OffsetNotifications;
use crate::query::brep::{calculate_bounding_box, BoundingBoxOptions};
use crate::surface::implicit::{self, AnalyticSupport};
use crate::surface::plane::Plane;
use crate::surface::surface::p4::NoObject;
use crate::surface::surface::p5::DropPointOptions;
use crate::surface::surface::Surface;
use crate::surface::surface::SurfaceKind;
use crate::topology::brep::p1 as bp1;
@@ -208,16 +212,33 @@ fn local_operation_on(
}
}
let domain = new.natural_uv_domain_if_built();
// (`EvaluateNormal(uv, TRUE, TRUE, ..)`: from the left)
let new_normal =
new.evaluate_normal(k, domain.evaluate(0.5, 0.5), Side::Left, Side::Left)?;
let old = own;
let old_normal = old.evaluate_normal(
let old_uv = b.face(face)?.uv_domain.evaluate(0.5, 0.5);
// (not upstream, which takes the middle of the new surface's domain) the new
// normal where the new surface passes nearest the old face's middle: a full
// cylinder's domain middle is across the axis from a round's middle, so the
// two normals there oppose though the surfaces face the same way. Where there
// is no nearest point, or no normal at it (a cone's apex), upstream's point is
// kept: the new rule only replaces it where it applies.
// (`EvaluateNormal(uv, TRUE, TRUE, ..)`: from the left)
let nearest = match new.drop_point::<NoObject>(
k,
b.face(face)?.uv_domain.evaluate(0.5, 0.5),
Side::Left,
Side::Left,
)?;
old.evaluate_point(k, old_uv)?,
&domain,
&DropPointOptions::default(),
) {
Ok(dropped) if dropped.success => {
Some(new.evaluate_normal(k, dropped.result_uv, Side::Left, Side::Left))
}
Ok(_) => None,
Err(e) => Some(Err(e)),
};
let new_normal = match nearest {
Some(Ok(n)) => n,
Some(Err(e)) if e.is_unported_or_cancelled() => return Err(e),
_ => new.evaluate_normal(k, domain.evaluate(0.5, 0.5), Side::Left, Side::Left)?,
};
let old_normal = old.evaluate_normal(k, old_uv, Side::Left, Side::Left)?;
// when surface normals oppose - reverse the input surface's orientation
if old_normal.dot(new_normal) < 0.0 {
new.reverse(k, SurfParamType::U)?;
@@ -1288,6 +1309,7 @@ mod tests {
use crate::geom::attribute::{AttributeStore, ObjectKey};
use crate::geom::object::ObjectRef;
use crate::kernel::UpstreamFixes;
use crate::topology::topology::Topology;
#[test]
@@ -2306,7 +2328,7 @@ mod tests {
/// surfaces had built before the call included (none dropped, none added), and the
/// caller's notification records and attributes. Refused while the offset's loop gaps
/// are closed — the straight bridge that would join a gap is not established on the
/// face's own surface within the operation's tolerance — a cube's round given a wider
/// face's own surface within the operation's tolerance — a cube's round given a narrower
/// coaxial cylinder, and a side moved off its round (the edge between them apart:
/// `LocalReach::apart`, adding nothing to the reach); `b` carries an attribute of its
/// own, lent to the offset, and the caller's store one of the caller's.
@@ -2319,7 +2341,7 @@ mod tests {
// the round is the face whose offset loop's gap the bridge would join
("round", |k| {
let (b, round) = box_rounded_r2(k);
let s = ap::create_cylinder_surface(k, Point3::new(8.0, 8.0, -1.0), 2.3, 12.0);
let s = ap::create_cylinder_surface(k, Point3::new(8.0, 8.0, -1.0), 1.8, 12.0);
(b, round, Surface::from(s.unwrap()), round)
}),
// the moved side is the face whose offset loop's gap the bridge would join
@@ -2675,26 +2697,25 @@ mod tests {
);
}
/// A 10 mm cube's vertical edge rounded r 2, the round given a coaxial cylinder
/// of another radius. For both wider and narrower replacements, the construction
/// cannot establish the gap's straight bridge on the round within tolerance.
/// It names the original round and refuses without changing the input.
/// A 10 mm cube's vertical edge rounded r 2, the round given the narrower coaxial
/// cylinder r 1.8. The sides x = 10 and y = 10 pass it 0.2 off, so their tangent edges
/// are apart, and the construction cannot establish the gap's straight bridge on the
/// round within tolerance. It names the original round and refuses without changing
/// the input. (A wider cylinder is met on the corner's side:
/// `tangent_edges_a_round_given_a_wider_coaxial_cylinder_meets_its_sides_on_the_corner_side`.)
#[test]
fn a_round_given_a_coaxial_cylinder_is_refused_at_its_loop_gap() {
fn a_round_given_a_narrower_coaxial_cylinder_is_refused_at_its_loop_gap() {
use crate::primitive::api_primitives as ap;
for radius in [2.3, 2.05, 1.8] {
let mut k = Kernel::default();
let (mut b, round) = box_rounded_r2(&mut k);
let s = Surface::from(
ap::create_cylinder_surface(&k, Point3::new(8.0, 8.0, -1.0), radius, 12.0).unwrap(),
);
let why = refused_unchanged_or_returned(&mut k, &mut b, &[round], vec![s]);
assert_eq!(
why,
Some(OffsetFailure::LoopGapUncertified { face: Some(round) }),
"{radius}"
);
}
let mut k = Kernel::default();
let (mut b, round) = box_rounded_r2(&mut k);
let s = Surface::from(
ap::create_cylinder_surface(&k, Point3::new(8.0, 8.0, -1.0), 1.8, 12.0).unwrap(),
);
let why = refused_unchanged_or_returned(&mut k, &mut b, &[round], vec![s]);
assert_eq!(
why,
Some(OffsetFailure::LoopGapUncertified { face: Some(round) })
);
}
/// The former flag-off refusal is no longer a default behaviour. With the same
@@ -2947,10 +2968,18 @@ mod tests {
0.5 * (u * (radius * radius - u * u).sqrt() + radius * radius * (u / radius).asin())
}
/// The cube's round r 5 given the coaxial cylinder r 5.1 (moved out 0.1).
fn round_moved_out(k: &mut Kernel) -> (Brep, Option<OffsetFailure>) {
/// The fix sets the tangent-edge cases run with: none (the kernel's default) and every
/// one (as an application turns them on, `offset_analytic_mate_edges` among them).
fn tangent_edge_fixes() -> [UpstreamFixes; 2] {
[UpstreamFixes::default(), UpstreamFixes::all()]
}
/// The cube's round r 5 given the coaxial cylinder r 5.1 (moved out 0.1), the operation
/// run with `fixes`.
fn round_moved_out(k: &mut Kernel, fixes: UpstreamFixes) -> (Brep, Option<OffsetFailure>) {
use crate::primitive::api_primitives as ap;
let mut b = cube_rounded_r5(k);
k.fixes = fixes;
let round = curved_face(&b);
let wider = Surface::from(
ap::create_cylinder_surface(k, Point3::new(15.0, 15.0, -1.0), 5.1, 22.0).unwrap(),
@@ -2959,83 +2988,130 @@ mod tests {
(b, why)
}
/// The cube's side x = 20, tangent to the round, moved into the body by 0.1.
fn side_moved_into_its_round(k: &mut Kernel) -> (Brep, Option<OffsetFailure>) {
/// The cube's side x = 20, tangent to the round, moved into the body by 0.1, the
/// operation run with `fixes`.
fn side_moved_into_its_round(
k: &mut Kernel,
fixes: UpstreamFixes,
) -> (Brep, Option<OffsetFailure>) {
let mut b = cube_rounded_r5(k);
k.fixes = fixes;
let side = face_on_x(k, &b, 20.0);
let moved = Surface::from(Plane::from_normal(Vec3::new(19.9, 0.0, 0.0), Vec3::X));
let why = refused_unchanged_or_returned(k, &mut b, &[side], vec![moved]);
(b, why)
}
/// The round moved out. The side planes, tangent to the round, are
/// extended to meet it 1.005 past their old edges (`reach`'s tests), but the tangent
/// edge across from the moved face is extended and trimmed, not met again: it keeps
/// its old line, so the straight bridge that would join the round's offset loop's gap
/// is not established on the round's own surface within the operation's tolerance.
/// Refused before any body is checked (issue draft "a moved face next to a tangent
/// edge (a round) is refused as unsound"), the cube as it came in.
#[test]
fn a_round_moved_out_is_refused_before_its_gap_bridge_is_established() {
let mut k = Kernel::default();
let (b, why) = round_moved_out(&mut k);
// the refusal left the input as it came in, so its round is still its own
let round = curved_face(&b);
assert_eq!(
why,
Some(OffsetFailure::LoopGapUncertified { face: Some(round) })
);
/// Each of `wants` is a vertex of `b`, within the input's tolerance 1e-5 (the mated
/// edges are intersections of the extended surfaces, within the B-rep's tolerance; a
/// vertex's own tolerance grows with the fixes on, so it would let a stray vertex pass).
fn assert_vertices_at(b: &Brep, wants: &[Point3]) {
use crate::topology::vertex;
let points: Vec<Point3> = b
.vertices()
.ids()
.map(|v| vertex::point(b, v).unwrap())
.collect();
for want in wants {
assert!(
points.iter().any(|p| p.distance(*want) <= 1e-5),
"no vertex at {want:?} in {points:?}"
);
}
}
/// The closed form the round moved out should have, once the tangent edges are met
/// again (issue draft "a moved face next to a tangent edge (a round) is refused as
/// unsound"): the cube with its corner cut by the r 5.1 circle, `20 (375 + A)`, `A`
/// the part of the corner square [15, 20]² inside the circle.
/// The round moved out. The side planes, tangent to the round, cut the r 5.1 cylinder
/// `w = √(5.1² − 5²) ≈ 1.005` either side of their old tangent lines; the tangent edges
/// are met on the round's side, at (20, 15 + w) and (15 + w, 20), and the arcs end
/// there. The body is the cube with its corner cut by the r 5.1 circle, `20 (375 + A)`,
/// `A` the part of the corner square [15, 20]² inside the circle.
#[test]
#[ignore = "a moved face next to a tangent edge (a round) is refused as unsound \
(notes: issue-draft-tangent-edges.md)"]
fn tangent_edges_a_round_moved_out_has_its_closed_form_volume() {
let mut k = Kernel::default();
let (mut b, why) = round_moved_out(&mut k);
assert_eq!(why, None);
let r = 5.1_f64;
let w = (r * r - 25.0).sqrt();
let inside = 5.0 * w + quarter_disc_area(r, 5.0) - quarter_disc_area(r, w);
let want = 20.0 * (375.0 + inside);
let (v, manifold) = volume_and_manifold(&k, &mut b);
assert!(manifold && (v - want).abs() <= 1e-9 * want, "{v} vs {want}");
for (set, fixes) in tangent_edge_fixes().into_iter().enumerate() {
let mut k = Kernel::default();
let (mut b, why) = round_moved_out(&mut k, fixes);
assert_eq!(why, None, "fix set {set}");
let r = 5.1_f64;
let w = (r * r - 25.0).sqrt();
let p = Point3::new;
let ends = [0.0, 20.0].map(|z| [p(20.0, 15.0 + w, z), p(15.0 + w, 20.0, z)]);
assert_vertices_at(&b, ends.as_flattened());
let inside = 5.0 * w + quarter_disc_area(r, 5.0) - quarter_disc_area(r, w);
let want = 20.0 * (375.0 + inside);
let (v, manifold) = volume_and_manifold(&k, &mut b);
assert!(
manifold && (v - want).abs() <= 1e-6 * want,
"fix set {set}: {v} vs {want}"
);
}
}
/// The side moved into its round: it meets the round 1 from its old tangent edge
/// (`reach`'s tests), and the same tangent edges leave the offset loop's gap unbridged:
/// the straight bridge is not established on the side's own surface within the
/// operation's tolerance. Refused before any body is checked, the cube as it came in.
#[test]
fn a_side_moved_into_its_round_is_refused_before_its_gap_bridge_is_established() {
let mut k = Kernel::default();
let (b, why) = side_moved_into_its_round(&mut k);
// the moved side is the face whose offset loop's gap the bridge would join; the
// input is refused as it came in, so its side is still its own
let side = face_on_x(&k, &b, 20.0);
assert_eq!(
why,
Some(OffsetFailure::LoopGapUncertified { face: Some(side) })
);
}
/// The closed form the side moved into its round should have (issue draft "a moved
/// face next to a tangent edge (a round) is refused as unsound"):
/// The side moved into its round: the plane x = 19.9 cuts the round r 5 at
/// `y = 15 ± √0.99`; the tangent edge is met on the round's own face, at
/// (19.9, 15 + √0.99), and the round's arcs end there. The body:
/// `20 (19.9 · 20 − 4.9 · 5 + ∫₀^4.9 √(25 − u²) du)`.
#[test]
#[ignore = "a moved face next to a tangent edge (a round) is refused as unsound \
(notes: issue-draft-tangent-edges.md)"]
fn tangent_edges_a_side_moved_into_its_round_has_its_closed_form_volume() {
let mut k = Kernel::default();
let (mut b, why) = side_moved_into_its_round(&mut k);
assert_eq!(why, None);
let want = 20.0 * (19.9 * 20.0 - 4.9 * 5.0 + quarter_disc_area(5.0, 4.9));
let (v, manifold) = volume_and_manifold(&k, &mut b);
assert!(manifold && (v - want).abs() <= 1e-9 * want, "{v} vs {want}");
for (set, fixes) in tangent_edge_fixes().into_iter().enumerate() {
let mut k = Kernel::default();
let (mut b, why) = side_moved_into_its_round(&mut k, fixes);
assert_eq!(why, None, "fix set {set}");
let y = 15.0 + 0.99_f64.sqrt();
assert_vertices_at(&b, &[0.0, 20.0].map(|z| Point3::new(19.9, y, z)));
let want = 20.0 * (19.9 * 20.0 - 4.9 * 5.0 + quarter_disc_area(5.0, 4.9));
let (v, manifold) = volume_and_manifold(&k, &mut b);
assert!(
manifold && (v - want).abs() <= 1e-6 * want,
"fix set {set}: {v} vs {want}"
);
}
}
/// The 10 mm cube's round r 2 about (8, 8) given a wider coaxial cylinder `R`. The sides
/// x = 10 and y = 10 cut it at 8 ± w, w = √(R² − 4); only 8 + w is on the corner's side
/// (the corner square [8, 10]²). The tangent edges are met there, at (10, 8 + w) and
/// (8 + w, 10), and the arcs that end the new round at the top and bottom end at those
/// vertices: ten vertices in all. The body is the cube with its corner cut by the
/// circle R: `10 (96 + A)`, `A` the part of the corner square inside the circle. The
/// new round faces out as the old one did, its outer loop running the surface's way:
/// the cylinder's domain middle is across the axis, where its normal opposes the old
/// round's (with every fix on, the face's UV trims are checked and its volume read on
/// them, so a round turned inside out is refused).
#[test]
fn tangent_edges_a_round_given_a_wider_coaxial_cylinder_meets_its_sides_on_the_corner_side() {
use crate::primitive::api_primitives as ap;
for (radius, fixes) in [2.3_f64, 2.05]
.into_iter()
.flat_map(|r| tangent_edge_fixes().map(|f| (r, f)))
{
let mut k = Kernel::default();
let (mut b, round) = box_rounded_r2(&mut k);
k.fixes = fixes;
let s = Surface::from(
ap::create_cylinder_surface(&k, Point3::new(8.0, 8.0, -1.0), radius, 12.0).unwrap(),
);
let why = refused_unchanged_or_returned(&mut k, &mut b, &[round], vec![s]);
assert_eq!(why, None, "{radius}");
let w = (radius * radius - 4.0).sqrt();
let p = Point3::new;
let ends = [0.0, 10.0].map(|z| [p(10.0, 8.0 + w, z), p(8.0 + w, 10.0, z)]);
assert_vertices_at(&b, ends.as_flattened());
assert_eq!(b.vertices().ids().count(), 10, "{radius}");
let inside = 2.0 * w + quarter_disc_area(radius, 2.0) - quarter_disc_area(radius, w);
let want = 10.0 * (96.0 + inside);
let (v, manifold) = volume_and_manifold(&k, &mut b);
assert!(
manifold && (v - want).abs() <= 1e-6 * want,
"{radius}: {v} vs {want}"
);
let round = curved_face(&b);
let outer = face_ops::loops(&b, round, false, None).unwrap()[0];
let along = crate::topology::face::p3::compute_loop_orientation(
&k, &mut b, round, outer, None, false,
);
let same = crate::nurbs::b_spline_surface_n_lib::p2::OrientType::Same;
assert_eq!(along.unwrap(), same, "{radius}");
}
}
/// The same side moved out by 0.1: the plane x = 20.1 passes the round, which bends
@@ -83,7 +83,6 @@ fn solidcraft_fixes() -> UpstreamFixes {
f.fillet_chamfer_cone_on_arc = true;
f.offset_excess_faces_after_merge = true;
f.offset_clip_right_side_out = true;
f.revolution_offset_axis_both_sides = true;
f.offset_extended_edges_mated = true;
f.keep_operand_uv_trims = true;
f.geometric_g2_continuity = true;
@@ -114,7 +113,6 @@ fn solidcraft_fixes() -> UpstreamFixes {
f.edge_reversal_keeps_failed_edge = true;
f.topology_failed_evaluations_propagate = true;
f.heal_stage3_failures_propagate = true;
f.sweep_frame_on_planar_path_normal = true;
f.cover_refuses_folded_continuation = true;
f.boolean_vertex_by_best_crossing = true;
f.boolean_cleanup_removes_slits = true;
@@ -123,7 +121,6 @@ fn solidcraft_fixes() -> UpstreamFixes {
f.degenerate_proto_edge_in_one_vertex = true;
f.new_edge_vertex_tolerance_covers_gap = true;
f.torus_signed_gen_curve_interval = true;
f.sheet_sweep_joins_profile_wires = true;
f.point_solve_keeps_triangle_guess = true;
f.imprint_trims_edge_to_proto_vertex = true;
f.imprint_extends_edge_to_proto_vertex = true;
@@ -310,7 +310,6 @@ fn solidcraft_fixes() -> UpstreamFixes {
f.fillet_chamfer_cone_on_arc = true;
f.offset_excess_faces_after_merge = true;
f.offset_clip_right_side_out = true;
f.revolution_offset_axis_both_sides = true;
f.offset_extended_edges_mated = true;
f.keep_operand_uv_trims = true;
f.geometric_g2_continuity = true;
@@ -341,7 +340,6 @@ fn solidcraft_fixes() -> UpstreamFixes {
f.edge_reversal_keeps_failed_edge = true;
f.topology_failed_evaluations_propagate = true;
f.heal_stage3_failures_propagate = true;
f.sweep_frame_on_planar_path_normal = true;
f.cover_refuses_folded_continuation = true;
f.boolean_vertex_by_best_crossing = true;
f.boolean_cleanup_removes_slits = true;
@@ -350,7 +348,6 @@ fn solidcraft_fixes() -> UpstreamFixes {
f.degenerate_proto_edge_in_one_vertex = true;
f.new_edge_vertex_tolerance_covers_gap = true;
f.torus_signed_gen_curve_interval = true;
f.sheet_sweep_joins_profile_wires = true;
f.point_solve_keeps_triangle_guess = true;
f.imprint_trims_edge_to_proto_vertex = true;
f.imprint_extends_edge_to_proto_vertex = true;
@@ -55,7 +55,6 @@ fn sc_fixes(k: &mut Kernel) {
k.fixes.fillet_split_edge_keeps_near_half = true;
k.fixes.offset_point_vertex_caps = true;
k.fixes.offset_clip_right_side_out = true;
k.fixes.revolution_offset_axis_both_sides = true;
k.fixes.offset_extended_edges_mated = true;
k.fixes.thicken_planar_side_walls = true;
k.fixes.keep_operand_uv_trims = true;
@@ -93,7 +92,6 @@ fn sc_fixes(k: &mut Kernel) {
k.fixes.local_operation_closes_walls_to_nothing = true;
k.fixes.offset_collapsed_rounds_mated = true;
k.fixes.offset_missing_surface_fails = true;
k.fixes.approximate_surface_bezier_starter = true;
k.fixes.revolution_round_offset_collapses = true;
k.fixes.fillet_nx1_end_rib_global_merge = true;
k.fixes.uv_trim_corner_end_points_only = true;
@@ -107,7 +105,6 @@ fn sc_fixes(k: &mut Kernel) {
k.fixes.iso_curve_sliver_domain_is_degenerate = true;
k.fixes.topology_failed_evaluations_propagate = true;
k.fixes.heal_stage3_failures_propagate = true;
k.fixes.sweep_frame_on_planar_path_normal = true;
k.fixes.cover_refuses_folded_continuation = true;
k.fixes.cover_wraps_across_closed_seam = true;
k.fixes.boolean2d_plane_covers_far_faces = true;
@@ -128,7 +125,6 @@ fn sc_fixes(k: &mut Kernel) {
k.fixes.found_torus_sphere_keep_tested_nurb = true;
k.fixes.internal_pole_row_through_knot = true;
k.fixes.extrusion_origin_scaled_zero = true;
k.fixes.sheet_sweep_joins_profile_wires = true;
k.fixes.point_solve_keeps_triangle_guess = true;
k.fixes.imprint_trims_edge_to_proto_vertex = true;
k.fixes.imprint_extends_edge_to_proto_vertex = true;
@@ -85,7 +85,6 @@ fn solidcraft_fixes() -> UpstreamFixes {
f.fillet_chamfer_cone_on_arc = true;
f.offset_excess_faces_after_merge = true;
f.offset_clip_right_side_out = true;
f.revolution_offset_axis_both_sides = true;
f.offset_extended_edges_mated = true;
f.keep_operand_uv_trims = true;
f.geometric_g2_continuity = true;
@@ -116,7 +115,6 @@ fn solidcraft_fixes() -> UpstreamFixes {
f.edge_reversal_keeps_failed_edge = true;
f.topology_failed_evaluations_propagate = true;
f.heal_stage3_failures_propagate = true;
f.sweep_frame_on_planar_path_normal = true;
f.cover_refuses_folded_continuation = true;
f.boolean_vertex_by_best_crossing = true;
f.boolean_cleanup_removes_slits = true;
@@ -125,7 +123,6 @@ fn solidcraft_fixes() -> UpstreamFixes {
f.degenerate_proto_edge_in_one_vertex = true;
f.new_edge_vertex_tolerance_covers_gap = true;
f.torus_signed_gen_curve_interval = true;
f.sheet_sweep_joins_profile_wires = true;
f.point_solve_keeps_triangle_guess = true;
f.imprint_trims_edge_to_proto_vertex = true;
f.imprint_extends_edge_to_proto_vertex = true;
@@ -211,7 +211,6 @@ fn flags_off_build_the_depths_above_the_floor() {
/// SolidCraft's `UpstreamFixes` preset (183 flags).
fn solidcraft_flags(k: &mut Kernel) {
k.fixes.analytic_copy_keeps_parameterization = true;
k.fixes.approximate_surface_bezier_starter = true;
k.fixes.boolean2d_plane_covers_far_faces = true;
k.fixes.boolean_cleanup_removes_slits = true;
k.fixes.boolean_lists_virtual_surface_copies = true;
@@ -319,13 +318,11 @@ fn solidcraft_flags(k: &mut Kernel) {
k.fixes.proto_edge_splits_at_captured_vertices = true;
k.fixes.proto_vertex_miss_is_false = true;
k.fixes.refine_cc_range_ends = true;
k.fixes.revolution_offset_axis_both_sides = true;
k.fixes.revolution_round_offset_collapses = true;
k.fixes.revolve_analytic_profile_faces = true;
k.fixes.revolve_holed_profile_void = true;
k.fixes.revolve_nested_shells_as_cavities = true;
k.fixes.revolve_splits_curves_on_axis = true;
k.fixes.sheet_sweep_joins_profile_wires = true;
k.fixes.shell_edges_on_analytics_exact = true;
k.fixes.shell_empty_cavity_fails = true;
k.fixes.simple_coincidence_crossing_is_point = true;
@@ -337,7 +334,6 @@ fn solidcraft_flags(k: &mut Kernel) {
k.fixes.ssi_tangent_curve_tracer = true;
k.fixes.ssi_trace_tangent_curves_once = true;
k.fixes.stitch_solid_nests_cavities = true;
k.fixes.sweep_frame_on_planar_path_normal = true;
k.fixes.taper_tangent_corner_planes = true;
k.fixes.thicken_planar_side_walls = true;
k.fixes.topology_failed_evaluations_propagate = true;
@@ -48,7 +48,6 @@ const SOLIDCRAFT_C494A18: &[&str] = &[
"analytic_copy_keeps_parameterization",
"analytic_corner_failure_is_false",
"analytic_inversion_answers_apart",
"approximate_surface_bezier_starter",
"ascii_doubles_round_trip",
"bend_volume_bbox_contains_wide_angle_and_inf_u",
"bend_volume_trim_param_box_across_period",
@@ -108,7 +107,6 @@ const SOLIDCRAFT_C494A18: &[&str] = &[
"ellipse_trim_seam_lines_span_cone_height",
"extent3d_contains_both_segment_endpoints",
"extrusion_origin_scaled_zero",
"extrusion_step_uses_clamped_uv",
"face_points_span_domain",
"face_tight_box_searches_trim",
"fillet_3x2_mixed_concave_side_edge",
@@ -214,13 +212,11 @@ const SOLIDCRAFT_C494A18: &[&str] = &[
"proto_vertex_miss_is_false",
"query_statuses_pass_on",
"refine_cc_range_ends",
"revolution_offset_axis_both_sides",
"revolution_round_offset_collapses",
"revolve_analytic_profile_faces",
"revolve_holed_profile_void",
"revolve_nested_shells_as_cavities",
"revolve_splits_curves_on_axis",
"sheet_sweep_joins_profile_wires",
"shell_edges_on_analytics_exact",
"shell_empty_cavity_fails",
"simple_coincidence_crossing_is_point",
@@ -239,7 +235,6 @@ const SOLIDCRAFT_C494A18: &[&str] = &[
"surface_continuity_checks_last_v_knot",
"surface_parallel_to_vector_checks",
"surface_point_solver_propagates_bbox_failure",
"sweep_frame_on_planar_path_normal",
"sweep_statuses_pass_on",
"taper_tangent_corner_planes",
"temporary_radius_setup_errors",
@@ -64,7 +64,6 @@ fn solidcraft_kernel() -> Kernel {
let names: Vec<String> = serde_json::from_str(include_str!("kernel_cases/fixes.json")).unwrap();
let mut f = UpstreamFixes::default();
for name in names.iter().map(String::as_str).chain([
"approximate_surface_checks_every_row",
"planar_section_face_tolerance",
"face_swap_uv_transposes_trims",
"closed_smooth_pole_tangent_limit",
@@ -32,7 +32,6 @@
"offset_point_vertex_caps",
"offset_clip_right_side_out",
"offset_edges_keep_source_tolerance",
"revolution_offset_axis_both_sides",
"offset_extended_edges_mated",
"offset_swap_uv_extended_distance",
"thicken_planar_side_walls",
@@ -98,7 +97,6 @@
"local_operation_closes_walls_to_nothing",
"offset_collapsed_rounds_mated",
"offset_missing_surface_fails",
"approximate_surface_bezier_starter",
"revolution_round_offset_collapses",
"fillet_nx1_end_rib_global_merge",
"composite_curve_bbox_segment_intervals",
@@ -127,7 +125,6 @@
"curve_property_gs_branch_bounds",
"topology_failed_evaluations_propagate",
"heal_stage3_failures_propagate",
"sweep_frame_on_planar_path_normal",
"database_io_byte_swaps_read_boolean_and_u16",
"cover_refuses_folded_continuation",
"cover_wraps_across_closed_seam",
@@ -150,9 +147,7 @@
"found_torus_sphere_keep_tested_nurb",
"internal_pole_row_through_knot",
"extrusion_origin_scaled_zero",
"extrusion_step_uses_clamped_uv",
"volume_representational_end_continuities",
"sheet_sweep_joins_profile_wires",
"point_solve_keeps_triangle_guess",
"analytic_corner_failure_is_false",
"sphere_from_arcs_common_centre",
+10 -76
View File
@@ -8,14 +8,15 @@
//! approximation tolerance, 3 subdivisions and no copy, the flat export (#1441) with 5
//! subdivisions and B-splines copied. The starter is the root B-spline of the swept surface,
//! with simple inner knots; its cubic Bezier patches run past its control points, and
//! upstream returns success with no surface. `Kernel::fixes.approximate_surface_bezier_starter`
//! (the fork's tangency-labs/smlib@eae96cb) raises the starter's inner knots to full
//! multiplicity first.
//! upstream returns success with no surface. solidcore raises the starter's inner knots to
//! full multiplicity first (WR-573, as the fork's tangency-labs/smlib@eae96cb), and checks
//! every row of patches of a pass against the tolerance (WR-726).
//!
//! Reference: the pinned C++ (v1.0.1) through `SmApiCreateLinearSweep`,
//! `SmApiOffsetBrepFull` and `ApproximateSurface` gives success with no surface for both
//! calls; with eae96cb's `SmSurface.cpp` linked in, both give a 33 x 4 surface with an
//! achieved tolerance of 1.1245747026548022e-05.
//! achieved tolerance of 1.1245747026548022e-05, above the requested 5e-6: its last row of
//! patches is within the tolerance, an earlier one is not (WR-726).
#![allow(
clippy::unwrap_used,
clippy::expect_used,
@@ -36,9 +37,6 @@ use solidcore::sweep::api_brep as sweeps;
use solidcore::topology::brep_data_write::bspline_surface_part;
use solidcore::{Brep, Kernel};
/// The achieved tolerance of the pinned C++ with eae96cb's fix, for both calls.
const CPP_FIXED_ACHIEVED_TOL: f64 = 1.124_574_702_654_802_2e-5;
/// (subdivisions, copy B-splines): `SmBrepData`'s writer, then the flat export.
const CALLS: [(usize, bool); 2] = [(3, false), (5, true)];
@@ -81,78 +79,13 @@ fn offset_wave(k: &mut Kernel) -> Brep {
moved
}
/// The defect WR-573 is about, pinned while the fix is opt-in: without the flag the
/// writer's call succeeds with nothing to write. This test goes away when the flag becomes
/// the default.
#[test]
fn without_the_flag_the_writer_succeeds_with_nothing_wr573() {
let mut k = Kernel::default();
assert!(!k.fixes.approximate_surface_bezier_starter);
let moved = offset_wave(&mut k);
let face = moved.face(moved.face_list()[0]).unwrap();
let surface = face.surface.as_ref().unwrap();
let tol = brep_approx_tol_3d(&k, &moved, SmallTopoType::NormalSize);
for (subdivisions, copy) in CALLS {
// as the pinned C++: success, no surface, nothing tested
let r = surface
.approximate_surface(&k, tol, subdivisions, copy)
.unwrap();
assert!(r.surface.is_none(), "({subdivisions}, {copy})");
assert_eq!(r.achieved_tol, -1.0, "({subdivisions}, {copy})");
}
}
#[test]
fn the_flag_approximates_the_offset_face() {
let mut k = Kernel::default();
k.fixes.approximate_surface_bezier_starter = true;
let moved = offset_wave(&mut k);
let face = moved.face(moved.face_list()[0]).unwrap();
let surface = face.surface.as_ref().unwrap();
let tol = brep_approx_tol_3d(&k, &moved, SmallTopoType::NormalSize);
for (subdivisions, copy) in CALLS {
let r = surface
.approximate_surface(&k, tol, subdivisions, copy)
.unwrap();
let nurbs = r.surface.expect("an approximating B-spline surface");
let SurfaceKind::BSpline(b) = nurbs.kind() else {
panic!("({subdivisions}, {copy}): not a B-spline");
};
assert_eq!(b.number_control_points(SurfParamType::U), 33);
assert_eq!(b.number_control_points(SurfParamType::V), 4);
// The C++ with the fix gives the same. It is above `tol` (5e-6): upstream accepts a
// pass when its last row of patches is within `tol`, and the achieved tolerance is
// the largest deviation over the whole pass (as the record says for its 1e-3 case).
assert!(
(r.achieved_tol - CPP_FIXED_ACHIEVED_TOL).abs() <= 1e-9 * CPP_FIXED_ACHIEVED_TOL,
"({subdivisions}, {copy}): {}",
r.achieved_tol
);
// over the face's domain the NURBS stays near the offset surface (upstream samples
// other points than this grid, hence the extra `tol`)
let bound = r.achieved_tol + tol;
let domain = face.uv_domain;
for i in 0..=20 {
for j in 0..=20 {
let uv = domain.evaluate(f64::from(i) / 20.0, f64::from(j) / 20.0);
let a = surface.evaluate_point(&k, uv).unwrap();
let p = nurbs.evaluate_point(&k, uv).unwrap();
let gap = a.distance(p);
assert!(gap <= bound, "({i}, {j}): gap {gap} > {bound}");
assert!((-bound..=10.0 + bound).contains(&p.y), "({i}, {j}): {p:?}");
}
}
}
}
/// #2507 (WR-726): a pass whose earlier rows of patches miss the tolerance is not accepted.
/// With `approximate_surface_checks_every_row` the pass the test above pins (33 x 4 at 2.25
/// times the tolerance) is subdivided once more, and the result is within the tolerance.
/// WR-573: the writer's calls give an approximating B-spline surface. #2507 (WR-726): a
/// pass whose earlier rows of patches miss the tolerance is not accepted, so the pass the
/// C++ with eae96cb accepts (33 x 4 at 2.25 times the tolerance) is subdivided once more,
/// and the result is within the tolerance.
#[test]
fn with_every_row_checked_the_fit_is_within_the_tolerance_2507() {
let mut k = Kernel::default();
k.fixes.approximate_surface_bezier_starter = true;
k.fixes.approximate_surface_checks_every_row = true;
let moved = offset_wave(&mut k);
let face = moved.face(moved.face_list()[0]).unwrap();
let surface = face.surface.as_ref().unwrap();
@@ -188,6 +121,7 @@ fn with_every_row_checked_the_fit_is_within_the_tolerance_2507() {
let p = nurbs.evaluate_point(&k, uv).unwrap();
let gap = a.distance(p);
assert!(gap <= bound, "({i}, {j}): gap {gap} > {bound}");
assert!((-bound..=10.0 + bound).contains(&p.y), "({i}, {j}): {p:?}");
}
}
}
@@ -156,7 +156,6 @@ fn top_edges(k: &Kernel, b: &Brep) -> Vec<EdgeId> {
fn round() -> (f64, Result<(bool, f64), i32>) {
let mut k = Kernel::default();
k.fixes.analytic_copy_keeps_parameterization = true;
k.fixes.approximate_surface_bezier_starter = true;
k.fixes.boolean2d_plane_covers_far_faces = true;
k.fixes.boolean_cleanup_removes_slits = true;
k.fixes.boolean_lists_virtual_surface_copies = true;
@@ -263,13 +262,11 @@ fn round() -> (f64, Result<(bool, f64), i32>) {
k.fixes.proto_edge_splits_at_captured_vertices = true;
k.fixes.proto_vertex_miss_is_false = true;
k.fixes.refine_cc_range_ends = true;
k.fixes.revolution_offset_axis_both_sides = true;
k.fixes.revolution_round_offset_collapses = true;
k.fixes.revolve_analytic_profile_faces = true;
k.fixes.revolve_holed_profile_void = true;
k.fixes.revolve_nested_shells_as_cavities = true;
k.fixes.revolve_splits_curves_on_axis = true;
k.fixes.sheet_sweep_joins_profile_wires = true;
k.fixes.shell_edges_on_analytics_exact = true;
k.fixes.shell_empty_cavity_fails = true;
k.fixes.simple_coincidence_crossing_is_point = true;
@@ -281,7 +278,6 @@ fn round() -> (f64, Result<(bool, f64), i32>) {
k.fixes.ssi_tangent_curve_tracer = true;
k.fixes.ssi_trace_tangent_curves_once = true;
k.fixes.stitch_solid_nests_cavities = true;
k.fixes.sweep_frame_on_planar_path_normal = true;
k.fixes.taper_tangent_corner_planes = true;
k.fixes.thicken_planar_side_walls = true;
k.fixes.topology_failed_evaluations_propagate = true;
@@ -6,8 +6,8 @@
//! The vase is a full turn of the profile (0, 0) - (15, 5) - (15, 30) - (0, 30) in the
//! XY plane about the y axis: a conical bottom whose generator starts on the axis, a
//! cylindrical wall and a flat top. The bottom's inward offset generator crosses the
//! axis, so (with WR-343's `revolution_offset_axis_both_sides`) the offset surface is
//! cut there and its domain no longer holds the face's. `BuildOffsetOfFaceuse` then
//! axis, so the offset surface is cut there (WR-343) and its domain no longer holds the
//! face's. `BuildOffsetOfFaceuse` then
//! clips the face to the offset domain with a 2D boolean of planar faces made from the
//! face's UV loops. Taken from the inset's opposite face use, upstream's loops run
//! clockwise, the planar face is inside out, the intersection is empty and the bottom
@@ -70,10 +70,9 @@ fn vase(k: &mut Kernel, up: bool) -> Brep {
create_rotational_sweep(k, &curves, origin, axis, 360.0, true).unwrap()
}
/// The vase shelled 2 inward, open at its top, with WR-342's `fixed` (WR-343 on).
/// The vase shelled 2 inward, open at its top, with WR-342's `fixed`.
fn shelled(up: bool, fixed: bool) -> (Kernel, Brep) {
let mut k = Kernel::default();
k.fixes.revolution_offset_axis_both_sides = true;
k.fixes.offset_clip_right_side_out = fixed;
let part = vase(&mut k, up);
let open = planar_faces_at(&k, &part, 1, H);
@@ -111,7 +110,6 @@ fn wr_342_vase_shells_closed_with_the_fix() {
fn wr_342_default_refuses_the_open_shell_and_retains_the_vase() {
for up in [true, false] {
let mut k = Kernel::default();
k.fixes.revolution_offset_axis_both_sides = true;
let mut part = vase(&mut k, up);
let open = planar_faces_at(&k, &part, 1, H);
assert_eq!(open.len(), 1);
@@ -4,11 +4,12 @@
//! r 52..64 over 90..270) overlap over a quarter turn and share the planes z = 0 and
//! z = 60, and are joined.
//!
//! With the sweep's frame as upstream builds it (`sweep_frame_on_planar_path_normal`
//! off), wall A's flat top is a B-spline 2.8e-5 off its plane, so it is not planar and
//! its search domain reaches past the fold of its continuation. Upstream's
//! `CoverCoincidentSurface` merges wall B's top onto that folded continuation and the
//! join loses 5573 mm3. With `cover_refuses_folded_continuation` the cover across the
//! With the sweep's frame as upstream built it, wall A's flat top was a B-spline 2.8e-5
//! off its plane, so it was not planar and its search domain reached past the fold of its
//! continuation; upstream's `CoverCoincidentSurface` merged wall B's top onto that folded
//! continuation and the join lost 5573 mm3. WR-407's frame on the path plane's normal is
//! the default now, and without `cover_refuses_folded_continuation` the join still loses
//! more than 1000 mm3. With `cover_refuses_folded_continuation` the cover across the
//! fold is refused and the join has the closed-form volume
//! `V_A + V_B - overlap = 60 (pi/2)(56^2 - 44^2) + 60 (pi/2)(64^2 - 52^2) - 60 (pi/4)(56^2 - 52^2)`.
//! The walls' radial faces are the sweep's approximation of the arcs, so the volume is
@@ -80,7 +81,7 @@ fn wall(k: &mut Kernel, turn: (Vec3, f64), radius: f64, start: f64, end: f64) ->
.brep
}
/// The two walls joined, with WR-407's fix `fixed`.
/// The two walls joined, with `cover_refuses_folded_continuation` (WR-407's cover half) `fixed`.
fn joined(fixed: bool) -> (Kernel, Brep) {
let mut k = Kernel::default();
k.fixes.cover_refuses_folded_continuation = fixed;
@@ -119,7 +120,8 @@ fn wr_407_walls_join_to_the_closed_form_with_the_fix() {
);
}
/// Upstream merges onto the folded continuation: the join is short by 5573 mm3.
/// Without the fold rule the cover merges onto the folded continuation: the join is short
/// by more than 1000 mm3 (5573 mm3 with upstream's sweep frame).
#[test]
fn wr_407_default_join_loses_the_folded_cover() {
let (k, mut b) = joined(false);
+13 -27
View File
@@ -16,9 +16,8 @@ fn pose(axis: Vec3, degrees: f64, offset: Vec3) -> Axis2Placement {
p.translate(offset);
p
}
fn body(transport: bool, centre: bool, pose: &Axis2Placement) -> (Kernel, Brep) {
fn body(centre: bool, pose: &Axis2Placement) -> (Kernel, Brep) {
let mut k = Kernel::default();
k.fixes.sweep_frame_on_planar_path_normal = transport;
let pts = [
Vec3::new(18., 0., -2.),
Vec3::new(22., 0., -2.),
@@ -62,10 +61,9 @@ fn inside(k: &Kernel, b: &Brep, p: Vec3) -> bool {
fn close(a: f64, b: f64, tol: f64) {
assert!((a - b).abs() < tol, "{a} != {b}");
}
/// `transport`: with WR-407's frame too, which takes the sweep's own pose-dependent fit
/// out; `box_tol` is the bound on the bounding boxes' drift.
fn compare(centre: bool, transport: bool, box_tol: f64) {
let (k, mut flat) = body(transport, centre, &Axis2Placement::IDENTITY);
/// `box_tol` is the bound on the bounding boxes' drift.
fn compare(centre: bool, box_tol: f64) {
let (k, mut flat) = body(centre, &Axis2Placement::IDENTITY);
let exact = std::f64::consts::PI * if centre { 160. } else { 144. };
close(volume(&k, &mut flat), exact, exact * 1e-4);
let bb = brep_bounding_box(&k, &flat, true).unwrap();
@@ -80,15 +78,15 @@ fn compare(centre: bool, transport: bool, box_tol: f64) {
(Vec3::X, 0.),
] {
let p = pose(axis, angle, Vec3::new(3., -7., 11.));
let (mut k, mut b) = body(transport, centre, &p);
let (mut k, mut b) = body(centre, &p);
let v = volume(&k, &mut b);
// Against the closed form (pi/4 (r1^2 - r0^2) 4): the sweep is an approximation
// whose fit depends on the pose, to about 5e-5 of the volume.
close(v, exact, exact * 1e-4);
solidcore::topology::brep::p5::transform(&mut k, &mut b, &p.invert(), None, false).unwrap();
let actual = brep_bounding_box(&k, &b, true).unwrap();
// Without WR-407's frame the sweep's fit depends on the pose (up to 3.3e-3 at
// the arc's far end); the roll this fixes is 0.73.
// The sweep's own fit leaves the boxes within 1e-5 of each other; the roll this
// fixes is 0.73.
let drift = actual
.min()
.distance(bb.min())
@@ -112,28 +110,16 @@ fn compare(centre: bool, transport: bool, box_tol: f64) {
}
#[test]
fn centred_profile_is_rigid_motion_invariant() {
compare(true, false, 5e-3);
compare(true, 1e-5);
}
#[test]
fn first_point_profile_is_rigid_motion_invariant() {
compare(false, false, 5e-3);
}
#[test]
fn with_the_transport_frame_the_boxes_agree_closely() {
compare(true, true, 1e-5);
compare(false, true, 1e-5);
compare(false, 1e-5);
}
/// A path of two pieces, the line along +y for 20 and the quarter arc, in either order,
/// the square as in `body`. `transport` turns on WR-407's frame: without it the sweep's
/// own pose dependence still cuts the arc-then-line path to volume 0 in some poses.
fn two_piece_body(
transport: bool,
centre: bool,
line_first: bool,
pose: &Axis2Placement,
) -> (Kernel, Brep) {
/// the square as in `body`.
fn two_piece_body(centre: bool, line_first: bool, pose: &Axis2Placement) -> (Kernel, Brep) {
let mut k = Kernel::default();
k.fixes.sweep_frame_on_planar_path_normal = transport;
let pts = [
Vec3::new(18., 0., -2.),
Vec3::new(22., 0., -2.),
@@ -173,7 +159,7 @@ fn two_piece_body(
fn a_path_of_a_line_and_an_arc_is_rigid_motion_invariant() {
for line_first in [true, false] {
for centre in [true, false] {
let (k, mut flat) = two_piece_body(true, centre, line_first, &Axis2Placement::IDENTITY);
let (k, mut flat) = two_piece_body(centre, line_first, &Axis2Placement::IDENTITY);
// the arc's volume, and the line's 20 x 16
let exact = std::f64::consts::PI * if centre { 160. } else { 144. } + 320.;
close(volume(&k, &mut flat), exact, exact * 1e-4);
@@ -188,7 +174,7 @@ fn a_path_of_a_line_and_an_arc_is_rigid_motion_invariant() {
(Vec3::new(-2., 1., 4.), 41.),
] {
let p = pose(axis, angle, Vec3::new(3., -7., 11.));
let (mut k, mut b) = two_piece_body(true, centre, line_first, &p);
let (mut k, mut b) = two_piece_body(centre, line_first, &p);
close(volume(&k, &mut b), exact, exact * 1e-4);
solidcore::topology::brep::p5::transform(&mut k, &mut b, &p.invert(), None, false)
.unwrap();
+16
View File
@@ -297,6 +297,22 @@ faces that did not move, which is the intersection of the same two surfaces agai
`local_operation.rs` or the executive keeps such an edge as it was; keeping the edges between
unmoved faces, so the operation changes nothing it does not move, is open.
**Tangent edges.** An edge whose faces were tangent in the input is extended and trimmed along its
old line, unless the reach proves that the new surfaces, a plane and a quadric, cross in every
normal section of the edge (`LocalReach::crossing`, `local_extension_reach`). The plane then cuts
the quadric on both sides of the old edge, about √(2rδ) away for a round of radius r moved by δ,
so the nearest midpoint decides nothing: the mitre keeps the cut on the curved face's side, where
the plane is extended past its edge. An edge mated next to such a tangent edge, once that one is
mated, ends at its new vertex (a lamina edge still ends at its vertex's image); where that vertex
is not on the edge's curve, the mate is refused (`OffsetFailure::TangentEndOffCurve`).
**A given surface's side.** A face's new surface is turned to face the way its old one did by
comparing the two normals. Upstream reads the new normal at the middle of the new surface's
domain; for a full cylinder given to a round that point is across the axis, where the normal
opposes the old round's though both face out, and the round came back inside out. The new normal
is read where the new surface passes nearest the old face's middle (the domain middle when that
drop fails or there is no normal at the nearest point, as at a cone's apex).
**Moving faces** (`offset/offset_faces.rs`, `OffsetFacesExecutive`): the tangent blends on the
named faces are taken off, the faces moved by a local operation onto their offset surfaces, and the
blends put back (`take_off`, `move_faces`, `reblend`); refusals are `OffsetFacesRefusal`. "Moved no
@@ -8,7 +8,6 @@
"name": "two_overlapping_swept_arc_walls_joined",
"note": "wall A over 0..180 degrees, wall B over 90..270: one lump, the two tops and the two bottoms joined by the merge",
"fixes": [
"sweep_frame_on_planar_path_normal",
"cover_refuses_folded_continuation"
],
"steps": [
@@ -126,7 +125,6 @@
"name": "two_overlapping_swept_arc_walls_outside_the_arcs",
"note": "the same walls with the arcs tangent to their inner faces (the profile 0..12 outward from the path start), so a swept top continued past its domain runs back through the sector's centre: without the fold rule the join keeps a face built on that continuation and runs 240 mm out of the body",
"fixes": [
"sweep_frame_on_planar_path_normal",
"cover_refuses_folded_continuation"
],
"steps": [
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="600" height="520" viewBox="0 0 600 520">
<title>surface.surf_of_revolution--offset-cut-at-the-axis-below-the-origin</title>
<g id="Layer_1" data-name="Layer 1">
<rect id="background" x="0" y="0" width="600" height="520" fill="#ffffff"/>
<rect id="axis_segment_of_the_fix" data-name="axis segment of the fix" x="164" y="70" width="12" height="410" fill="#dce9f5"/>
<line id="axis" x1="170" y1="64" x2="170" y2="493" fill="none" stroke="#999999" stroke-width="1.5" stroke-miterlimit="10" stroke-dasharray="6 4"/>
<line id="upstream_axis_segment" data-name="upstream axis segment" x1="170" y1="130" x2="170" y2="70" fill="none" stroke="#555555" stroke-width="4" stroke-miterlimit="10"/>
<line id="generator" x1="170" y1="185" x2="390" y2="405" fill="none" stroke="#1f6fb2" stroke-width="3" stroke-miterlimit="10"/>
<line id="offset_outward" data-name="offset outward" x1="208.9" y1="146.1" x2="428.9" y2="366.1" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10"/>
<line id="offset_inward__kept" data-name="offset inward, kept" x1="170" y1="262.8" x2="351.1" y2="443.9" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10"/>
<line id="stretch_past_the_axis" data-name="stretch past the axis" x1="131.1" y1="223.9" x2="170" y2="262.8" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="4 3"/>
<circle id="cut_point" data-name="cut point" cx="170" cy="262.8" r="5" fill="#555555"/>
<circle id="frame_origin" data-name="frame origin" cx="170" cy="130" r="5" fill="#555555"/>
<text transform="matrix(1 0 0 1 20 32)" x="0" y="0" xml:space="preserve" font-family="Helvetica-Bold, sans-serif" font-size="20" fill="#000000"><tspan x="0" y="0">WR-343: an offset crossing the axis below the origin</tspan></text>
<text transform="matrix(1 0 0 1 20 52)" x="0" y="0" xml:space="preserve" font-family="Helvetica, sans-serif" font-size="12" fill="#000000"><tspan x="0" y="0">Meridian plane of the issue 973 test (x right, z up); offsets by 1</tspan></text>
<text transform="matrix(1 0 0 1 180 90)" x="0" y="0" xml:space="preserve" font-family="Helvetica, sans-serif" font-size="14" fill="#000000"><tspan x="0" y="0">upstream&apos;s axis segment</tspan></text>
<text transform="matrix(1 0 0 1 158 135)" x="0" y="0" xml:space="preserve" font-family="Helvetica, sans-serif" font-size="14" fill="#000000" text-anchor="end"><tspan x="0" y="0">frame origin</tspan></text>
<text transform="matrix(1 0 0 1 410 388)" x="0" y="0" xml:space="preserve" font-family="Helvetica, sans-serif" font-size="14" fill="#000000"><tspan x="0" y="0">offset away from the axis</tspan></text>
<text transform="matrix(1 0 0 1 340 470)" x="0" y="0" xml:space="preserve" font-family="Helvetica, sans-serif" font-size="14" fill="#000000"><tspan x="0" y="0">offset towards the axis</tspan></text>
<text transform="matrix(1 0 0 1 158 290)" x="0" y="0" xml:space="preserve" font-family="Helvetica, sans-serif" font-size="14" fill="#000000" text-anchor="end"><tspan x="0" y="0">cut here: z = -1 - √2</tspan></text>
<text transform="matrix(1 0 0 1 125 200)" x="0" y="0" xml:space="preserve" font-family="Helvetica, sans-serif" font-size="14" fill="#000000" text-anchor="end"><tspan x="0" y="0">upstream keeps</tspan></text>
<text transform="matrix(1 0 0 1 125 217)" x="0" y="0" xml:space="preserve" font-family="Helvetica, sans-serif" font-size="14" fill="#000000" text-anchor="end"><tspan x="0" y="0">the part past the axis</tspan></text>
<text transform="matrix(1 0 0 1 158 462)" x="0" y="0" xml:space="preserve" font-family="Helvetica, sans-serif" font-size="12" fill="#000000" text-anchor="end"><tspan x="0" y="0">axis segment of the fix:</tspan></text>
<text transform="matrix(1 0 0 1 158 477)" x="0" y="0" xml:space="preserve" font-family="Helvetica, sans-serif" font-size="12" fill="#000000" text-anchor="end"><tspan x="0" y="0">the offset&apos;s whole height</tspan></text>
<text transform="matrix(1 0 0 1 398 422)" x="0" y="0" xml:space="preserve" font-family="Helvetica, sans-serif" font-size="14" fill="#000000"><tspan x="0" y="0">generator</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -0,0 +1,268 @@
# Five surface fixes become the only path: 133 golden rows in ten units (2026-11-12)
- Flags retired (#14246 batch B47): `approximate_surface_bezier_starter` (WR-573),
`approximate_surface_checks_every_row` (WR-726), `extrusion_step_uses_clamped_uv` (WR-61),
`revolution_frame_from_profile_origin` (WR-738), `revolution_offset_axis_both_sides` (WR-343).
The addenda dated 2026-11-12 under `plan/upstream-issues/` name each retirement.
- All units at the fix (`cargo run --profile goldens -p goldens -- --all --goldens`): 283 units,
273 pass; ten units move 133 rows, added 1 (`surface.surface_approximate/gen0381`, which leaves
`known_failing.json`), removed 0, broken 0.
- Which flag moves which row: main's code with one flag switched on at a time (a temporary patch
to `Kernel::new`, not committed), the ten units each time. The rows add up: WR-573 42, WR-726 31
(plus gen0381), WR-61 9, WR-738 36, WR-343 14. No row moves under two flags.
- Rows that bless rewrote only within the goldens' tolerance (`1e-9` relative) keep their old
text, so the diff holds the 133 moved rows only.
- Run time: `surface.surface_approximate` takes 250 s at the fix, 135 s on main (WR-573 alone 220 s,
WR-726 alone 128 s): the starters main returned no surface for are now fitted, pass by pass.
## Why the new rows are right
### `surface.surface_approximate` (74 rows: WR-573 42, WR-726 32 with gen0381)
A row records the status, the achieved tolerance and the surface. A temporary test (not committed)
ran each moved case at the fix and printed its status and achieved tolerance next to the requested
one (`approx_tol3d`: `USE_DEFAULT` = 9876543.21 is 5e-6, a negative tolerance counts by its size).
In all 74 rows either a surface comes back with an achieved tolerance at or below the requested
one, or the call fails with `SM_ERR` and an achieved tolerance above it.
| rows | flag | old | new |
|---|---|---|---|
| 18 | WR-573 | success, no surface | surface within tolerance |
| 24 | WR-573 | success, no surface | `SM_ERR`, tolerance missed |
| 12 | WR-726 | surface | finer surface within tolerance |
| 19 | WR-726 | surface | `SM_ERR`, tolerance missed |
| 1 | WR-726 | known failing (gen0381) | `SM_ERR`, 0.546 for 1.38e-3 |
- WR-573: these starters have simple inner knots (the fixtures `simple_knot_cubic_patch_index`,
`offset_simple_knot_cubic`, `issue1501_offset_wave_simple_knots` and 39 generated ones). Main's
patches ran past the control net and it returned success with no surface and an achieved
tolerance of -1 (nothing tested). Where the search then reaches the tolerance there is a surface;
where its last pass misses it, `SM_ERR`.
- WR-726: main accepted a pass when its last row of patches was within the tolerance, so it
returned surfaces whose earlier rows were not. The quarter cylinder fixtures (issue #1441's
offset quarter cylinder and its wrapped copies) now give a finer net (204 numbers instead of
184) at 8.99e-4 for 1e-3; `approximates_an_offset_quarter_cylinder` checks the distance to the
offset surface.
### `surface.surf_of_extrusion_p1` (9 rows, WR-61)
Every moved case evaluates a parameter outside the surface's domain with out-of-bounds evaluation
off (`review4_*_oob0_*`, `review_*_eval_out`, `gen0404`). The evaluators now clamp to the domain:
for `review4_line_oob0_eval0` (line (0, 0, 0)-(1, 2, 3) swept along z, u = -20, v = 2) the point is
(0, 0, 2), the start of the line lifted by 2, where main extrapolated to u = -20. The unit tests
`step_evaluators_clamp_to_the_surface_domain` and `step_clamp_reads_the_analytic_domain_with_out_of_bounds_off`
check the clamped values exactly.
### Surfaces of revolution: the frame (36 rows, WR-738)
Rows of `surf_of_revolution_p1` (7), `_p2` (8), `_p3` (15), `blend.fillet_solver_p4` (2),
`surface.offset_surface` (1), `surface.cone_p1` (2) and `surface.surface_point_solve_p3` (1).
All are `create_canonical` surfaces whose profile interval holds parameter 0 (an arc on
[-146°, 122°]), or whose reference point is near the axis by angle but not by distance. The
generator and the axis are unchanged, so the surface is the same point set; only the frame's
X axis, where u = 0, turns. The rows record parameters, NURB nets and iso curves measured from it.
- `to_nurbs` rows whose second coordinate moves (`surf_of_revolution_p3` gen0637, gen0650, gen0808)
have `swap_uv`: their second coordinate is the angle.
- `blend.fillet_solver_p4` gen0189 and gen0281 solve a fillet point on revolutions of lines at
coordinates near 1e6, given by surface parameters. In each, one line starts on the axis to
rounding (3.3e-11 and 5.8e-11 from it, 5.5e5 and 8.1e5 from the axis point). Main's test (within 1° of
the axis direction) calls it on the axis and turns to the 45% point; WR-738's test
(`|Z x (P - O)|` below `SM_EFF_ZERO` = 1e-12, an absolute bound) does not, so the frame's X axis
points along the rounding residue. Every X axis perpendicular to the axis gives the same full
turn, so the surfaces are the same; the given parameters name other points of them. A bound
relative to the coordinates would keep main's frame here; that is outside this retirement.
- Check on `surf_of_revolution_p1/gen0052` (`to_step`): the angle moves from 180.84860962° to
171.10481566°, the profile parameter stays at -93.2734222°. The X axes of main (towards the arc's
start, -145.97°) and of the fix (towards its point at 0°) are 9.74379395938° apart about the axis;
the angle moved by 9.74379395937°.
- `surface.cone_p1` (`iso_circle`, 2 rows): the reference shim refuses the op (`-1002`) when its
STEP inversion of the point fails. The inversion now solves both sides of the axis, so the op
runs. `review2_std_bnd_neg_h_oob_icb8` builds the circle at height 0 (radius 1; tolerance 1);
`review2_std_semi_unb_120_oob_ic9` refuses with `SM_ERR`: its point (0, -2, 2) is 2 from the axis
where the cone's radius (`Cone::radius`, 1 + h tan 120° at h = 2) is -2.46, so it lies 0.46 off
the surface (tolerance 1e-6).
- `surface.surface_point_solve_p3/matrix_1143`: the solved point is the same to 2e-16; its residual
moves from 6.6486136e-10 to 6.6486168e-10 (3e-16 absolute).
### Surfaces of revolution: the offset at the axis (14 rows, WR-343)
Rows of `surf_of_revolution_p2` (11 `offset` ops, among them the review cases
`review_offset_axis_crossing_merge` and `review_circle_touch_axis_offset`), `surface.offset_surface`
(`gen1740`) and `offset.offset_geometry_creation` (`gen0330`, `gen0492`, offsets of the torus
fixtures). Their offset generators cross the axis below the frame's origin, where main's axis
segment (from the origin along +z only) did not reach: main kept a stretch of negative radius,
the fix cuts it at the axis and keeps the generatrix's side. `issue_973_offset_cut_at_axis_below_origin`
checks all four end points of its two offset generators against the closed form.
![The meridian plane of the issue 973 surface: the axis, the frame origin on it, the generator from (0, 0, -1) to (4, 0, -5), its offset away from the axis, and its offset towards the axis, which crosses the axis at z = -1 - sqrt(2). Upstream's axis segment runs up from the origin and misses the crossing; the fix's spans the offset's whole height.](surface.surf_of_revolution--offset-cut-at-the-axis-below-the-origin.svg)
*The offset towards the axis is cut where it crosses it; upstream keeps the dashed part past it.*
## Every row
Old -> new values as the golden stores them (arrays of up to 16 numbers in full, longer ones as
their count and the first block's sum).
### surface.surface_approximate (74 rows)
- `curve_bounded_cylinder` (WR-726): out: n 184, first sum 15566.74928 -> out: n 204, first sum 15628.78678 (requested 0.001, achieved 0.000898756)
- `curve_bounded_just_copy` (WR-726): out: n 184, first sum 15566.74928 -> out: n 204, first sum 15628.78678 (requested 0.001, achieved 0.000898756)
- `gen0008` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.046544378290028623,0.0] (requested 1.5151e-06, achieved 0.0465444)
- `gen0014` (WR-726): out: n 1202, first sum 254860757.3 -> out: [1001.0,1004112.8532259027,0.0] (requested 0.225753, achieved 1.00411e+06)
- `gen0017` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.020595937621324497,0.0] (requested 0.00264246, achieved 0.0205959)
- `gen0028` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.016413593121751058,0.0] (requested 1.15385e-06, achieved 0.0164136)
- `gen0060` (WR-573): out: [0.0,-1.0,0.0] -> out: n 142, first sum 15145.61193 (requested 0.000956969, achieved 4.996e-16)
- `gen0062` (WR-726): out: n 594, first sum 19046.25145 -> out: [1001.0,4.023979816229703,0.0] (requested 1, achieved 4.02398)
- `gen0065` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.0029210410795245377,0.0] (requested 5e-06, achieved 0.00292104)
- `gen0073` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,4.817904104538713e-07,0.0] (requested 1e-08, achieved 4.8179e-07)
- `gen0080` (WR-726): out: n 34526, first sum 16336.03343 -> out: [1001.0,799.3748293739277,0.0] (requested 1.39378e-06, achieved 799.375)
- `gen0082` (WR-726): out: n 216, first sum 15455.63576 -> out: [1001.0,0.11719779725830086,0.0] (requested 5e-06, achieved 0.117198)
- `gen0084` (WR-726): out: n 910, first sum 14385.25789 -> out: [1001.0,0.015286884767212525,0.0] (requested 0.0032924, achieved 0.0152869)
- `gen0085` (WR-573): out: [0.0,-1.0,0.0] -> out: n 170, first sum 15412.85781 (requested 5e-06, achieved 3.97205e-15)
- `gen0097` (WR-573): out: [0.0,-1.0,0.0] -> out: n 150, first sum 15145.11754 (requested 8.16175e-07, achieved 6.28037e-16)
- `gen0101` (WR-573): out: [0.0,-1.0,0.0] -> out: n 150, first sum 15382.465 (requested 0.00239282, achieved 2.26442e-15)
- `gen0117` (WR-726): out: n 310, first sum 15192.48655 -> out: n 312, first sum 15191.17452 (requested 0.00130784, achieved 0.00118715)
- `gen0131` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.0029973409689794013,0.0] (requested 0.00102197, achieved 0.00299734)
- `gen0139` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,1104.3950770781375,0.0] (requested 1000, achieved 1104.4)
- `gen0140` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,3.476496676255416e-05,0.0] (requested 5e-06, achieved 3.4765e-05)
- `gen0142` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.000772678801436295,0.0] (requested 0.000563528, achieved 0.000772679)
- `gen0150` (WR-573): out: [0.0,-1.0,0.0] -> out: n 284, first sum 15276.27 (requested 0.00102232, achieved 0.000348235)
- `gen0152` (WR-573): out: [0.0,-1.0,0.0] -> out: n 82, first sum 15204.11082 (requested 5e-06, achieved 1.77636e-15)
- `gen0159` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.10440668582753658,0.0] (requested 5e-06, achieved 0.104407)
- `gen0163` (WR-726): out: n 1202, first sum 15450.54378 -> out: [1001.0,0.016705497909646823,0.0] (requested 0.00310834, achieved 0.0167055)
- `gen0170` (WR-573): out: [0.0,-1.0,0.0] -> out: n 758, first sum 15160.00016 (requested 1e-09, achieved 4.38789e-10)
- `gen0184` (WR-726): out: n 2562, first sum 15492.68866 -> out: [1001.0,0.4287885036687718,0.0] (requested 0.275788, achieved 0.428789)
- `gen0187` (WR-726): out: n 2100, first sum 15389.56103 -> out: [1001.0,0.10695810274694764,0.0] (requested 0.00177256, achieved 0.106958)
- `gen0193` (WR-726): out: n 10054, first sum 15388.51785 -> out: n 10390, first sum 15433.41929 (requested 5e-06, achieved 4.93785e-06)
- `gen0194` (WR-726): out: n 430, first sum 14430.79958 -> out: n 514, first sum 14445.04768 (requested 0.00260479, achieved 0.00213861)
- `gen0198` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,2.621617324553154e-06,0.0] (requested 1.47481e-06, achieved 2.62162e-06)
- `gen0202` (WR-726): out: n 150, first sum 15027.13736 -> out: [1001.0,0.4656540030115775,0.0] (requested 0.200214, achieved 0.465654)
- `gen0204` (WR-573): out: [0.0,-1.0,0.0] -> out: n 150, first sum 15150.31415 (requested 7.17309e-07, achieved 8.00593e-16)
- `gen0209` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.002789624015908364,0.0] (requested 0.00215219, achieved 0.00278962)
- `gen0217` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.011836907557934699,0.0] (requested 2.25581e-06, achieved 0.0118369)
- `gen0224` (WR-573): out: [0.0,-1.0,0.0] -> out: n 3522, first sum 15235.99957 (requested 1e-08, achieved 5.27681e-09)
- `gen0226` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.016068016380867373,0.0] (requested 0.00141482, achieved 0.016068)
- `gen0235` (WR-573): out: [0.0,-1.0,0.0] -> out: n 192, first sum 15167.03145 (requested 0.0743657, achieved 0.0122023)
- `gen0250` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.001130920677656001,0.0] (requested 0.000619082, achieved 0.00113092)
- `gen0265` (WR-726): out: n 1422, first sum 2.308078675e+13 -> out: [1001.0,248881929874.52722,0.0] (requested 10, achieved 2.48882e+11)
- `gen0267` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.10378410025796724,0.0] (requested 0.0012147, achieved 0.103784)
- `gen0272` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.013105380031664254,0.0] (requested 0.00246033, achieved 0.0131054)
- `gen0275` (WR-726): out: n 430, first sum 15547.68894 -> out: n 474, first sum 15559.87157 (requested 0.00221586, achieved 0.00197381)
- `gen0276` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.9108579385852134,0.0] (requested 9.69193e-07, achieved 0.910858)
- `gen0282` (WR-573): out: [0.0,-1.0,0.0] -> out: n 122, first sum 15071.00006 (requested 1e-07, achieved 4.31345e-08)
- `gen0287` (WR-573): out: [0.0,-1.0,0.0] -> out: n 2786, first sum 15438.19196 (requested 0.00256914, achieved 0.00246246)
- `gen0288` (WR-573): out: [0.0,-1.0,0.0] -> out: n 116, first sum 15068.50073 (requested 5e-06, achieved 4.06053e-06)
- `gen0294` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.08423910237000219,0.0] (requested 5e-06, achieved 0.0842391)
- `gen0297` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,45457284320282.61,0.0] (requested 0, achieved 4.54573e+13)
- `gen0299` (WR-726): out: n 352, first sum 15142.72336 -> out: [1001.0,1.0232805440672476e-09,0.0] (requested 1e-09, achieved 1.02328e-09)
- `gen0312` (WR-726): out: n 1014, first sum 15163.86785 -> out: [1001.0,0.17599829158680422,0.0] (requested 0.00149895, achieved 0.175998)
- `gen0317` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.0011264268224270937,0.0] (requested 2.11138e-06, achieved 0.00112643)
- `gen0333` (WR-726): out: n 116, first sum 15260.05586 -> out: [1001.0,0.06986167971141,0.0] (requested 0.00326581, achieved 0.0698617)
- `gen0339` (WR-726): out: n 1006, first sum 15181.99789 -> out: n 910, first sum 15181.99812 (requested 1e-08, achieved 9.14075e-09)
- `gen0344` (WR-573): out: [0.0,-1.0,0.0] -> out: n 82, first sum 15055.00002 (requested 5e-06, achieved 7.75126e-08)
- `gen0347` (WR-726): out: n 866, first sum 10606153.9 -> out: n 1140, first sum 10436697.26 (requested 100, achieved 96.31)
- `gen0351` (WR-726): out: n 1226, first sum -1.150875092e+10 -> out: [1001.0,1114772477.5853817,0.0] (requested 0.00263127, achieved 1.11477e+09)
- `gen0357` (WR-573): out: [0.0,-1.0,0.0] -> out: n 150, first sum 15307.65723 (requested 0.00282072, achieved 2.22045e-15)
- `gen0363` (WR-726): out: n 1226, first sum -9195201622 -> out: [1001.0,1110000664.1427076,0.0] (requested 0.00342223, achieved 1.11e+09)
- `gen0364` (WR-726): out: n 390, first sum 15519.11627 -> out: [1001.0,0.3698797955277302,0.0] (requested 0.336171, achieved 0.36988)
- `gen0367` (WR-726): out: n 594, first sum 109911249.8 -> out: [1001.0,68106.11406241925,0.0] (requested 1000, achieved 68106.1)
- `gen0370` (WR-726): out: n 266, first sum 2.595511372e+10 -> out: [1001.0,161512933.98002112,0.0] (requested 0.316304, achieved 1.61513e+08)
- `gen0373` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,1.8619006149354548e-16,0.0] (requested 0, achieved 1.8619e-16)
- `gen0374` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.001183010532713599,0.0] (requested 0, achieved 0.00118301)
- `gen0381` (WR-726): (known failing, no row) -> out: [1001.0,0.5460107506506426,0.0] (requested 0.00138129, achieved 0.546011)
- `gen0389` (WR-573): out: [0.0,-1.0,0.0] -> out: [1001.0,0.0026650155416503584,0.0] (requested 0.000782867, achieved 0.00266502)
- `gen0395` (WR-726): out: n 2510, first sum -1221.276073 -> out: [1001.0,4088.13408350007,0.0] (requested 0.00174909, achieved 4088.13)
- `issue1501_offset_wave_simple_knots` (WR-573): out: [0.0,-1.0,0.0] -> out: n 240, first sum 15665.14101 (requested 0.001, achieved 0.000918398)
- `offset_simple_knot_cubic` (WR-573): out: [0.0,-1.0,0.0] -> out: n 186, first sum 15260.33855 (requested 0.001, achieved 0.000961809)
- `offset_zero_cylinder` (WR-726): out: n 184, first sum 15566.74928 -> out: n 204, first sum 15628.78678 (requested 0.001, achieved 0.000898756)
- `quarter_cylinder_bare` (WR-726): out: n 184, first sum 15566.74928 -> out: n 204, first sum 15628.78678 (requested 0.001, achieved 0.000898756)
- `simple_knot_cubic_patch_index` (WR-573): out: [0.0,-1.0,0.0] -> out: n 118, first sum 15167.2 (requested 0.001, achieved 1.33234e-15)
- `volume_cylinder` (WR-726): out: n 184, first sum 15561.63605 -> out: n 204, first sum 15621.48975 (requested 0.001, achieved 0.000898756)
- `volume_just_copy` (WR-726): out: n 184, first sum 15561.63605 -> out: n 204, first sum 15621.48975 (requested 0.001, achieved 0.000898756)
### surface.surf_of_extrusion_p1 (9 rows)
- `gen0404` (WR-61 `eval_step`): build: [0.0]; out: n 48, first sum 0.1140089018; status: [0.0] -> build: [0.0]; out: n 48, first sum 0.5967790265; status: [0.0]
- `review4_circ_oob0_eval0` (WR-61 `eval_step`): build: [0.0]; out: n 48, first sum 10.23970742; status: [0.0] -> build: [0.0]; out: n 48, first sum 11.03428672; status: [0.0]
- `review4_line_oob0_eval0` (WR-61 `eval_step`): build: [0.0]; out: n 48, first sum -111; status: [0.0] -> build: [0.0]; out: n 48, first sum 9; status: [0.0]
- `review4_line_oob0_eval1` (WR-61 `eval_step`): build: [0.0]; out: n 48, first sum 27; status: [0.0] -> build: [0.0]; out: n 48, first sum 15; status: [0.0]
- `review4_line_oob0_eval3` (WR-61 `eval_step`): build: [0.0]; out: n 48, first sum 309; status: [0.0] -> build: [0.0]; out: n 48, first sum 15; status: [0.0]
- `review_hugeh_eval_out` (WR-61 `eval_step`): build: [0.0]; out: n 27, first sum 128284.9639; status: [0.0] -> build: [0.0]; out: n 27, first sum 128286.9639; status: [0.0]
- `review_hyp_eval_out` (WR-61 `eval_step`): build: [0.0]; out: n 27, first sum 13.07859639; status: [0.0] -> build: [0.0]; out: n 27, first sum 15.07859639; status: [0.0]
- `review_par_eval_out` (WR-61 `eval_step`): build: [0.0]; out: n 27, first sum 3.5; status: [0.0] -> build: [0.0]; out: n 27, first sum 5.5; status: [0.0]
- `review_tinyp_eval_out` (WR-61 `eval_step`): build: [0.0]; out: n 27, first sum -0.9952; status: [0.0] -> build: [0.0]; out: n 27, first sum 1.0048; status: [0.0]
### surface.surf_of_revolution_p1 (7 rows)
- `gen0052` (WR-738 `to_step`): build: [0.0]; out: [180.84860962098946,-93.27342223079216]; status: [0.0] -> build: [0.0]; out: [171.10481566161275,-93.27342223079216]; status: [0.0]
- `gen0061` (WR-738 `directrix`): build: [0.0]; out: n 60, first sum 14386.30753; status: [0.0] -> build: [0.0]; out: n 60, first sum 14380.63623; status: [0.0]
- `gen0184` (WR-738 `iso`): build: [0.0]; out: n 57, first sum 14218.2252; status: [0.0] -> build: [0.0]; out: n 57, first sum 14260.27235; status: [0.0]
- `gen0259` (WR-738 `start_plane`): build: [0.0]; out: [1.0648073416517736,1.199218327003228,-0.36554872688804285,1.0,153.91068538477188,1.0]; status: [0.0] -> build: [0.0]; out: [1.056398047914386,1.5499503341938334,-0.36554872688804285,1.0,149.17954822171788,1.0]; status: [0.0]
- `gen0329` (WR-738 `directrix`): build: [0.0]; out: n 60, first sum 14467.44267; status: [0.0] -> build: [0.0]; out: n 60, first sum 14439.84247; status: [0.0]
- `gen0341` (WR-738 `start_plane`): build: [0.0]; out: [-6.700176938643628,-3.357395923882631,2.6819422214491437,1.0,129.95724729768597,1.0]; status: [0.0] -> build: [0.0]; out: [-6.654514455177553,-3.3231040753908836,2.7324075544640247,1.0,127.97973993307939,1.0]; status: [0.0]
- `gen0436` (WR-738 `to_step`): build: [0.0]; out: [-3.619929122494868e-15,1.797055921200299]; status: [0.0] -> build: [0.0]; out: [1.0118009489537107e-15,1.797055921200299]; status: [0.0]
### surface.surf_of_revolution_p2 (19 rows)
- `gen0042` (WR-343 `offset`): build: [0.0]; out: n 333, first sum -inf; status: [0.0] -> build: [0.0]; out: n 291, first sum -inf; status: [0.0]
- `gen0082` (WR-343 `offset`): build: [0.0]; out: n 4701, first sum -inf; status: [0.0] -> build: [0.0]; out: n 7893, first sum -inf; status: [0.0]
- `gen0092` (WR-738 `offset`): build: [0.0]; out: n 1845, first sum -inf; status: [0.0] -> build: [0.0]; out: n 1761, first sum -inf; status: [0.0]
- `gen0101` (WR-343 `offset`): build: [0.0]; out: n 2559, first sum -inf; status: [0.0] -> build: [0.0]; out: n 753, first sum -inf; status: [0.0]
- `gen0168` (WR-343 `offset`): build: [0.0]; out: n 415, first sum 30350.19326; status: [0.0] -> build: [0.0]; out: n 415, first sum 30893.99302; status: [0.0]
- `gen0199` (WR-738 `build`): build: [0.0]; out: n 331, first sum 29909.79442 -> build: [0.0]; out: n 331, first sum 29921.0605
- `gen0236` (WR-343 `offset`): build: [0.0]; out: n 331, first sum 30161.0456; status: [0.0] -> build: [0.0]; out: n 331, first sum 28873.88458; status: [0.0]
- `gen0251` (WR-738 `adjust`): build: [0.0]; out: n 331, first sum 31395.90324; status: [0.0] -> build: [0.0]; out: n 331, first sum 31387.13815; status: [0.0]
- `gen0255` (WR-343 `offset`): build: [0.0]; out: n 415, first sum 30503.84519; status: [0.0] -> build: [0.0]; out: n 415, first sum 29883.4412; status: [0.0]
- `gen0329` (WR-738 `is_nurb`): build: [0.0]; out: n 247, first sum 30242.64304; status: [1.0] -> build: [0.0]; out: n 247, first sum 30242.01894; status: [1.0]
- `gen0368` (WR-343 `offset`): build: [0.0]; out: n 1005, first sum -inf; status: [0.0] -> build: [0.0]; out: n 1131, first sum -inf; status: [0.0]
- `gen0490` (WR-738 `trim`): build: [0.0]; out: n 331, first sum 30400.73745; status: [0.0] -> build: [0.0]; out: n 331, first sum 30397.20058; status: [0.0]
- `gen0492` (WR-343 `offset`): build: [0.0]; out: n 331, first sum 30101.03753; status: [0.0] -> build: [0.0]; out: n 247, first sum 30585.77127; status: [0.0]
- `gen0530` (WR-738 `adjust`): build: [0.0]; out: n 417, first sum 30348.83147; status: [0.0] -> build: [0.0]; out: n 417, first sum 30517.23051; status: [0.0]
- `gen0654` (WR-738 `adjust`): build: [0.0]; out: n 175, first sum 31268.55007; status: [0.0] -> build: [0.0]; out: n 175, first sum 31268.30354; status: [0.0]
- `gen0784` (WR-738 `adjust`): build: [0.0]; out: n 175, first sum 29710.9047; status: [0.0] -> build: [0.0]; out: n 175, first sum 29674.94916; status: [0.0]
- `gen0859` (WR-343 `offset`): build: [0.0]; out: n 279, first sum 31344.55107; status: [0.0] -> build: [0.0]; out: n 279, first sum 32033.55865; status: [0.0]
- `review_circle_touch_axis_offset` (WR-343 `offset`): build: [0.0]; out: n 415, first sum 31341.56261; status: [0.0] -> build: [0.0]; out: n 499, first sum 31100.17336; status: [0.0]
- `review_offset_axis_crossing_merge` (WR-343 `offset`): build: [0.0]; out: n 1215, first sum -inf; status: [0.0] -> build: [0.0]; out: n 1173, first sum -inf; status: [0.0]
### surface.surf_of_revolution_p3 (15 rows)
- `gen0052` (WR-738 `to_nurbs`): build: [0.0]; out: [0.794606939681873,-1.1975216339726882]; status: [0.0] -> build: [0.0]; out: [0.7668146372816801,-1.1975216339726882]; status: [0.0]
- `gen0240` (WR-738 `copy`): build: [0.0]; out: n 248, first sum 30316.21087; status: [0.0] -> build: [0.0]; out: n 248, first sum 30314.75713; status: [0.0]
- `gen0246` (WR-738 `swap`): build: [0.0]; out: n 331, first sum 30358.86501; status: [0.0] -> build: [0.0]; out: n 331, first sum 30358.63694; status: [0.0]
- `gen0348` (WR-738 `update`): build: [0.0]; out: n 289, first sum 29772.86668; status: [0.0] -> build: [0.0]; out: n 289, first sum 29973.05179; status: [0.0]
- `gen0422` (WR-738 `to_nurbs`): build: [0.0]; out: [0.6073151686569089,0.21182382616090017]; status: [0.0] -> build: [0.0]; out: [0.6188740940019568,0.21182382616090017]; status: [0.0]
- `gen0435` (WR-738 `to_nurbs`): build: [0.0]; out: [0.0,-1.4938106444378023]; status: [0.0] -> build: [0.0]; out: [0.00022029345919263152,-1.4938106444378023]; status: [0.0]
- `gen0444` (WR-738 `reverse`): build: [0.0]; out: n 415, first sum 30351.54025; status: [0.0] -> build: [0.0]; out: n 415, first sum 30344.99313; status: [0.0]
- `gen0462` (WR-738 `to_nurbs`): build: [0.0]; out: [0.0,0.27442760919883785]; status: [0.0] -> build: [0.0]; out: [0.0024955948462567473,0.27442760919883785]; status: [0.0]
- `gen0598` (WR-738 `swap`): build: [0.0]; out: n 247, first sum 30179.86981; status: [0.0] -> build: [0.0]; out: n 247, first sum 30177.38084; status: [0.0]
- `gen0629` (WR-738 `to_nurbs`): build: [0.0]; out: [0.8721515637142568,0.0]; status: [0.0] -> build: [0.0]; out: [0.8848737795653722,0.0]; status: [0.0]
- `gen0637` (WR-738 `to_nurbs`): build: [0.0]; out: [0.16476778247020873,0.8433855990942796]; status: [0.0] -> build: [0.0]; out: [0.16476778247020873,0.8544575028947936]; status: [0.0]
- `gen0650` (WR-738 `to_nurbs`): build: [0.0]; out: [1.2856814046429315,1.0]; status: [0.0] -> build: [0.0]; out: [1.2856814046429315,0.6905218664371997]; status: [0.0]
- `gen0736` (WR-738 `swap`): build: [0.0]; out: n 289, first sum 29635.37828; status: [0.0] -> build: [0.0]; out: n 289, first sum 29367.11663; status: [0.0]
- `gen0808` (WR-738 `to_nurbs`): build: [0.0]; out: [-2.05301802277461,0.0637711762079021]; status: [0.0] -> build: [0.0]; out: [-2.05301802277461,0.0]; status: [0.0]
- `gen0896` (WR-738 `update`): build: [0.0]; out: n 405, first sum 29861.38547; status: [0.0] -> build: [0.0]; out: n 405, first sum 29832.28703; status: [0.0]
### surface.cone_p1 (2 rows)
- `review2_std_bnd_neg_h_oob_icb8` (WR-738 `iso_circle`): build: [1000.0]; out: []; status: [-1002.0] -> build: [1000.0]; out: n 56, first sum 14420.82843; status: [1000.0]
- `review2_std_semi_unb_120_oob_ic9` (WR-738 `iso_circle`): build: [1000.0]; out: []; status: [-1002.0] -> build: [1000.0]; out: []; status: [1001.0]
### blend.fillet_solver_p4 (2 rows)
- `gen0189` (WR-738 `point_values`): fillet_point_step: [1e-05]; offsets: [-63589.196620918556,63589.196620918556]; point: n 80, first sum -5197450.253 -> fillet_point_step: [1e-05]; offsets: [-63589.196620918556,63589.196620918556]; point: n 80, first sum -5409431.087
- `gen0281` (WR-738 `point_values`): fillet_point_step: [0.001]; offsets: [123313.9716096632,123313.9716096632]; point: n 80, first sum -11003044.26 -> fillet_point_step: [0.001]; offsets: [123313.9716096632,123313.9716096632]; point: n 80, first sum -78544753.77
### surface.offset_surface (2 rows)
- `gen1268` (WR-738): out: n 154, first sum 91749.73271 -> out: n 154, first sum 91752.27761
- `gen1740` (WR-343): out: n 155, first sum 76061.50659 -> out: n 155, first sum 76067.9005
### surface.surface_point_solve_p3 (1 row)
- `matrix_1143` (WR-738 `global`): out: [1.0,1.0,0.3285420301022051,-0.24305095022573142,6.648613582037027e-10,1.0,2.0,1.0,1.0]; status: [0.0] -> out: [1.0,1.0,0.32854203010220506,-0.24305095022573123,6.648616832786346e-10,1.0,2.0,1.0,1.0]; status: [0.0]
### offset.offset_geometry_creation (2 rows)
- `gen0330` (WR-343): state: n 455, first sum 14925.90205 -> state: n 529, first sum 15801.35173
- `gen0492` (WR-343): state: n 781, first sum 15917.32169 -> state: n 781, first sum 14895.50505
@@ -0,0 +1,52 @@
# Sweeps: two fixes become the default, 3 golden rows move (2026-11-12)
- Flags retired (tangency-labs/solidcraft#14246, batch B43): `sheet_sweep_joins_profile_wires`
(WR-409) and `sweep_frame_on_planar_path_normal` (WR-407; its other half,
`cover_refuses_folded_continuation`, stays opt-in). Each WR record has an addendum of
2026-11-12. The other flags read in `p3/sweeps.rs` (batch B42) are untouched.
- `cargo run --profile goldens -p goldens -- --unit sweep.api_brep --bless` at the fix:
696 cases, changed=3, added, removed and broken 0. These are the three rows brett19's
survey lists for B43.
## WR-409: a drilled hole's open profile revolved part way is built (2 rows)
`issue1463_drilled_hole_pz_120` and `issue1463_drilled_hole_mz_120`
(`fixtures/sweep.api_brep/issue1463-inside-out-revolve.json`): the open chain of the line
(2, 0, 5)-(2, 0, 1) and the line (2, 0, 1)-(0, 0, 0), revolved 120 degrees about +z and
about -z, without caps.
| row | old | new |
|---|---|---|
| `issue1463_drilled_hole_pz_120` | status 1001 | status 0, counts [2, 6, 5] |
| `issue1463_drilled_hole_mz_120` | status 1001 | status 0, counts [2, 6, 5] |
Upstream makes each profile curve a wire of its own, so the topology sweep's copies of the
two coincident vertices at (2, 0, 1) come out coincident and its stitch refuses them
(WR-409). With the wires glued there the sheet has two faces, 6 edges and 5
vertices: the profile's three and the turned copies of the two off the axis. The full
turns of the same profile (`_pz_360`, `_mz_360`) do not move.
The new rows are checked, not only counted: the regression test
`issue_1463_drilled_hole_profile_revolves_part_way_into_a_joined_sheet` (`p3/sweeps.rs`)
builds both on `Kernel::default()` and finds them valid at Level 2 (`check_brep`), both
faces cones (the native cone class; the wall's has half-angle 0), and the face areas
within 1e-8 of Pappus' closed forms: the wall 2 pi / 3 * 2 * 4 = 16 pi / 3 and the cone,
slant sqrt 5 about mean radius 1, 2 pi sqrt 5 / 3. Measured at accuracy 1e-9, both
axes: 16.755160819285 against 16.755160819146 and 4.683209820733 against 4.683209820694,
8.3e-12 relative each.
## WR-407: a curve sweep along a planar path moves by roundoff (1 row)
`box_face1_curve_path_cap` (`fixtures/sweep.api_brep/box-face-curve-sweep.json`): face 1
of the box 2 x 3 x 4 swept, capped, along the cubic through (0, 0, 4), (0, 0, 6),
(1, 0, 8), (3, 0, 9), which lies in the plane y = 0. Its start tangent is +z, so
upstream's `MakeUnitOrthoVectors` frame and the frame on the plane's normal differ, and the
path and profile are turned differently before the sweep.
| row | old | new |
|---|---|---|
| `box_face1_curve_path_cap` volume | 30.2282600710325 | 30.228259877867142 |
The volume moves by 1.9e-7 (6.4e-9 relative); counts, face groups and the volume status
stay. The sweep approximates its side faces, so the volume depends on the frame's
rounding; neither value is closed form.
@@ -0,0 +1,37 @@
# gen0030: a side tilted into its round is built, not refused (2026-11-12)
Evidence for the one `topology.brep_local_operation` golden row that tangency-labs/solidcraft#13718
moves. It does not justify other rows.
## The case
`gen0030` reads `upstream/TestFiles/pt_TestFiles/box_with_large_fillet.smb` (a 10 mm box whose
edge at x = z = 10 is rounded r 8 about the axis x = z = 2), without rebuilding its trims, and
replaces three faces with planes (`p + offset·n`, normal `n + tilt`, from each face's middle):
- face 0 (z = 0, n = (0, 0, −1)): offset 0, no tilt (its own plane);
- face 4 (y = 10, p = (5, 10, 5), n = (0, 1, 0)): offset `-0.06811719631876167`, tilt
`[0.08793954552342825,0.07961790888854042,-0.2941607901132638]`;
- face 3 (x = 10, p = (10, 5, 5), n = (1, 0, 0)): offset `-0.4721612964504289`, tilt
`[-0.06661980589432345,-0.1406187786332792,-0.27482081400878505]`.
Face 3 is tangent to the round along z = 2. Moved in and tilted, its plane cuts the r 8 cylinder:
the side and the round cross along their old tangent edge.
## Before and after
- Before (main 9e805b7bdc): refused, `LoopGapUncertified { face: 3 }`, the input put back.
- After: built, 7 faces, 15 edges, 10 vertices (as the input). The check at level 2 reports
nothing, the body is a manifold solid, and its volume is certified in [795.35, 796.67].
## Independent checks
- **Volume.** The body is the region x ≥ 0, y ≥ 0, z ∈ [0, 10] on the inner side of the two new
planes, and inside the cylinder (x − 2)² + (z − 2)² ≤ 64 wherever x > 2 and z > 2. Integrating
its exact y-extent over an (x, z) midpoint grid (no kernel value enters) gives 796.0148,
796.0085, 796.0068 and 796.0084 at 1000², 2000², 4000² and 8000² cells: 796.008 ± 0.003, inside
the certified enclosure.
- **Vertices.** Every vertex lies within 9.3e-7 of each of its three faces' new surfaces (point
drops onto the planes and the cylinder); vertex tolerances are 1e-5 to 1.07e-5, except the
vertex (2, 0, 10), where the round meets the top on the unmoved side y = 0, which keeps the
input's 2.37e-3.
@@ -0,0 +1,8 @@
Updates: WR-343
#### WR-343 (update, 2026-11-12). `revolution_offset_axis_both_sides` is removed
`SurfOfRevolution::create_offset_surface` intersects the offset generator with an axis segment that spans the offset's whole extent along the axis, on both sides of the frame's origin, trims only at a crossing inside the interval, and keeps the part on the generatrix's side. This is now the only path, and the flag is gone. SolidCraft and the kernel rig already ran with it on. Removed: the field, SolidCraft's preset line, its entry in `fixes.json` and every test setting of it.
- **Results:** the goldens rows that move, with their old and new values and why the new ones are right, are in `plan/findings/surface.surface_approximate--b47-five-flags-retired-2026-11-12.md`.
- **Tests:** `issue_973_offset_cut_at_axis_below_origin` (`surface/surf_of_revolution/p2.rs`) runs on the default kernel; its half that pinned upstream's stretch past the axis is removed.
@@ -0,0 +1,9 @@
Updates: WR-407
#### WR-407 (update, 2026-11-12). `sweep_frame_on_planar_path_normal` is removed
The sweep-frame half of WR-407 is the default and its flag is gone (tangency-labs/solidcraft#14246, batch B43, which retires `sweep_frame_on_planar_path_normal` and `sheet_sweep_joins_profile_wires`). `prepare_curve_sweep` (`primitive/primitive_creation/p3/geometry.rs`) always starts a planar path's frame with its z on the plane's normal. The other half, `cover_refuses_folded_continuation`, stays opt-in.
- **Tests:** `issue_434_planar_path_frame_keeps_the_plane_normal_as_the_frame_z` runs on `Kernel::default()`; `issue_434_default_keeps_the_upstream_sweep_frame` is removed. `tests/wr_660_planar_path_pose.rs` no longer switches the frame: both rigid-motion tests hold the bounding boxes to 1e-5, the bound of the removed test that switched it on (on main's default kernel they already met it). Fixture `434-two-overlapping-swept-arc-walls` turns on only `cover_refuses_folded_continuation`.
- **Results:** the goldens rows B43 moves, with their old and new values:
`plan/findings/sweep.api_brep--b43-two-flags-retired-2026-11-12.md`.
@@ -0,0 +1,9 @@
Updates: WR-409
#### WR-409 (update, 2026-11-12). `sheet_sweep_joins_profile_wires` is removed
The fix is the default and its flag is gone (tangency-labs/solidcraft#14246, batch B43, which retires `sheet_sweep_joins_profile_wires` and `sweep_frame_on_planar_path_normal`). `join_profile_wire_ends` (`primitive/primitive_creation/p3/sweeps.rs`) always glues an open profile's wire ends that meet before a partial revolve or a repeated linear sweep.
- **Tests:** `issue_1113_open_chain_revolves_into_a_joined_sheet` runs on `Kernel::default()` (two faces, 6 vertices, 7 edges, the faces' areas 14 pi and 16 pi to 1e-8). New: `issue_1463_drilled_hole_profile_revolves_part_way_into_a_joined_sheet` (same file), the goldens cases `issue1463_drilled_hole_{pz,mz}_120`: valid at Level 2, 2 faces, 6 edges, 5 vertices, both faces cones, areas 16 pi / 3 and 2 pi sqrt 5 / 3 to 1e-8.
- **Results:** the goldens rows B43 moves, with their old and new values:
`plan/findings/sweep.api_brep--b43-two-flags-retired-2026-11-12.md`.
@@ -0,0 +1,8 @@
Updates: WR-573
#### WR-573 (update, 2026-11-12). `approximate_surface_bezier_starter` is removed
`Surface::approximate_surface` raises the starter's inner knots to full multiplicity (exact knot insertion) before each round of cubic Bezier patches, and a patch that runs past the control net is `SM_ERR`. This is now the only path, and the flag is gone, together with WR-726's `approximate_surface_checks_every_row` (the two work together: with this fix alone a pass is accepted when only its last row is within the tolerance). SolidCraft and the kernel rig already ran with it on. Removed: the field, SolidCraft's preset line, its entry in `fixes.json` and every test setting of it.
- **Results:** the goldens rows that move, with their old and new values and why the new ones are right, are in `plan/findings/surface.surface_approximate--b47-five-flags-retired-2026-11-12.md`.
- **Tests:** `surface::surface::approximate::tests::wr573_default_gives_success_without_surface` and `tests/wr573_offset_wave_export.rs`'s `without_the_flag_the_writer_succeeds_with_nothing_wr573`, which pinned the defect, are removed. `wr573_bezier_starter_approximates_the_offset_wave` and `the_flag_approximates_the_offset_face` pinned the pass that only the last-row check accepts (1.62e-3 for 1e-3; 33 x 4 at 1.12e-5 for 5e-6, the C++ with eae96cb); they are removed, and the WR-726 tests on the default kernel check that a surface comes back, within the tolerance.
@@ -0,0 +1,8 @@
Updates: WR-61
#### WR-61 (update, 2026-11-12). `extrusion_step_uses_clamped_uv` is removed
`SurfOfExtrusion::evaluate_step` and `evaluate_step_point` evaluate the STEP parameters clamped to the analytic UV domain when out-of-bounds evaluation is off, the copy upstream clamps and then ignores. This is now the only path, and the flag is gone. SolidCraft and the kernel rig already ran with it on. Removed: the field, SolidCraft's preset line, its entry in `fixes.json` and every test setting of it.
- **Results:** the goldens rows that move, with their old and new values and why the new ones are right, are in `plan/findings/surface.surface_approximate--b47-five-flags-retired-2026-11-12.md`.
- **Tests:** in `surface/surf_of_extrusion/p1.rs`, `step_clamped_uv_fix_is_off_by_default` becomes `step_evaluators_clamp_to_the_surface_domain` (the clamped values, with its matrix and error checks kept), `step_clamped_uv_fix_clamps_when_enabled_and_out_of_bounds_off` becomes `step_clamp_reads_the_analytic_domain_with_out_of_bounds_off` (its comparison of the flag off and on becomes the line's exact values in the domain), and `step_clamped_uv_fix_keeps_the_generator_flag_policy` becomes `step_clamp_overrides_the_generator_flag`.
@@ -0,0 +1,8 @@
Updates: WR-726
#### WR-726 (update, 2026-11-12). `approximate_surface_checks_every_row` is removed
`Surface::approximate_surface` accepts a pass of patches only when every row (and column) of it is within the tolerance, subdivides every failed row, and fails with `SM_ERR` when the last allowed pass is still outside it. This is now the only path, and the flag is gone, together with WR-573's `approximate_surface_bezier_starter`. SolidCraft already ran with it on; the kernel rig's `fixes.json` did not list it. Removed: the field, SolidCraft's preset line and every test setting of it.
- **Results:** the goldens case `surface.surface_approximate/gen0381`, listed known failing for this record (success with an achieved tolerance of 0.546 for a requested 1.38e-3), leaves `known_failing.json`. It and the other rows that move, with their old and new values and why the new ones are right, are in `plan/findings/surface.surface_approximate--b47-five-flags-retired-2026-11-12.md`.
- **Tests:** `wr726_every_row_checked_keeps_the_fit_within_the_tolerance`, `wr726_every_row_checked_refuses_when_the_last_subdivision_misses` and `with_every_row_checked_the_fit_is_within_the_tolerance_2507` run on the default kernel, with the values this record gives (13 x 4 at 9.18e-4; 34 x 4 at 4.03e-6).
@@ -0,0 +1,8 @@
Updates: WR-738
#### WR-738 (update, 2026-11-12). `revolution_frame_from_profile_origin` is removed
`SurfOfRevolution::create_canonical` takes the frame's X axis from the profile's point at parameter 0 when its STEP interval holds it (then its start, then its 45% point, each when the one before is on the axis, by its distance from the axis), and `step_inversion` turns a point of a planar generator onto both half planes of the start plane. This is now the only path, and the flag is gone. SolidCraft already ran with it on; the kernel rig's `fixes.json` did not list it. Removed: the field, SolidCraft's preset line and every test setting of it.
- **Results:** the goldens rows that move, with their old and new values and why the new ones are right, are in `plan/findings/surface.surface_approximate--b47-five-flags-retired-2026-11-12.md`.
- **Tests:** in `surface/surf_of_revolution/p1.rs` the tests of #2628 items 4 and 7 to 9 run on the default kernel; their halves that pinned upstream's frame or refusal are removed, and `bounded_profile_frame_is_upstreams_with_the_fix` becomes `bounded_profile_frame_is_upstreams` (the frame's axes, exact).
@@ -0,0 +1,19 @@
#### WR-777. LocalOperation turns a full cylinder given to a round inside out
- **Severity:** Wrong result
- **Area:** Offset
- **Location:** `source/SMLib/src/SmBrep.cpp:11982`, `SmBrep::LocalOperation` (def. `:11945`).
- **What happens:** each new surface is turned to face the way the face's old surface did by
comparing normals (`sOldNorm.Dot(sNewNorm) < 0.0` reverses it, `:11991`). The new normal is
read at the middle of the new surface's own UV domain, the old one at the middle of the
face's UV domain. For a full cylinder given to a round, the middle of the cylinder's domain is
across the axis from the round, where the outward normal points the other way: the normals
"oppose" though both surfaces face out, the cylinder is reversed, and the new round faces
into the body. Its outer loop runs against its surface (`SmFace` check "Loop Orientation"
where the face keeps UV trims), and with exact mate curves the body's volume comes out
negative.
- **Trigger:** a 10 mm cube whose vertical edge at x = y = 10 is rounded r 2, the round given
the coaxial cylinder r 2.3 (`create_cylinder_surface` about (8, 8)): the cylinder's domain
middle is at (5.7, 8), normal (−1, 0, 0); the round's middle normal is (0.71, 0.71, 0).
- **How found:** read during porting (tangency-labs/solidcraft#13718)
- **Confirmed:** no (no C++ run of the case; solidcore reads the new normal where the new
surface passes nearest the old face's middle, `topology/brep/local_operation.rs`)
+1 -1
View File
@@ -7,4 +7,4 @@
# solidcore-ops or solidcore-tess (solidcore docs/layers.md). `cargo xtask layers --paths [crate]`
# lists the paths counted.
crates/geom 20
crates/smlib 736
crates/smlib 727