Skip to content

Conversation

@tigercosmos
Copy link
Collaborator

The PR is a prototype of trapezoidal decomposition for the further support of polygon operations.

@tigercosmos tigercosmos marked this pull request as draft December 26, 2025 14:12
Copy link
Collaborator Author

@tigercosmos tigercosmos left a comment

Choose a reason for hiding this comment

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

@yungyuc Could you take a look and discuss if the current PR approach is correct? I want to add trapezoidal decomposition with further support of polygon boolean operations. However, after studying a while, I am still confusing what to do next.

Comment on lines 987 to 994
struct Trapezoid
{
value_type top_y; ///< Y-coordinate of the top horizontal edge
value_type bottom_y; ///< Y-coordinate of the bottom horizontal edge
segment_type left_edge; ///< Left boundary segment of the trapezoid
segment_type right_edge; ///< Right boundary segment of the trapezoid
size_t source_polygon; ///< ID of the polygon this trapezoid came from
};
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Struct for trapazoid

Copy link
Member

Choose a reason for hiding this comment

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

Trapezoid should be taken outside to be a normal class (with the name Trapezoid3d, class declaration, private data with prefix, and accessors). It will be so useful that you may just create TrapezoidPad using this PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Got it. Thanks.

* @throws std::out_of_range if polygon_id is invalid
* @throws std::invalid_argument if polygon has fewer than 3 nodes
*/
std::vector<Trapezoid> const & decompose_polygon(size_t polygon_id);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The API for trapazoidal decomposition.

Copy link
Member

Choose a reason for hiding this comment

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

Create a helper class TrapezoidalDecomposition (or a more accurate name you like), and rename PolygonPad::decompose_polygon() to PolygonPad::decompose_to_trapezoid() for accurate semantics. Call the helper class from decompose_to_trapezoid().

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Got it.

* @param p2 Second polygon
* @return Vector of polygons forming the union
*/
std::vector<polygon_type> boolean_union(polygon_type const & p1, polygon_type const & p2);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The part I am not sure. Do we want to have operation APIs in the PolygonPad? The operation is between polygon and polygon, or polygon pad or polygon pad?

Copy link
Member

Choose a reason for hiding this comment

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

Good point. Area Boolean is set Boolean, so it operates between sets, so PolygonPad and PolygonPad.

Our goal is volumetric Boolean, but we need to make the area Boolean very clear before moving forward.

Copy link
Member

Choose a reason for hiding this comment

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

Create helper classes AreaBooleanUnion, AreaBooleanIntersection, and AreaBooleanDifference for the 3 operations you are prototyping here.

Use naive implementation at this point. Add unit tests to make sure they have Python interface and do not crash. Leave TODO comments in the tests saying results are not confirmed to be correct and the correctness is left for a follow-up PR.

This is good progress.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I see. Thanks!

@yungyuc yungyuc added the geometry Geometry entities and manipulation label Dec 27, 2025
Copy link
Member

@yungyuc yungyuc left a comment

Choose a reason for hiding this comment

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

Did you use any AI tool to help? I am curious about it. If you did please specify.

This is good progress. Points to address:

  • Make a normal (non-nested) Trapezoid3d and TrapezoidPad.
  • Do not use hash table and do not use hash table to vectors.
  • Create a helper class TrapezoidalDecomposition (or a more accurate name you like).
  • Rename PolygonPad::decompose_polygon() to PolygonPad::decompose_to_trapezoid() for accurate semantics.
  • Create helper classes AreaBooleanUnion, AreaBooleanIntersection, and AreaBooleanDifference for the 3 operations.
  • For the area Boolean, add unit tests to make sure they have Python interface and do not crash. Leave TODO comments in the tests saying results are not confirmed to be correct and the correctness is left for a follow-up PR.

Comment on lines 987 to 994
struct Trapezoid
{
value_type top_y; ///< Y-coordinate of the top horizontal edge
value_type bottom_y; ///< Y-coordinate of the bottom horizontal edge
segment_type left_edge; ///< Left boundary segment of the trapezoid
segment_type right_edge; ///< Right boundary segment of the trapezoid
size_t source_polygon; ///< ID of the polygon this trapezoid came from
};
Copy link
Member

Choose a reason for hiding this comment

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

Trapezoid should be taken outside to be a normal class (with the name Trapezoid3d, class declaration, private data with prefix, and accessors). It will be so useful that you may just create TrapezoidPad using this PR.

// - Point location query structure
// - Integration with node lists for synchronization
/// Cache of trapezoidal decompositions, keyed by polygon ID. Computed lazily on first access.
std::unordered_map<size_t, std::vector<Trapezoid>> m_decompositions;
Copy link
Member

Choose a reason for hiding this comment

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

Do not use hash table. Hash table is the last resort when (1) you run out of ideas or (2) you run out of time.

A has table to vector is even worth: tons of dynamic memory allocation. It is bound to be slow.

This is why you need TrapezoidPad.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Got it.

* @throws std::out_of_range if polygon_id is invalid
* @throws std::invalid_argument if polygon has fewer than 3 nodes
*/
std::vector<Trapezoid> const & decompose_polygon(size_t polygon_id);
Copy link
Member

Choose a reason for hiding this comment

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

Create a helper class TrapezoidalDecomposition (or a more accurate name you like), and rename PolygonPad::decompose_polygon() to PolygonPad::decompose_to_trapezoid() for accurate semantics. Call the helper class from decompose_to_trapezoid().

* @param p2 Second polygon
* @return Vector of polygons forming the union
*/
std::vector<polygon_type> boolean_union(polygon_type const & p1, polygon_type const & p2);
Copy link
Member

Choose a reason for hiding this comment

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

Good point. Area Boolean is set Boolean, so it operates between sets, so PolygonPad and PolygonPad.

Our goal is volumetric Boolean, but we need to make the area Boolean very clear before moving forward.

* @param p2 Second polygon
* @return Vector of polygons forming the union
*/
std::vector<polygon_type> boolean_union(polygon_type const & p1, polygon_type const & p2);
Copy link
Member

Choose a reason for hiding this comment

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

Create helper classes AreaBooleanUnion, AreaBooleanIntersection, and AreaBooleanDifference for the 3 operations you are prototyping here.

Use naive implementation at this point. Add unit tests to make sure they have Python interface and do not crash. Leave TODO comments in the tests saying results are not confirmed to be correct and the correctness is left for a follow-up PR.

This is good progress.

@tigercosmos
Copy link
Collaborator Author

@yungyuc Yes, I used AI and spent around 5–6 hours exploring how to implement the trapezoidal decomposition. In the end, I wasn’t happy with the outcome and wasn’t confident about the design direction. I removed most of the implementation, kept what seemed to be the most reasonable interface, and opened this PR for discussion.

@yungyuc
Copy link
Member

yungyuc commented Jan 1, 2026

@yungyuc Yes, I used AI and spent around 5–6 hours exploring how to implement the trapezoidal decomposition. In the end, I wasn’t happy with the outcome and wasn’t confident about the design direction. I removed most of the implementation, kept what seemed to be the most reasonable interface, and opened this PR for discussion.

6 hours on the trapezoidal algorithm are not too many, but spending time to study it and then prototype incrementally will be more productive. Dividing tasks is an art that we cannot count on tools already.

}

TrianglePad(
TrianglePad(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

GitHub's diff is weird here. There is new added Trapezoid3d and TrapezoidPad.

Copy link
Member

Choose a reason for hiding this comment

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

Since this is a large diff, the diff issue needs to be fixed. The messy diff stops reasonable tracking in the future.

Please reorder the new code to make diff looks good. Use a follow-up refactoring PR to reorder after this one.

self.assertFalse(polygon1.is_same(polygon2))
self.assertTrue(polygon1.is_not_same(polygon2))

# TODO: Verify and implement boolean operations once the C++ side is complete. # noqa: E501
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Put TODO for now and only verify the interface.

Copy link
Member

Choose a reason for hiding this comment

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

Do not waive flake8 E501 for comments other than URL. Fix the comment length instead.

Comment on lines 1734 to 1736
std::shared_ptr<trapezoid_pad_type> m_trapezoids;
std::vector<ssize_type> m_begins;
std::vector<ssize_type> m_ends;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No hash table now.

* @tparam T floating-point type
*/
template <typename T>
class AreaBooleanUnion
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Helper classes as requsted.

* @throws std::out_of_range if polygon_id is invalid
* @throws std::invalid_argument if polygon has fewer than 3 nodes
*/
std::pair<size_t, size_t> decompose_to_trapezoid(size_t polygon_id);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed the naming.

}

template <typename T>
std::pair<size_t, size_t> PolygonPad<T>::decompose_to_trapezoid(size_t polygon_id)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Get the index range of the trapazoids in the pad after decomposition.

@tigercosmos tigercosmos changed the title [WIP] Trapezoidal Decomposition Prototype for Discussion Trapezoidal Decomposition Prototype for Discussion Jan 7, 2026
@tigercosmos tigercosmos marked this pull request as ready for review January 7, 2026 15:15
@tigercosmos
Copy link
Collaborator Author

@yungyuc the aforementioned issues are addressed. Please help take a look, thanks!

Copy link
Member

@yungyuc yungyuc left a comment

Choose a reason for hiding this comment

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

Good progress. Points to address:

  • Reorder the new code to make diff looks good.
  • Rename TrapezoidalDecomposer::Trapezoid to something very different from Trapezoid3d or Trapezoid and make it private.
  • Reduce memory footprint for coordinates for TrapezoidalDecomposer::YTrap.
  • Return PolygonPad from AreaBooleanUnion::compute, AreaBooleanIntersection::compute, AreaBooleanDifference::compute.

}

TrianglePad(
TrianglePad(
Copy link
Member

Choose a reason for hiding this comment

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

Since this is a large diff, the diff issue needs to be fixed. The messy diff stops reasonable tracking in the future.

Please reorder the new code to make diff looks good. Use a follow-up refactoring PR to reorder after this one.

* @tparam T floating-point type
*/
template <typename T>
class Trapezoid3d
Copy link
Member

Choose a reason for hiding this comment

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

This (line 813) is the new Trapezoid3d. Add inline annotation close to the line to make discussions clear.

SimpleArray<T> const & y1,
SimpleArray<T> const & x2,
SimpleArray<T> const & y2,
SimpleArray<T> const & x3,
Copy link
Member

Choose a reason for hiding this comment

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

Diff algorithm has a hard time knowing that you are inserting a block of new code. So we need to choose a better location for the new code to make diff happy. Things like this happen, though inconvenience.

* In polygon decomposition, vertical lines are extended from each vertex to partition
* the polygon into trapezoids.
*/
struct Trapezoid
Copy link
Member

Choose a reason for hiding this comment

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

It's confusing to have the inner class Trapezoid while there is a standalone Trapezoid3d. Rename this internal class name and make it private.

I suggest a name like YTrap because your scanlines are perpendicular to the Y-axis.

*/
struct Trapezoid
{
value_type top_y;
Copy link
Member

Choose a reason for hiding this comment

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

You are using scanlines perpendicular to the Y-axis and should take advantage of that constraint. You do not need flexibility at the moment. (You will in the future, but do not over-engineer.)

Use real number for member data. Don't use points.

value_type lower_y, upper_y;
value_type lower_x0, lower_x1, upper_x0, upper_x1;
value_type lower_z0, lower_z1, upper_z0, upper_z1;

It takes only 10 real numbers for the trapezoid whose lower and upper edges are perpendicular to the Y-axis. Your old struct takes 14 real numbers. Don't be so wasteful.

value_type top_y;
value_type bottom_y;
segment_type left_edge;
segment_type right_edge;

I usually see lower-left, lower-right, upper-left, and upper-right in geometry code. Let's use the convention and avoid top and bottom.

/**
* Trapezoid structure for decomposition.
*
* A trapezoid is a quadrilateral with two parallel horizontal edges (top and bottom).
Copy link
Member

Choose a reason for hiding this comment

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

Remove the trivial statement to explain trapezoid. People not understanding trapezoid cannot contribute.

Add descriptions for the fact that the struct is for traps whose bottom and top lines perpendicular to Y-axis. It's the most important point to understand the algorithm.

* @param polygon_id2 ID of second polygon
* @return Vector of polygons forming the union
*/
std::vector<Polygon3d<T>> compute(polygon_pad_type & pad, size_t polygon_id1, size_t polygon_id2);
Copy link
Member

Choose a reason for hiding this comment

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

Return PolygonPad from AreaBoolean*::compute.

@tigercosmos
Copy link
Collaborator Author

@yungyuc I addressed the aforementioned issues. Cloud you take a look again?

@tigercosmos tigercosmos requested a review from yungyuc January 18, 2026 09:57
Copy link
Member

@yungyuc yungyuc left a comment

Choose a reason for hiding this comment

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

Only cosmetic issues:

  • Some overload signature of TrapezoidPad::set() and TrapezoidPad::set_at() is too long. Break them with proper indentation. (Leave an inline annotation at the improved line to remind me to review.)
  • Do not waive flake8 E501 for comments other than URL. Fix the comment length instead.

x3_at(i) = x3;
y3_at(i) = y3;
}
void set_at(size_t i, value_type x0, value_type y0, value_type z0, value_type x1, value_type y1, value_type z1, value_type x2, value_type y2, value_type z2, value_type x3, value_type y3, value_type z3)
Copy link
Member

Choose a reason for hiding this comment

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

This line is too long. Break it with proper indent.

z3(i) = p3.z();
}
}
void set(size_t i, value_type x0_value, value_type y0_value, value_type x1_value, value_type y1_value, value_type x2_value, value_type y2_value, value_type x3_value, value_type y3_value)
Copy link
Member

Choose a reason for hiding this comment

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

This line is too long. Break it with proper indent.

x3(i) = x3_value;
y3(i) = y3_value;
}
void set(size_t i, value_type x0_value, value_type y0_value, value_type z0_value, value_type x1_value, value_type y1_value, value_type z1_value, value_type x2_value, value_type y2_value, value_type z2_value, value_type x3_value, value_type y3_value, value_type z3_value)
Copy link
Member

Choose a reason for hiding this comment

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

This line is too long. Break it with proper indent.

self.assertFalse(polygon1.is_same(polygon2))
self.assertTrue(polygon1.is_not_same(polygon2))

# TODO: Verify and implement boolean operations once the C++ side is complete. # noqa: E501
Copy link
Member

Choose a reason for hiding this comment

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

Do not waive flake8 E501 for comments other than URL. Fix the comment length instead.

@tigercosmos
Copy link
Collaborator Author

tigercosmos commented Jan 19, 2026

@yungyuc the issues are fixed in ac56bae, please take a look.

Copy link
Member

@yungyuc yungyuc left a comment

Choose a reason for hiding this comment

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

The PR is a prototype of trapezoidal decomposition for the further support of polygon operations.

  • Write informative summary in the PR first comment and commit log. This PR added more than 1.4k lines of code and deserve a good summaries.

And consider to review the code formatting again. There is at least one defect but could be more.

pass
# TODO: Verify and implement boolean operations
# once the C++ side is complete.

Copy link
Member

Choose a reason for hiding this comment

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

The blank line is misplaced. You can fix it in this PR or in a later one.

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

Labels

geometry Geometry entities and manipulation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants