Skip to content
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

Fix BoundBox.is_inside #828

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft

Conversation

jdegenstein
Copy link
Collaborator

The old comparison was not working correctly per issue #827 so I fixed that and added a test that returns False too. This fixes issue #827.

Copy link

codecov bot commented Dec 19, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 96.09%. Comparing base (fd5515d) to head (4c53117).
Report is 1 commits behind head on dev.

Additional details and impacted files
@@           Coverage Diff           @@
##              dev     #828   +/-   ##
=======================================
  Coverage   96.09%   96.09%           
=======================================
  Files          24       24           
  Lines        8493     8493           
=======================================
  Hits         8161     8161           
  Misses        332      332           

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

@snoyer
Copy link
Contributor

snoyer commented Dec 19, 2024

is "A is inside B" supposed to mean "B fully encloses A" or "A and B are overlapping"?

edit:

def is_inside(self, second_box: BoundBox) -> bool:
"""Is the provided bounding box inside this one?

def is_inside(self, point: VectorLike, tolerance: float = 1.0e-6) -> bool:
"""Returns whether or not the point is inside a solid or compound

def is_inside(self, point: VectorLike, tolerance: float = 1.0e-6) -> bool:
"""Point inside Face
Returns whether or not the point is inside a Face within the specified tolerance.

so basically self.is_inside(other) is supposed to be read as "Other is inside of Self" ?
Wouldn't it be more intuitive to call these methods self.contains(other) ?

The bounding box method does look more like an overlap test than a is_inside/contains test. Maybe you need 2 methods:

  • bbox1.is_inside(bbox2) returning whether bbox1 is fully inside bbox2
  • bbox1.overlaps(bbox2) returning whether one box contains any part of the other

@jdegenstein
Copy link
Collaborator Author

jdegenstein commented Dec 19, 2024

@snoyer Putting aside the other is_inside methods, you are correct that BoundBox.is_inside in this PR is really talking about overlapping bounding boxes. I think that may have been what was intended with the current version in dev as well, but it is hard to tell as it is not working correctly per the referenced issue. I was thinking that this method could be enhanced with some different flags, but I now think you are right that it makes more sense to simply have different methods.

With that in mind I agree that the current implementation in this PR should be renamed to BoundBox.overlaps and that a new implementation of BoundBox.contains is created that represents e.g. bboxA fully contains bboxB -> bboxA.contains(bboxB). The current docstring reads to me like "bboxB is inside bboxA" -> bboxA.is_inside(bboxB) which does not make much sense to me because the ordering is backwards. With this in mind I think BoundBox.is_inside should be fixed and renamed to BoundBox.contains and BoundBox.is_inside should be deleted.

EDIT: New proposed behavior

bboxA.overlaps(bboxB) == bboxB.overlaps(bboxA) #i.e. they are always equivalent
bboxA.contains(bboxB) != bboxB.contains(bboxA) #i.e. they are never equivalent even if bboxA and bboxB are coincident 

I am open to input on the latter behavior as this is the most strict implementation.

@snoyer
Copy link
Contributor

snoyer commented Dec 20, 2024

@jdegenstein

bboxA.contains(bboxB) != bboxB.contains(bboxA) #i.e. they are never equivalent even if bboxA and bboxB are coincident 

Assuming you mean bboxA.contains(bboxA) == False, saying "a box does not contain itself" may be tempting from a natural language point-of-view but wouldn't it also implies "a box does not contain its own corners" when implemented?
IMO it would make sense to use inclusive comparisons and have:

bbox(A,B).contains(bbox(A,B)) == True  # box contains itself
bbox(A,B).contains(bbox(A)) == True  # box contains [the box of] any of its points
bbox(A,B).contains(bbox(B)) == True

The same could be done for the overlapping to have:

bbox(A,B).overlaps(B,C) == True  # boxes have a common corner

@jdegenstein
Copy link
Collaborator Author

IMO it would make sense to use inclusive comparisons

@snoyer Maybe this should be controllable by a boolean flag? It can be useful to differentiate these assumptions in practice when e.g. writing lambdas for filtering purposes.

bboxA.contains(bboxA, inclusive=True) == True
bboxA.contains(bboxA, inclusive=False) == False

and also:

# when e.g. bboxB shares a corner, edge, or face with bboxA
bboxA.overlaps(bboxB, inclusive=True) == True
bboxA.overlaps(bboxB, inclusive=False) == False

Is there a better name for the flag than inclusive?

@snoyer
Copy link
Contributor

snoyer commented Dec 21, 2024

Is there a better name for the flag than inclusive?

could flip the boolean value and call it strict? other than that I don't know but I didn't think about it that hard because I'm not a fan of these boolean flags either way.

If you really need to have both versions of contains (do you? bounding boxes would only be used for pre-checks and actual precise tests would be done on actual geometry) you could have separate contains() and contains_strictly() methods.
The implementation would be cleaner (no branching in the code), the documentation would be easier to read (no "branching" in the doc string) and if the names are explicit enough the users won't even have to check it.
Also compare the filtering usage:

filter(mybox.contains_strictly, other_boxes)

vs

filter(lambda b: mybox.contains(b, inclusive=False), other_boxes)

or

filter(partial(mybox.contains, inclusive=False), other_boxes)

Obviously this is all subjective API design choices and @gumyr's opinion would be more important than mine :)

@jdegenstein
Copy link
Collaborator Author

@snoyer Yeah I agree we need @gumyr to weigh in -- this PR discussion has grown a lot in scope from when it was simply trying to fix a broken method. I personally do not like having 4 separate methods for this, I prefer 2 methods with the flags with the inclusive name vs. strict although still open to other names.

I am convinced that having the effect of inclusive True/False is a useful distinction and should be available.

Regardless I am not sure anyone has even tried to use the existing method given the bug that I discovered -- so at some point it is better to just move on to more important issues and add these small fixes/enhancements.

@gumyr
Copy link
Owner

gumyr commented Dec 23, 2024

The current assumption in the code is that objects on the edge of another object are "inside" it. Internally these two can be separated:

solid_classifier.State() == ta.TopAbs_IN or solid_classifier.IsOnAFace()

but I didn't see the value in that. However, I'm happy to have them separate if that's helpful.

I like the proposed contains & overlaps change. Maybe contains could include on the edges and there could be another method to identify objects specifically on the edge or surface of another?

@snoyer
Copy link
Contributor

snoyer commented Dec 24, 2024

Regardless of which one(s) you decide to implement in build123d you may want to pick the names according to existing standards/conventions if possible, for example: https://en.wikipedia.org/wiki/DE-9IM#Spatial_predicates, or, as per shapely:

  • A.contains(B)

    Returns True if geometry B is completely inside geometry A.

    A contains B if no points of B lie in the exterior of A and at least one
    point of the interior of B lies in the interior of A.

  • A.contains_properly(B)

    Returns True if geometry B is completely inside geometry A, with no
    common boundary points.

    A contains B properly if B intersects the interior of A but not the
    boundary (or exterior). This means that a geometry A does not
    "contain properly" itself, which contrasts with the contains function,
    where common points on the boundary are allowed.

  • A.covered_by(B)

    Returns True if no point in geometry A is outside geometry B.

  • A.covers(B)

    Returns True if no point in geometry B is outside geometry A.

  • A.crosses(B)

    Returns True if A and B spatially cross.

    A crosses B if they have some but not all interior points in common,
    the intersection is one dimension less than the maximum dimension of A or B,
    and the intersection is not equal to either A or B.

  • A.disjoint(B)

    Returns True if A and B do not share any point in space.

    Disjoint implies that overlaps, touches, within, and intersects are False.

  • A.intersects(B)

    Returns True if A and B share any portion of space.

    Intersects implies that overlaps, touches, covers, or within are True.

  • A.overlaps(B)

    Returns True if A and B spatially overlap.

    A and B overlap if they have some but not all points in common, have the
    same dimension, and the intersection of the interiors of the two geometries
    has the same dimension as the geometries themselves. That is, only polyons
    can overlap other polygons and only lines can overlap other lines.

    If either A or B are None, the output is always False.

  • A.touches(B)

    Returns True if the only points shared between A and B are on the
    boundary of A and B.

  • A.within(B)

    Returns True if geometry A is completely inside geometry B.

    A is within B if no points of A lie in the exterior of B and at least one
    point of the interior of A lies in the interior of B.

@jdegenstein jdegenstein marked this pull request as draft January 7, 2025 21:52
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