Skip to content

GeomConvert_ApproxSurface at GeomAbs_C0 returns a degree-1 collapse while reporting IsDone and a maxError five orders of magnitude too small #522

Description

@gsdali

Found while building the parity tests for #491 (which unified the two GeomConvert_ApproxSurface
wrappers). Not a duplication finding and not caused by #491's change — both wrappers hit it
identically, before and after, because both call the same OCCT class. Filed separately so #491
stays a refactor.

What happens

GeomConvert_ApproxSurface asked for GeomAbs_C0 continuity can return a surface that is nowhere
near its input while reporting IsDone() == true and a MaxError() that describes something other
than the surface it hands back.

Reachable from the public Swift API through both approximation entry points:

let sphere = Surface.sphere(center: .zero, radius: 10)!

// continuity: 0 == C0
let fit = sphere.approximated(tolerance: 1e-3, continuity: 0)!
// fit.uDegree == 1, fit.uPoleCount == 2  -- a straight line across the full 2*pi of longitude
// sampled deviation from `sphere`: 19.9999  (the sphere's own diameter)

let detailed = sphere.approxWithDetails(tolerance: 1e-3, uContinuity: .c0, vContinuity: .c0)
// detailed.isDone   == true
// detailed.maxError == 0.000106971   <- describes neither the input nor the returned surface

The reported error is not a rounding-level disagreement: it is off by five orders of magnitude,
and isDone says the tolerance was met.

Measured shape of the defect

Sweep of 98 requests (7 surface families x all 9 (uContinuity, vContinuity) combinations of
C0/C1/C2 at tolerance 1e-3, plus C0/C0 across five tolerances), comparing OCCT's reported
MaxError() against the real maximum deviation over a 21x21 grid of the source domain.
12 of 98 misreport (real deviation more than 10x the reported error). Every one of them requests
C0 in at least one direction:

surface uCont vCont reported maxError real deviation result
sphere r=10 C0 C0 1.07e-4 19.9999 degree 1x7, 2x8 poles
sphere r=10 C1 C0 1.60e-4 19.9999 degree 3x8, 4x9 poles
sphere r=10 C2 C0 1.87e-4 7.66293 degree 5x8, 6x9 poles
bezier 4x4 (bicubic) C0 C0 4.08e-15 0.13824 degree 1x1, 2x2 poles

The same sphere at uCont = C0, vCont = C1 is fine (reported 3.83e-5, real 1.44e-5), as is every
C1/C2 combination — so it is the C0 request itself, not the surface, that triggers it.

Two further observations:

  • The fit collapses to degree 1 in a direction where degree 1 cannot represent the input. The
    sphere's U is the full [0, 2*pi] of longitude; the C0 fit returns 2 poles at degree 1 across it.
    A cylinder trimmed in V legitimately gets vDegree = 1 (it is linear in V) and reports
    correctly, so degree collapse per se is not the bug — collapsing where the input is not linear is.
  • At C0/C0 the request's tolerance stops mattering. The bicubic Bezier returns the identical
    2x2 bilinear patch with the identical 4.08e-15 reported error at every tolerance from 1e-1 down
    to 1e-7. Tightening the tolerance changes nothing.

A full sphere is affected while a sphere trimmed in V to [-1, 1] is not, which suggests the
apex/pole rows of the V parameterisation are involved.

Where to look

Not root-caused — this is the repro, not the diagnosis. GeomConvert_ApproxSurface::Approximate
(Libraries/occt-src/src/ModelingData/TKGeomBase/GeomConvert/GeomConvert_ApproxSurface.cxx:345-421)
forwards the continuity straight into AdvApp2Var_ApproxAFunc2Var as theUContinuity/
theVContinuity, which become AdvApp2Var_Context's iu/iv constraint orders. Those feed
lesparam (AdvApp2Var_Context.cxx:22-76), which derives the Jacobi degree and the initial
per-axis sample count, and hMaxFactor (AdvApp2Var_Context.cxx:104), which special-cases order 0
and -1. myMaxError is read back from approx.MaxError(3, 1) — i.e. from
AdvApp2Var_ApproxAFunc2Var::my3DMaxError, computed per patch during the iso-curve fit
(AdvApp2Var_ApproxAFunc2Var.cxx:845-865) rather than measured against the surface finally built by
ConvertBS. That gap is the likely reason a degree-1 collapse can still report a tiny error, but it
has not been confirmed.

Worth checking whether the reported error is wrong, the returned surface is wrong, or both, before
choosing a fix — and whether a kernel patch or a bridge-side rejection of the C0 request is the
right shape (this project carries patches 0001-0017, see Scripts/patches/).

Reproducer

/private/tmp scratch copies used during the #491 investigation:
occt_491_c0.mm (minimal: sphere at C0, prints fit domain, degrees, poles, reported vs real error)
and occt_491_c0_sweep.mm (the 98-case sweep tabulated above). Both compile with the standard
ground-truth invocation from CLAUDE.md. They should be promoted into
Scripts/repro/<this issue>-approx-c0-collapse/ when this is picked up.

Interaction with #491

#491's parity tests keep C0 in their request set — both entry points must return the same
surface at C0, and after #491 they do. But
Tests/OCCTSurfaceTests/Issue491SurfaceApproxParityTests.swift's
maxErrorDescribesTheSharedFit test has to skip the affected C0 requests, because asserting
"sampled deviation <= reported maxError" fails on OCCT's own numbers there. That skip carries a
comment pointing at this issue and should be removed when this is fixed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    phase:1bPass 1b — C++ bridge header duplication audit (#381)refactorPart of the #377 codebase/docs duplication-audit efforttype:bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions