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.
Found while building the parity tests for #491 (which unified the two
GeomConvert_ApproxSurfacewrappers). 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_ApproxSurfaceasked forGeomAbs_C0continuity can return a surface that is nowherenear its input while reporting
IsDone() == trueand aMaxError()that describes something otherthan the surface it hands back.
Reachable from the public Swift API through both approximation entry points:
The reported error is not a rounding-level disagreement: it is off by five orders of magnitude,
and
isDonesays 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:
The same sphere at
uCont = C0, vCont = C1is fine (reported 3.83e-5, real 1.44e-5), as is everyC1/C2 combination — so it is the C0 request itself, not the surface, that triggers it.
Two further observations:
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 reportscorrectly, so degree collapse per se is not the bug — collapsing where the input is not linear is.
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 theapex/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_ApproxAFunc2VarastheUContinuity/theVContinuity, which becomeAdvApp2Var_Context'siu/ivconstraint orders. Those feedlesparam(AdvApp2Var_Context.cxx:22-76), which derives the Jacobi degree and the initialper-axis sample count, and
hMaxFactor(AdvApp2Var_Context.cxx:104), which special-cases order 0and -1.
myMaxErroris read back fromapprox.MaxError(3, 1)— i.e. fromAdvApp2Var_ApproxAFunc2Var::my3DMaxError, computed per patch during the iso-curve fit(
AdvApp2Var_ApproxAFunc2Var.cxx:845-865) rather than measured against the surface finally built byConvertBS. That gap is the likely reason a degree-1 collapse can still report a tiny error, but ithas 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, seeScripts/patches/).Reproducer
/private/tmpscratch 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 standardground-truth invocation from
CLAUDE.md. They should be promoted intoScripts/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'smaxErrorDescribesTheSharedFittest 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.