GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — NURBS: Correct working with planar surfaces by ruevs · Pull Request #1812 · solvespace/solvespace

Conversation

@ruevs

@ruevs ruevs commented Nov 8, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

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 plane 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". In addition higher order surfaces can still be planar.

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:

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:

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:

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:

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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@ruevs
ruevs requested a review from phkahler November 8, 2026 16:03
@ruevs
ruevs force-pushed the NURBSPlanes branch from 43acd7c to 4db8337 Compare November 8, 2026 16:08
@ruevs
NURBS: Correct working with planar surfaces
0c63d8c
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.
@ruevs
ruevs force-pushed the NURBSPlanes branch from 4db8337 to 0c63d8c Compare November 8, 2026 16:12
phkahler
phkahler reviewed Nov 8, 2026
View reviewed changes
Comment thread src/srf/boolean.cpp

@phkahler phkahler Nov 8, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
phkahler
phkahler reviewed Nov 8, 2026
View reviewed changes
Comment thread src/srf/ratpoly.cpp

@phkahler phkahler Nov 8, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
phkahler
phkahler reviewed Nov 8, 2026
View reviewed changes
Comment thread src/srf/raycast.cpp

@phkahler phkahler Nov 8, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
phkahler
phkahler reviewed Nov 8, 2026
View reviewed changes
Comment thread src/srf/surfinter.cpp

@phkahler phkahler Nov 8, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for 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.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@phkahler

phkahler commented Nov 8, 2026

Copy link
Copy Markdown
Member

Ugh. I thought I was commenting on specific code sections.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@phkahler

phkahler commented Nov 8, 2026

Copy link
Copy Markdown
Member

"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! ;-)

ruevs reacted with laugh emoji
All reactions
  • 😄 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@phkahler

phkahler commented Nov 8, 2026

Copy link
Copy Markdown
Member

Also, that's a really really big commit message, and I don't like the language GPT uses ;-b

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ruevs

ruevs commented Nov 8, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member Author

@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. boolean.cpp: the cost concern is right; the degree-one argument is not generally right

The new code sends every exact straight edge through SBezier::ClosestPointTo(), which still samples and iterates for degree-one curves. That is unnecessary work on ordinary native models.

However, a straight XYZ trim does not necessarily have a straight UV image. For example, on the planar patch

S(u,v) = (u, v·(1 + u/4), 0)

the straight trim y = 0.5 has UV endpoints (0, 0.5) and (1, 0.4). Their UV midpoint maps to y = 0.50625, not 0.5. The PR’s existing straight-trim regression exercises this problem.

I would therefore not restore the unconditional exact.deg != 1 exclusion. Instead, handle exact degree-one trims with a direct XYZ line projection—or the XYZ midpoint of the current segment—then recover UV. That preserves correctness without invoking the generic curve-refinement routine.

2. ratpoly.cpp: agree, but distinguish call overhead from classification work

IsAffine() already starts with the degree check and short-circuits. Adding an outer degree check does not avoid substantial geometric work on higher-degree surfaces; it mainly avoids an out-of-line function call when the compiler cannot inline it.

Keeping that cheap guard is reasonable. Making the small predicate available for inlining could also avoid duplicated arithmetic between the predicate and the inverse calculation. The weight checks and corrected skew-axis inverse should remain.

A potentially larger performance problem is EqualsExactly(). Ordinary floating-point construction and transformation can make intended-affine native planes fail that bit-for-bit parallelogram check. They then lose the direct inverse. In particular, the mustConverge=false fallback can perform a 20 × 20 initial search.

I would measure the affine fast-path hit rate on translated and rotated native models—not just the predicate’s execution time. Simply loosening the check to LENGTH_EPS would be the wrong remedy, because that could restore the small-warp correctness bug.

3. raycast.cpp: this is where I agree most strongly

IsPlanar() currently makes three control-net passes, including finding a suitable plane and normalizing its normal. That is a substantial replacement for two integer comparisons, especially when repeated for rays against curved surfaces.

For this optional shortcut, a cheap degree-(1,1) check followed by a fourth-corner coplanarity test is sensible. Degenerate configurations should fall back conservatively, and the tolerance should remain roundoff-scale rather than model tolerance.

Higher-degree patches can take the general intersection route, provided its behavior is validated for the supported cases. We do not need to discover every possible plane during every ray query.

His lathe observation is also correct: MakeFirstOrderRevolvedSurfaces() already converts the ordinary planar lathe patches to degree (1,1).

4. surfinter.cpp: the block has a correctness purpose, but needs clearer separation

The overall clipping step is an optimization. The non-affine branch makes that optimization valid for the newly recognized planes.

The old calculation assumes:

u(t) = up + t·ud
v(t) = vp + t·vd

That holds for an affine parameterization, but not for an arbitrary rational or non-affine planar patch. Using it anyway can incorrectly shorten or eliminate a real intersection.

The added block instead bounds the XYZ intersection line conservatively; subsequent splitting and trimming remove excess portions. It is not merely another early-out.

I would retain that distinction, but:

  • Keep the ordinary affine route inexpensive.
  • Extract the conservative clipping into a clearly named helper.
  • Reuse the bounding boxes already calculated at the start of IntersectAgainst().
  • Avoid repeatedly rediscovering planarity inside loops that already know the surface is planar.

My recommendation

Make a focused performance revision around cheap common-case recognition, direct straight-line handling, and reuse of classification results. Keep the full control-net checks where actual planarity affects correctness; do not mechanically replace them everywhere with degree checks.

Then compare Release builds against the parent commit using large planar Boolean models, translated/rotated versions, ordinary curved/lathed models, and the existing rational/non-affine regressions. A global classification cache would need care because control nets are publicly mutable and some paths run in parallel.

The extra work is evident from the code, but I have not measured an end-to-end slowdown. His review identifies real optimization opportunities; it does not invalidate the underlying planarity-versus-affinity correction.

No code or branch changes made.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ruevs
NURBS: Preserve inexpensive planar surface fast paths
791d9ef
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.
@ruevs

ruevs commented Nov 8, 2026

Copy link
Copy Markdown
Member Author

Assistant

Committed 791d9efe — NURBS: Preserve inexpensive planar surface fast paths, on NURBSPlanes directly above 0c63d8ca.

The change covers every original replacement, not just the reviewed locations:

  • Cheap bilinear planarity checks and inline, strict affine recognition.
  • Better inverse initialization without relaxing correctness checks.
  • Direct straight-line Boolean probe projection.
  • Operation-local classification reuse in intersection, merging, coincidence and curve containment.
  • Classification passed through triangulation; unrolled common-case section checks.
  • Cylinder-first recognition while retaining direct higher-degree planar ray intersections.

Validation: Release and Debug/OpenMP both passed 312 cases / 1,867 checks, with shuffle seeds 1 and 2026. Existing regressions remain, with additional kernel coverage.

Same-machine Release load/regenerate benchmarks versus 0c63d8ca:

Existing sketch Improvement
Planar array 3.4%
Coplanar Booleans 15.5%
Curved tangencies 23.4%

Tracked working tree is clean. STEP work, fixtures, build/bin, and pre-existing untracked directories remain untouched. Nothing pushed.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ruevs

ruevs commented Nov 8, 2026

Copy link
Copy Markdown
Member Author

It went to town a bit.... I'm going to bed :-)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@phkahler

phkahler commented Nov 8, 2026

Copy link
Copy Markdown
Member

The new code sends every exact straight edge through SBezier::ClosestPointTo(), which still samples and iterates for degree-one curves. That is unnecessary work on ordinary native models.

However, a straight XYZ trim does not necessarily have a straight UV image. For example, on the planar patch

S(u,v) = (u, v·(1 + u/4), 0)

the straight trim y = 0.5 has UV endpoints (0, 0.5) and (1, 0.4). Their UV midpoint maps to y = 0.50625, not 0.5. The PR’s existing straight-trim regression exercises this problem.

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ruevs ruevs mentioned this pull request Nov 9, 2026
Fillet squash - add internal functions (no UI) for fillets and chamfers #1815
Open
@ruevs

ruevs commented Nov 9, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member Author

The new code sends every exact straight edge through SBezier::ClosestPointTo(), which still samples and iterates for degree-one curves. That is unnecessary work on ordinary native models.
However, a straight XYZ trim does not necessarily have a straight UV image. For example, on the planar patch

S(u,v) = (u, v·(1 + u/4), 0)

the straight trim y = 0.5 has UV endpoints (0, 0.5) and (1, 0.4). Their UV midpoint maps to y = 0.50625, not 0.5. The PR’s existing straight-trim regression exercises this problem.

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.

@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 src/srf/boolean.cpp:432, EdgeNormalsWithinSurface() starts with:

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 sc->exact.

The call chain confirms this:

  • MakeTrimEdgesInto() converts the XYZ PWL vertices to UV when asked for MakeAs::UV.
  • MakeCopyTrimAgainst() passes those UV edge endpoints to EdgeNormalsWithinSurface().
  • Previously, an exact degree-one curve bypassed the correction onto the XYZ trim.

The example satisfies his definition of “exact”

For the existing regression’s non-rational variant:

Surface:        S(u,v) = (u, v·(1 + u/4), 2)
Exact XYZ trim: C(t)   = (t, 0.5, 2)

Every point of that XYZ line lies exactly on the surface: choose u=t, v=0.5/(1+t/4).

Nevertheless:

Endpoint UVs:                 (0, 0.5), (1, 0.4)
Their UV midpoint:            (0.5, 0.45)
Surface evaluated there:      (0.5, 0.50625, 2)

The probe misses the exact trim—and an incident face at y=0.5—by 0.00625 mm.

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 EdgeNormalsWithinSurface() first; that is the missing connection in my earlier explanation.

No code changes made.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ruevs

ruevs commented Nov 11, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member Author

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.

@phkahler I (or rather the future Übermensch) have STEP import/linking mostly working locally. Yes; it will only support geometry that SSurface can already represent. However that is still much more geometry than what can be constructed within SolveSpace with the available requests. For example saddles - curved surfaces with 1 == degm && 1 == degn. They are the reason for this PR.

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 SSurface objects would not have occurred to me, and (again) for geometry constructed within SolveSpace they overlap - all planar faces we can construct are affine (I'm 98% sure), but this is not the case for geometry in an imported STEP file.

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 SSurface.

The importer is intentionally very strict about the file format - both syntactically and as geometry - and rejects anything invalid or not representable by SSurface with an obnoxiously detailed error message. My hope is that if it ever gets merged and people start linking STEP files, they can easily open issues with failing ones - to improve both us and maybe the software that exported them. I'll push the branch for you to play with when I sit on the computer again.

BTW now I see what I've been doing to you! ;-)

:-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. ;-)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@phkahler

phkahler commented Nov 11, 2026

Copy link
Copy Markdown
Member

And now I'm doing it to you, and myself :-D Probably with same reasoning - STEP linking/import is "worth it" (and curiosity) :-D

@ruvs That would exactly why. Next release will have all the bigger things that people have wanted for 10 years.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@phkahler phkahler phkahler left review comments

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@ruevs @phkahler
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.