Skip to content

Conversation

@mfedderly
Copy link
Collaborator

Instead of messing around with @turf/jsts needing types, there's actually a new entrant into the underlying library space that solves a lot of problems at once.

clipper2-ts seems pretty exciting for us. I happened to find it because of our desire to drop our jsts dependency, but it also has intersect, union, difference, xor. So perhaps we can rework the other packages to depend on this library (polyclip-ts is slow with its bignumber implementation, and we'd have to fork it if we wanted to drop that). Because it's a port of a mature C library, it already comes with a ready-made test suite.

In the current state of this PR, the test suite fails to run, and 3 of the output fixtures look weird/broken (issue#783, negative-buffer, polygon-with-holes), but I'm pretty sure this is just me not understanding how to use the library.

Here's some numbers I grabbed with the work in progress version. Dropping @turf/jsts saves us ~26 of the total turf.min.js size, and it benchmarks faster (sometimes by a lot!).

before (turf.min.js 537847 bytes)
feature-collection-points x 10,026 ops/sec ±1.00% (95 runs sampled)
geometry-collection-points x 15,186 ops/sec ±0.25% (94 runs sampled)
issue-#783 x 17,635 ops/sec ±0.23% (97 runs sampled)
issue-#801-Ecuador x 26,334 ops/sec ±0.18% (100 runs sampled)
issue-#801 x 26,044 ops/sec ±0.43% (99 runs sampled)
issue-#815 x 39,962 ops/sec ±0.17% (99 runs sampled)
issue-#900 x 8,381 ops/sec ±0.44% (97 runs sampled)
issue-#916 x 22,839 ops/sec ±0.15% (97 runs sampled)
linestring x 31,301 ops/sec ±0.32% (94 runs sampled)
multi-linestring x 6,356 ops/sec ±0.28% (98 runs sampled)
multi-point x 18,566 ops/sec ±0.75% (94 runs sampled)
multi-polygon x 11,505 ops/sec ±0.28% (94 runs sampled)
negative-buffer x 41,211 ops/sec ±0.23% (97 runs sampled)
north-latitude-points x 15,355 ops/sec ±0.45% (100 runs sampled)
northern-polygon x 41,477 ops/sec ±0.42% (95 runs sampled)
point x 52,443 ops/sec ±0.24% (95 runs sampled)
polygon-with-holes x 27,893 ops/sec ±0.70% (96 runs sampled)

after (turf.min.js 396067 bytes)
feature-collection-points x 17,013 ops/sec ±0.66% (94 runs sampled)
geometry-collection-points x 24,760 ops/sec ±0.48% (98 runs sampled)
issue-#783 x 27,644 ops/sec ±0.29% (97 runs sampled)
issue-#801-Ecuador x 41,459 ops/sec ±1.94% (93 runs sampled)
issue-#801 x 40,359 ops/sec ±0.54% (100 runs sampled)
issue-#815 x 46,283 ops/sec ±0.45% (95 runs sampled)
issue-#900 x 6,972 ops/sec ±0.29% (100 runs sampled)
issue-#916 x 33,186 ops/sec ±0.50% (98 runs sampled)
linestring x 40,859 ops/sec ±0.42% (97 runs sampled)
multi-linestring x 10,623 ops/sec ±0.72% (92 runs sampled)
multi-point x 35,441 ops/sec ±0.50% (99 runs sampled)
multi-polygon x 20,049 ops/sec ±0.28% (97 runs sampled)
negative-buffer x 67,207 ops/sec ±0.20% (99 runs sampled)
north-latitude-points x 43,080 ops/sec ±0.25% (100 runs sampled)
northern-polygon x 66,229 ops/sec ±0.40% (99 runs sampled)
point x 96,881 ops/sec ±0.57% (95 runs sampled)
polygon-with-holes x 43,719 ops/sec ±1.19% (100 runs sampled)

@bratter
Copy link
Contributor

bratter commented Dec 25, 2025

FWIW this would be amazing if it works out for both the buffer and the clipping operations! On the off chance if there is any support you would need for this, let me know.

@bratter
Copy link
Contributor

bratter commented Jan 3, 2026

Spent some time taking a look at just the intersect operations today. Couple of insights if you haven't got there already:

  1. It appears as though the winding order clipper2 uses is counterclockwise, therefore not requiring the inversion you suggested above. It seems to be in clippper2 Delphi/C++ docs - see the "Clipping Closed Paths" section, and also seems to be true of my experimentation with turf. That will take away a some of the transformation required.
  2. Expected given my understanding of the algorithm, but it seems to operate on the integer part of the numbers only. The test suite also only tests integer inputs. So at least WGS84 coords would require scaling, but probably better to do some form of projection like is done here for buffering. Makes it the same non-trivial amount of work to do the conversion here. In my (janky) exploration I just multiplied decimal degrees by some large number and it got pretty decent.
  3. It is VERY fast for intersections. The two benchmarks in the suite already are on the simple side, but this recovers more than the ~10x lost in the bignumber move. See below for the benchmark results that I got, noting that it was done on a non-feature complete implementation, so will lose a decent chunk of time (e.g., no projection yet) to that.
  4. There is very likely to be precision-related issues given point (2). I didn't play around too much, but did still have issues getting exact matches even to 6 d.p. Will need exploration, and maybe at least coming to a tentative conclusion on the whole precision topic given the magnitude of this particular change.
  5. I really like the idea of dropping both jsts and polyclip-ts.

For reference the aforementioned benchmarks:

Current 7.3 polyclip-ts:
turf-intersect#simple x 3,256 ops/sec ±0.58% (97 runs sampled)
turf-intersect#armenia x 2,410 ops/sec ±0.48% (97 runs sampled)

My clipper replacement (but as noted above, anything final will be slower than this:
turf-intersect#simple x 216,990 ops/sec ±0.23% (98 runs sampled)
turf-intersect#armenia x 170,644 ops/sec ±0.26% (97 runs sampled)

@mfedderly
Copy link
Collaborator Author

@bratter

re: winding order, I think it still needs the reversal step because latitude is inverse of the cartesian Y axis. areaD returns positive area with the outer rings with the code as is. This stuff kinda makes my brain hurt, so I may be wrong.

re: integer vs float64, I intended to use the float64 variants before but got it wrong in the first commit. I added the D suffix today to all of the calls. My output fixtures are now visually the same (except I think clipper2 adds more points along the rounded sides with clipper's default settings). I also had to add handling for invalid winding orders as seen in polygon-with-holes's inner ring.

turf.min.js's linting does complain about the bigint syntax, which I think I'm not transitively using, but gets retained by the build instead of being treeshaken. We may have to do a major rev to be able to change that to some newer syntax (probably es2022). Perhaps when we do this, we just go ahead and document that we may roll forward and use any syntax that caniuse declares as "baseline" or another metric that we can use with babel's preset-env.

@bratter
Copy link
Contributor

bratter commented Jan 5, 2026

@mfedderly

Winding order
Think I worked it out. Indeed both winding orders are the same. The clipper docs do explicitly mention this elsewhere where they state: "Positive winding paths will be oriented in an anti-clockwise direction in Cartesian coordinates (where x coordinate values increase toward the right and y coordinate values increase upward). However, in graphics display libraries that use an inverted Y-axis, Positive winding paths will be oriented clockwise." This is the same as lng/lat.

HOWEVER - given that you are also projecting in your code the projection (which, apropos of the quote, is indeed from a graphics library :-)) it is the projection that does as you say and inverts the y and therefore requires the winding reversing that you are doing. I was playing with clipping, so was not projecting, hence experiencing the opposite.

So in summary - clipper and geojson wind the same, but clipper and a d3 geoAzimuthalEquidistant projection wind opposite. Therefore - no reversal required when using geojson, reversal is required when running through a projection.

Integer vs Float
Unfortunately Clipper2 does all its operations on ints. Even the *D versions just do some intermediate processing then delegate to the *64 versions. You can see there here for buffer where it just scales and delegates.

I think this means that the bigint syntax will be a requirement for clipper, so definitely major release, but as you point out it has been baseline for ages. Won't comment on the broader policy question.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants