8000 Fix intersection behavior in CrossSection::BatchBoolean by tomasf · Pull Request #1247 · elalish/manifold · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Fix intersection behavior in CrossSection::BatchBoolean #1247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 2, 2025

Conversation

tomasf
Copy link
Contributor
@tomasf tomasf commented May 1, 2025

CrossSection::BatchBoolean builds a big list of clip paths from the tail of the crossSections list and then performs the boolean operation with the head. This works fine for union and difference, but when intersecting more than two cross sections, it produces incorrect results.

This PR adds a special case for OpType::Intersect where C2::BooleanOp is instead called repeatedly for every part of the tail.

I'm not a C++ guy, so take the code with a scoop of salt.

Copy link
codecov bot commented May 1, 2025

Codecov Report

Attention: Patch coverage is 14.28571% with 6 lines in your changes missing coverage. Please review.

Project coverage is 92.07%. Comparing base (1ec2036) to head (25c3fe3).
Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/cross_section/cross_section.cpp 14.28% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1247      +/-   ##
==========================================
- Coverage   92.17%   92.07%   -0.10%     
==========================================
  Files          31       31              
  Lines        5888     5895       +7     
==========================================
+ Hits         5427     5428       +1     
- Misses        461      467       +6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Trzeth
Copy link
Contributor
Trzeth commented May 2, 2025

Yes, I think you are right.
Here is my test sample. A square intersect with three non-overlapping circles.
The desired result should be only one half circle, but come out three.
image

const result = CrossSection.intersection(
  [
    CrossSection.square(100)
    ,CrossSection.circle(30,30).translate(-10,30)
    ,CrossSection.circle(20,30).translate(110,20)
    ,CrossSection.circle(40,30).translate(50,110)
  ]).extrude(1)

@tomasf
Copy link
Contributor Author
tomasf commented May 2, 2025

Yes, I think you are right. Here is my test sample. A square intersect with three non-overlapping circles. The desired result should be only one half circle, but come out three.

I actually think the correct result for your code should be empty. There's no area covered by all four shapes. The result should be equivalent to:

const result = CrossSection.intersection([
  CrossSection.square(100),
  CrossSection.intersection([
    CrossSection.circle(30,30).translate(-10,30),
    CrossSection.intersection([
      CrossSection.circle(20,30).translate(110,20),
      CrossSection.circle(40,30).translate(50,110)
    ])
  ])
]).extrude(1)

or, expressed in 3D:

const result = Manifold.intersection(
[
  Manifold.cube([100, 100, 1])
  ,Manifold.cylinder(1, 30).translate(-10,30)
  ,Manifold.cylinder(1, 20).translate(110,20)
  ,Manifold.cylinder(1, 40).translate(50,110)
])

@tomasf
Copy link
Contributor Author
tomasf commented May 2, 2025

This code makes a Reuleaux triangle:

const size = 10.0
const offset = size / Math.sqrt(3)
const result = CrossSection.intersection([
  CrossSection.circle(size)
    .translate([offset, 0]),
  CrossSection.circle(size)
    .translate([offset, 0])
    .rotate(120),
  CrossSection.circle(size)
    .translate([offset, 0])
    .rotate(240)
]).extrude(1)

Expected

Screenshot 2025-05-02 at 09 07 43

Actual

Screenshot 2025-05-02 at 09 07 56

@Trzeth
Copy link
Contributor
Trzeth commented May 2, 2025

My bad, the output CrossSection indeed should be empty.
I believe your code can fix it but I'm not sure it is the best way.
Because what manifold is just collect CrossSections path and send to Clipper2 to handle it. Do you think this problem might come from Clipper2?

@tomasf
Copy link
Contributor Author
tomasf commented May 2, 2025

My bad, the output CrossSection indeed should be empty. I believe your code can fix it but I'm not sure it is the best way. Because what manifold is just collect CrossSections path and send to Clipper2 to handle it. Do you think this problem might come from Clipper2?

I'm also not sure it's the best way. As far as I can tell, Clipper2 has no direct way to intersect more than two shapes, so Manifold would need to make multiple intersection calls, one way or another.

@Trzeth
Copy link
Contributor
Trzeth commented May 2, 2025

Actually Clipper2 can handle multi shapes. But the different is Clipper2 handles the input as a whole set, not process one by one with order as we thought. So manifold just keeps this feature.

So I think this is not a bug. Because this apparently can be useful under some circumstances, and no other exposed api can replace this.
But it is indeed misleading since the behavior is not as the same as Manifold::BatchBoolean.

Rather than throw this feature completely, maybe the CrossSection::BatchBoolean can have another name? Like whole boolean or something?
Or change the Manifold::BatchBoolean behavior not doing it one by one but as a whole.

image

// Process CrossSection as a whole, output three parts
const result = CrossSection.intersection(
  [
    CrossSection.square(100)
    ,CrossSection.circle(30,30).translate(-10,30)
    ,CrossSection.circle(20,30).translate(110,20)
    ,CrossSection.circle(40,30).translate(50,110)
  ]).extrude(1)

// Process Manifold one by one, output nothing
 const result = Manifold.intersection([
    CrossSection.square(100).extrude(1)
    ,CrossSection.circle(30,30).translate(-10,30).extrude(1)
    ,CrossSection.circle(20,30).translate(110,20).extrude(1)
    ,CrossSection.circle(40,30).translate(50,110).extrude(1)
  ])

@tomasf
Copy link
Contributor Author
tomasf commented May 2, 2025

So I think this is not a bug. Because this apparently can be useful under some circumstances, and no other exposed api can replace this. But it is indeed misleading since the behavior is not as the same as Manifold::BatchBoolean.

As far as I can tell, the current behavior is the same as an intersection with a union of the tail. For your example, the proper replacement for the same output is:

const result = CrossSection.intersection(
  [
    CrossSection.square(100),
    CrossSection.union([
      CrossSection.circle(30,30).translate(-10,30),
      CrossSection.circle(20,30).translate(110,20),
      CrossSection.circle(40,30).translate(50,110)
    ])
  ]).extrude(1)

@Trzeth
Copy link
Contributor
Trzeth commented May 2, 2025

You are right, I think your current way is the best way to fix the misleading behaviour. Good job!

@pca006132 pca006132 merged commit 3027282 into elalish:master May 2, 2025
27 checks passed
@tomasf tomasf deleted the cross-section-intersection-fix-poc branch May 2, 2025 15:30
Copy link
Owner
@elalish elalish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! One thing I would have asked for is a test for this - note that codecov shows your addition doesn't have any coverage.

@elalish elalish mentioned this pull request May 14, 2025
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.

4 participants
0