GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||
GeezerVison is showing github.com — NURBS: Correct working with planar surfaces by ruevs · Pull Request #1812 · solvespace/solvespace Conversation
|
| Surface | Planar? | Affine UV → XYZ? |
|---|---|---|
P(u,v) = (u, v, 0) |
Yes | Yes |
P(u,v) = (u, (1+u)*v, 0) |
Yes | No |
P(u,v) = (u, v, u*v) |
No—a saddle | No |
The usual built-in construction paths largely made the old assumption safe: SSurface::FromPlane() constructs an unweighted parallelogram, and extruding an ordinary line produces one too. That is a property of those constructors, however, not of the general SSurface representation.
There is also the converse: a degree-(2,2) or (3,3) patch can be perfectly planar.
Rational weights add another distinction: even a planar parallelogram control net generally has a nonlinear parameterization when its weights differ.
Both are implemented in src/srf/surface.cpp, with declarations in src/srf/surface.h.
IsPlanar()This asks:
Does the complete control net lie in one plane?
It finds three non-collinear control points defining a plane, then checks every control point against it. It works independently of the surface degree. Coplanar controls also keep a rational surface in that plane, provided its denominator is defined.
The recognition tolerance is roundoff-sized, capped at LENGTH_EPS / 100, rather than the ordinary modeling tolerance. Thus a slightly warped patch is not deliberately treated as flat at LENGTH_EPS.
It does not modify controls or weights. Collapsed or entirely collinear nets return false because they do not define a usable surface plane.
IsAffine()This asks the more restrictive question:
Can this patch use the simple affine UV-coordinate formulas?
Currently it requires:
(1,1).For a non-degenerate patch, this gives:
P(u,v) = origin + u*A + v*B
A and B need not be perpendicular.
One naming caveat worth making explicit during review: this implementation recognizes the bilinear affine fast path, not every possible higher-degree representation of an affine map. A false result simply sends the calculation through the general path.
src/srf/ratpoly.cpp: XYZ → UV inversionSSurface::ClosestPointTo() now uses its closed-form inverse only when IsAffine() is true. Other parameterizations continue through the existing numerical inverse.
The previous parallelogram check:
There is also an independent skew-axis correction here. The old calculation mixed the original parameter directions with denominators derived from perpendicularized directions. That worked for orthogonal axes but gave wrong coordinates for skew axes.
The new calculation consistently uses the dual directions—equivalent to solving the two-coordinate system for non-orthogonal basis vectors.
src/srf/raycast.cpp: line–surface intersectionsThe analytic plane-intersection path now requires IsPlanar(), rather than degree (1,1).
Consequently:
ClosestPointTo().src/srf/surfinter.cpp: surface intersections and coincidenceThere are several related changes:
CoincidentWithPlane() checks the entire control net, not just four corner controls.That last function retains the existing LENGTH_EPS coincidence tolerance. That is a different question from the stricter planarity recognition performed by IsPlanar().
src/srf/surface.cpp and src/srf/triangulate.cpp: triangulationPreviously, degree (1,1) selected the plane-style triangulation shortcuts.
Now:
Otherwise a saddle with four boundary corners could be displayed as essentially two flat triangles, without resolving its interior curvature.
This changes tessellation of the surface, not the underlying native surface.
src/srf/merge.cpp: merging coincident planesThe merger remains a plane merger; it is not a general curved-surface merger.
It now admits higher-degree or rational planar patches and excludes warped bilinear ones.
There is one deliberate reparameterization here: after merging planar regions, the existing algorithm constructs a new rectangular supporting plane. Because the input can now have higher degree or unequal weights, the code must explicitly reset:
degm = degn = 1; weight[...] = 1;
before writing the four new controls. Leaving the old degree or weights would make those four assignments describe the wrong surface.
This is an exact replacement of a planar supporting surface during the normal merge operation—not flattening curved geometry or rewriting the cached STEP source.
src/srf/boolean.cpp: classification probes on straight trimsThis is another substantive correction, not merely a predicate replacement.
A straight XYZ trim can have a curved UV image on a non-affine plane. Therefore:
PointAt(average(endpoint UVs))
need not lie on that straight trim.
The old code refined the probe onto an exact curve only when the curve’s degree was not one. It now refines onto every exact trim, including degree-one lines.
Without that change, Boolean classification could test a point lying off the other incident face and make an incorrect keep/discard decision. The rational/non-affine-plane cube-union regression exposed precisely that failure.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
This was discovered while vibe-coding STEP linking. However it is applicable in general. In many places the code checks if a `SSurface` is a lane by checking if `srf->degm == 1 && srf->degn == 1`. Currently a surface that can be created within SoveSpace will always be planar when `degm == 1 && degn == 1`. However in general (in this would be the case when coming from a linked STEP file) this is not the case - the surface can be e.g. a "saddle". Therefore two functions `IsPlanar` and `IsAffine` are introduced and replace `srf->degm == 1 && srf->degn == 1`. The above change should have no impact for the current NURBS operations, however there is a real fix in ratpoly.cpp that will apply even now - see below. Below is the detailed explanation that GPT-6.1 Sol max effort provided. I have read the explanation an the code carefully and agree. **Yes—your interpretation is broadly correct.** But there are two separate assumptions being corrected: 1. **Degree `(1,1)` does not necessarily mean planar.** 2. **Planar does not necessarily mean an affine mapping between UV and XYZ.** I agree that these are native geometry-kernel changes and belong in a separate prerequisite commit. I have not changed the commits or working tree. ## 1. Degree, planarity and parameterization For an unweighted degree-`(1,1)` patch, the surface can be written as: ```text P(u,v) = P00 + u*A + v*B + u*v*C ``` The mixed `u*v` term is the important part. Degree one **in each parameter** is not the same as being jointly linear. Examples: | Surface | Planar? | Affine UV → XYZ? | |---|---:|---:| | `P(u,v) = (u, v, 0)` | Yes | Yes | | `P(u,v) = (u, (1+u)*v, 0)` | Yes | No | | `P(u,v) = (u, v, u*v)` | No—a saddle | No | The usual built-in construction paths largely made the old assumption safe: `SSurface::FromPlane()` constructs an unweighted parallelogram, and extruding an ordinary line produces one too. That is a property of those **constructors**, however, not of the general `SSurface` representation. There is also the converse: a degree-`(2,2)` or `(3,3)` patch can be perfectly planar. Rational weights add another distinction: even a planar parallelogram control net generally has a nonlinear parameterization when its weights differ. ## 2. What the two predicates mean Both are implemented in `src/srf/surface.cpp`, with declarations in `src/srf/surface.h`. ### `IsPlanar()` This asks: > Does the complete control net lie in one plane? It finds three non-collinear control points defining a plane, then checks **every** control point against it. It works independently of the surface degree. Coplanar controls also keep a rational surface in that plane, provided its denominator is defined. The recognition tolerance is roundoff-sized, capped at `LENGTH_EPS / 100`, rather than the ordinary modeling tolerance. Thus a slightly warped patch is not deliberately treated as flat at `LENGTH_EPS`. It does not modify controls or weights. Collapsed or entirely collinear nets return false because they do not define a usable surface plane. ### `IsAffine()` This asks the more restrictive question: > Can this patch use the simple affine UV-coordinate formulas? Currently it requires: - Degree `(1,1)`. - All four weights equal—not necessarily equal to one. - An exact parallelogram control net. For a non-degenerate patch, this gives: ```text P(u,v) = origin + u*A + v*B ``` `A` and `B` need not be perpendicular. One naming caveat worth making explicit during review: this implementation recognizes the **bilinear affine fast path**, not every possible higher-degree representation of an affine map. A false result simply sends the calculation through the general path. ## 3. Changes to the algorithms ### `src/srf/ratpoly.cpp`: XYZ → UV inversion `SSurface::ClosestPointTo()` now uses its closed-form inverse only when `IsAffine()` is true. Other parameterizations continue through the existing numerical inverse. The previous parallelogram check: - Ignored rational weights. - Used approximate point equality, potentially overlooking a small mixed term. There is also an **independent skew-axis correction** here. The old calculation mixed the original parameter directions with denominators derived from perpendicularized directions. That worked for orthogonal axes but gave wrong coordinates for skew axes. The new calculation consistently uses the dual directions—equivalent to solving the two-coordinate system for non-orthogonal basis vectors. ### `src/srf/raycast.cpp`: line–surface intersections The analytic plane-intersection path now requires `IsPlanar()`, rather than degree `(1,1)`. Consequently: - A bilinear saddle no longer gets intersected as though it were a plane. - A genuinely planar higher-degree patch can use the plane intersection. - Finding the intersection’s UV coordinates still respects its actual parameterization through `ClosestPointTo()`. ### `src/srf/surfinter.cpp`: surface intersections and coincidence There are several related changes: - Plane–plane and plane–extrusion special cases now recognize **actual planes**. - Plane–plane intersection produces a straight XYZ line, but clipping that line using linear UV equations is valid only for an affine patch. - For a non-affine plane, the line is instead clipped conservatively against the XYZ control-point bounding box. The existing trim/split processing removes the excess. No linear UV map is invented. - The affine clipping formulas also receive the skew-axis correction. - Plane coincidence and plane-containing-curve tests now accept planar patches of higher degree. - `CoincidentWithPlane()` checks the entire control net, not just four corner controls. That last function retains the existing `LENGTH_EPS` **coincidence tolerance**. That is a different question from the stricter planarity recognition performed by `IsPlanar()`. ### `src/srf/surface.cpp` and `src/srf/triangulate.cpp`: triangulation Previously, degree `(1,1)` selected the plane-style triangulation shortcuts. Now: - Actual planes use the planar triangulation path. - Nonplanar bilinear patches use the curvature-resolving grid path. - Triangle-fan and ear-selection shortcuts are likewise guarded by actual planarity. Otherwise a saddle with four boundary corners could be displayed as essentially two flat triangles, without resolving its interior curvature. This changes tessellation of the surface, not the underlying native surface. ### `src/srf/merge.cpp`: merging coincident planes The merger remains a **plane merger**; it is not a general curved-surface merger. It now admits higher-degree or rational planar patches and excludes warped bilinear ones. There is one deliberate reparameterization here: after merging planar regions, the existing algorithm constructs a new rectangular supporting plane. Because the input can now have higher degree or unequal weights, the code must explicitly reset: ```cpp degm = degn = 1; weight[...] = 1; ``` before writing the four new controls. Leaving the old degree or weights would make those four assignments describe the wrong surface. This is an exact replacement of a planar supporting surface during the normal merge operation—not flattening curved geometry or rewriting the cached STEP source. ### `src/srf/boolean.cpp`: classification probes on straight trims This is another substantive correction, not merely a predicate replacement. A straight XYZ trim can have a **curved UV image** on a non-affine plane. Therefore: ```text PointAt(average(endpoint UVs)) ``` need not lie on that straight trim. The old code refined the probe onto an exact curve only when the curve’s degree was not one. It now refines onto **every exact trim**, including degree-one lines. Without that change, Boolean classification could test a point lying off the other incident face and make an incorrect keep/discard decision. The rational/non-affine-plane cube-union regression exposed precisely that failure. ## 4. Commit separation I would split the work as follows: 1. **Native surface-kernel corrections** - All eight changed files under `src/srf`. - The six added kernel regression cases in `test/core/raycast/test.cpp`. - No STEP dependency or import option required. 2. **Generic native STEP spline linking** - Exact decomposition and trimming. - Importer/provenance/reference changes. - STEP integration tests and documentation. The STEP-driven Boolean regression can remain in the second commit; the focused straight-trim probe regression already exercises the kernel correction without STEP. So yes: **the first commit makes the existing native representation work correctly for a broader set of valid inputs; the second makes STEP linking supply those inputs.** That is a cleaner dependency and review boundary than the current combined commit.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I like the original comment better. The logic should function either way, but checking if exact.deg != 1 ensures we don't waste time refining a segment that is actually straight. That means almost every straight edge will take the longer refining path needlessly. The new comment says "A UV chord need not map onto the trim" but it does for degree 1 curves.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I think the degree check should be kept so we can avoid callingl the more complex check unless needed. This is an optimization and also provide an exact calculation rather than an itterative one. The degree check is part of the affine check but we call this a LOT. Maybe a performance test with lots of flat surfaces is in order?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
While technically correct, again the point is a performance boost by using a direct calculation for planar surfaces. While the check of degree is technically wrong, the IsPlanar check does an awful lot of work. Maybe in this place (in AllPointsIntersecting()) we should check for degree 1x1 AND something simple, like verify that a plane defined by 3 points also contains the 4th? And leave higher degree surfaces that happen to be planer to the full test?
BTW when you do a lathe operation and it has a flat top or botton, we convert those to degree 1x1 right after creating them. If not they would be planar but degree 1x2.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I don't know why this big block of code is here inside an IsAffine() test and the comment isn't helping me. I looks like the test is another perfomance optimization with an early out. Is that what it is?
The rest of this looks like more technically correct tests that could slow things down.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Ugh. I thought I was commenting on specific code sections. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
"Without that change, Boolean classification could test a point lying off the other incident face and make an incorrect keep/discard decision. The rational/non-affine-plane cube-union regression exposed precisely that failure." So part of this was in response to a boolean failure? My main concern is using an overly complicate IsPlanar() check for speedups. And also that the check is testing way more than solvespace needs. On the other hand, if you're going to be linking STEP files who knows what's in those. I always thought we should implement enough STEP to link our own output step files. I hope you're thinking along those lines because STEP is huge and handles NURBS up to something like degree 11, with knots and everything. @ruevs BTW now I see what I've been doing to you! ;-) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
Also, that's a really really big commit message, and I don't like the language GPT uses ;-b |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
@phkahler I agree with you about the performance - as soon as I saw the new functions that was my first thought - two integer comparisons replaced by all this floating point math... But the new logic is correct - that is why I opened the PR early and requested your review - without mentioning any of this - to not bias you :-) Now to the point - the LLM is good at this so I asked it to read your review comments and think about it. Here is the result: Next I'll ask it to implement it's own suggestions. I agree with phkahler’s overall direction, but I would optimize the implementation rather than undo the correctness fixes. The PR mixes two things: removing invalid geometric assumptions, and recognizing more kinds of planar surfaces. The latter need not make every common kernel operation more expensive. I read all four comments and traced the affected paths. Here is my assessment. 1.
|
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Specialize bilinear planarity, inline the exact affine predicate, and use the direct dual-basis inverse. Seed rounded/non-affine bilinear inverses on the original patch without weakening affinity or flattening curvature. Project straight Boolean probes analytically, including rational lines. Reuse operation-local plane classifications and equations in intersection, merging, coincidence and curve-containment loops. Reuse existing XYZ boxes for conservative non-affine clipping, and pass the known planar/curved classification through polygon and contour triangulation. Keep section checks unrolled for bilinears and complete for higher degrees. Try native cylinders before full higher-degree ray-plane recognition, retaining direct intersection of arbitrary higher-degree planes. Keep robust classification for collapsed or ill-conditioned corner bases. Extend the kernel regressions for rounded transforms, rational straight trims, degenerate corners, reused plane normals, non-affine clipping, higher-degree ray intersections and planar merging. Validation: 312 cases / 1867 checks in Release and Debug with OpenMP, using shuffle seeds 1 and 2026. Same-machine Release load/regenerate benchmarks on three unchanged sketches improved about 3%, 16% and 23% against 0c63d8c.
AssistantCommitted The change covers every original replacement, not just the reviewed locations:
Validation: Release and Debug/OpenMP both passed 312 cases / 1,867 checks, with shuffle seeds Same-machine Release load/regenerate benchmarks versus
Tracked working tree is clean. STEP work, fixtures, |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
It went to town a bit.... I'm going to bed :-) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There is some confusion here. In some CAD software they do have trim curves as Beziers or maybe even rational ones in UV space. In solvespace ALL trims are in XYZ model space. The exact flag means the curve is exact in XYZ. It also means it follows exactly along the surface. A curve in XYZ may be a straight line in UV, thats what curved surfaces do. So I'm not sure why it cares that a straight line in XYZ might not be straight in UV, the XYZ one is the actual exact trim curve. All of our curves have a PWL representation, but the ones tagged exact also have an exact XYZ rational bezier representation. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
@phkahler the model replied:Thinking: He is correct about how SolveSpace stores trims. My earlier explanation should have pointed directly at the probe calculation—not suggested that SolveSpace stores UV trim curves. The distinction is: the stored trim is exact, but the initially constructed probe is not necessarily on it. In Point2d muv = (auv.Plus(buv)).ScaledBy(0.5);
*pt = PointAt(muv);That point comes from evaluating the surface at an averaged UV coordinate, not from evaluating The call chain confirms this:
The example satisfies his definition of “exact”For the existing regression’s non-rational variant: Every point of that XYZ line lies exactly on the surface: choose Nevertheless: The probe misses the exact trim—and an incident face at So his description and the fix are consistent. The updated code projects this computed probe onto the actual exact XYZ line, then recovers its UV coordinates. No UV Bézier trim representation is involved or needed. I would respond by showing those two lines of No code changes made. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
@phkahler I (or rather the future Übermensch) have STEP import/linking mostly working locally. Yes; it will only support geometry that The detection of higher order surfaces as actual planes is not needed, but presumably the LLM added it as an extra optimisation (in the otherwise slower new function). The distinction between planar and affine This PR, the step export fix in #1816 and one more much bigger export fix (that I need to review carefully before opening a PR) all come from the work on STEP import/linking. The final goal is to be able to link in all STEP files that we can export, as well as any "external" ones that we can represent as NURBS with our The importer is intentionally very strict about the file format - both syntactically and as geometry - and rejects anything invalid or not representable by
:-D And now I'm doing it to you, and myself :-D Probably with same reasoning - STEP linking/import is "worth it" (and curiosity) :-D P.S. Electricity killed the cat. ;-) |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
@ruvs That would exactly why. Next release will have all the bigger things that people have wanted for 10 years. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.