diff --git a/include/packingsolver/irregular/shape.hpp b/include/packingsolver/irregular/shape.hpp index e59f849eb..c0ccfd8e7 100644 --- a/include/packingsolver/irregular/shape.hpp +++ b/include/packingsolver/irregular/shape.hpp @@ -356,5 +356,24 @@ bool equal( const Shape& shape_1, const Shape& shape_2); +// Check if a point is on a line segment +bool is_point_on_line_segment( + const Point& p, + const Point& start, + const Point& end); + +// Check if a point is strictly inside a shape (excluding the boundary) +bool is_point_strictly_inside_shape( + const Point& point, + const Shape& shape); + +// Check if a point is inside a shape or on its boundary +bool is_point_inside_or_on_shape( + const Point& point, + const Shape& shape); + +std::vector borders( + const Shape& shape); + } } diff --git a/src/irregular/CMakeLists.txt b/src/irregular/CMakeLists.txt index 8ed2633e6..0373d4a8e 100644 --- a/src/irregular/CMakeLists.txt +++ b/src/irregular/CMakeLists.txt @@ -10,6 +10,7 @@ target_sources(PackingSolver_irregular PRIVATE shape_element_intersections.cpp shape_convex_hull.cpp shape_extract_borders.cpp + shape_inflate.cpp shape_self_intersections_removal.cpp shape_simplification.cpp shape_trapezoidation.cpp diff --git a/src/irregular/shape.cpp b/src/irregular/shape.cpp index db327f028..cb443d59c 100644 --- a/src/irregular/shape.cpp +++ b/src/irregular/shape.cpp @@ -1,6 +1,14 @@ #include "packingsolver/irregular/shape.hpp" +#include #include +#include +#include + +// Define M_PI (if not provided by the system) +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif using namespace packingsolver; using namespace packingsolver::irregular; @@ -1252,3 +1260,164 @@ bool irregular::equal( return true; } + +// Check if a point is on a line segment +bool irregular::is_point_on_line_segment(const Point& p, const Point& start, const Point& end) { + // Calculate the squared length of the line segment + LengthDbl line_length_squared = std::pow(end.x - start.x, 2) + std::pow(end.y - start.y, 2); + + // If the line segment is actually a point + if (equal(line_length_squared, 0.0)) { + return equal(p.x, start.x) && equal(p.y, start.y); + } + + // Calculate parameter t, representing the position of the point on the line segment (between 0 and 1 indicates on the segment) + LengthDbl t = ((p.x - start.x) * (end.x - start.x) + (p.y - start.y) * (end.y - start.y)) / line_length_squared; + + // If t is outside the [0,1] range, the point is not on the line segment + if (strictly_lesser(t, 0.0) || strictly_greater(t, 1.0)) { + return false; + } + + // Calculate the projection point + Point projection; + projection.x = start.x + t * (end.x - start.x); + projection.y = start.y + t * (end.y - start.y); + + // Check if the distance to the projection point is small enough + LengthDbl distance_squared = std::pow(p.x - projection.x, 2) + std::pow(p.y - projection.y, 2); + return equal(distance_squared, 0.0); +} + +// Check if a point is strictly inside a shape (excluding the boundary) +bool irregular::is_point_strictly_inside_shape(const Point& point, const Shape& shape) { + if (shape.elements.empty()) { + return false; + } + + // First check if the point lies on any boundary + for (const ShapeElement& element : shape.elements) { + if (element.type == ShapeElementType::LineSegment) { + // Check if the point is on the line segment + if (is_point_on_line_segment(point, element.start, element.end)) { + return false; // Point is on the boundary, not strictly inside + } + } else if (element.type == ShapeElementType::CircularArc) { + // Check if the point is on the circular arc + LengthDbl dx = point.x - element.center.x; + LengthDbl dy = point.y - element.center.y; + LengthDbl distance = std::sqrt(dx * dx + dy * dy); + + // Calculate the radius of the arc + LengthDbl radius = std::sqrt( + std::pow(element.start.x - element.center.x, 2) + + std::pow(element.start.y - element.center.y, 2) + ); + + // If the point is on the circle + if (equal(distance, radius)) { + // Calculate the point's angle + LengthDbl point_angle = angle_radian({dx, dy}); + + // Calculate the start and end angles of the arc + LengthDbl start_angle = angle_radian({element.start.x - element.center.x, element.start.y - element.center.y}); + LengthDbl end_angle = angle_radian({element.end.x - element.center.x, element.end.y - element.center.y}); + + // Ensure angles are in the correct range + if (element.anticlockwise && end_angle <= start_angle) { + end_angle += 2 * M_PI; + } else if (!element.anticlockwise && start_angle <= end_angle) { + start_angle += 2 * M_PI; + } + + // Check if the point's angle is within the arc range + bool in_arc_range; + if (element.anticlockwise) { + in_arc_range = (!strictly_lesser(point_angle, start_angle) && !strictly_greater(point_angle, end_angle)); + } else { + in_arc_range = (!strictly_greater(point_angle, start_angle) && !strictly_lesser(point_angle, end_angle)); + } + + if (in_arc_range) { + return false; // Point is on the arc, not strictly inside + } + } + } + } + + // Then use the ray-casting algorithm to check if the point is inside + int intersection_count = 0; + + for (const ShapeElement& element : shape.elements) { + if (element.type == ShapeElementType::LineSegment) { + // Handle the special case of horizontal line segments + if (equal(element.start.y, element.end.y)) { + // Horizontal line segment: if the point's y coordinate equals the segment's y coordinate, + // and the x coordinate is within the segment range, the point is on the segment + if (equal(point.y, element.start.y) && + (!strictly_lesser(point.x, std::min(element.start.x, element.end.x))) && + (!strictly_greater(point.x, std::max(element.start.x, element.end.x)))) { + // Already checked in the previous section, no need to handle here + continue; + } + } + + // Standard ray-casting algorithm for line segment checking + // Cast a ray to the right from the point, count intersections with segments + bool cond1 = (strictly_greater(element.start.y, point.y) != strictly_greater(element.end.y, point.y)); + bool cond2 = strictly_lesser(point.x, (element.end.x - element.start.x) * (point.y - element.start.y) / + (element.end.y - element.start.y) + element.start.x); + + if (cond1 && cond2) { + intersection_count++; + } + } else if (element.type == ShapeElementType::CircularArc) { + // Circular arc checking is more complex + LengthDbl dx = point.x - element.center.x; + LengthDbl dy = point.y - element.center.y; + LengthDbl distance = std::sqrt(dx * dx + dy * dy); + + LengthDbl radius = std::sqrt( + std::pow(element.start.x - element.center.x, 2) + + std::pow(element.start.y - element.center.y, 2) + ); + + // If the point is inside the circle and to the left of the center, there may be intersections with a ray to the right + if (strictly_lesser(distance, radius) && strictly_lesser(point.x, element.center.x)) { + LengthDbl start_angle = angle_radian({element.start.x - element.center.x, element.start.y - element.center.y}); + LengthDbl end_angle = angle_radian({element.end.x - element.center.x, element.end.y - element.center.y}); + + // Ensure angles are in the correct range + if (element.anticlockwise && end_angle <= start_angle) { + end_angle += 2 * M_PI; + } else if (!element.anticlockwise && start_angle <= end_angle) { + start_angle += 2 * M_PI; + } + + // Calculate the point's line-of-sight angle (angle between the line from point to center and the horizontal) + LengthDbl point_angle = angle_radian({dx, dy}); + if (strictly_lesser(point_angle, 0)) { + point_angle += 2 * M_PI; // Adjust angle to [0, 2π) + } + + // Calculate the intersection angle of the ray to the right with the circle + LengthDbl ray_angle = 0; // Angle of ray to the right is 0 + + // Check if the ray intersects the arc + bool intersects_arc; + if (element.anticlockwise) { + intersects_arc = (!strictly_lesser(ray_angle, start_angle) && !strictly_greater(ray_angle, end_angle)); + } else { + intersects_arc = (!strictly_greater(ray_angle, start_angle) && !strictly_lesser(ray_angle, end_angle)); + } + + if (intersects_arc) { + intersection_count++; + } + } + } + } + + // If the number of intersections is odd, the point is inside the shape + return (intersection_count % 2 == 1); +} diff --git a/src/irregular/shape_inflate.cpp b/src/irregular/shape_inflate.cpp new file mode 100644 index 000000000..1b0d0cb05 --- /dev/null +++ b/src/irregular/shape_inflate.cpp @@ -0,0 +1,459 @@ +#include "packingsolver/irregular/shape.hpp" +#include "irregular/shape_inflate.hpp" +#include "irregular/shape_self_intersections_removal.hpp" +#include + +// Define M_PI (if not provided by the system) +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +namespace packingsolver +{ +namespace irregular +{ + +// Determine vertex type (convex/concave/regular) +enum class VertexType { + Convex, // Convex vertex (outer angle < 180 degrees) + Concave, // Concave vertex (outer angle > 180 degrees) + Regular // Non-corner vertex +}; + +// Detect vertex type by analyzing adjacent elements +VertexType detect_vertex_type(const Point& prev_end, const Point& curr_start, const Point& curr_end) { + // Ensure the current point is a corner point + if (equal(prev_end, curr_start) && !equal(prev_end, curr_end)) { + // Calculate vectors + Point v1 = {prev_end.x - curr_start.x, prev_end.y - curr_start.y}; + Point v2 = {curr_end.x - curr_start.x, curr_end.y - curr_start.y}; + + // Use cross product to determine vertex type + LengthDbl cross = cross_product(v1, v2); + + if (std::abs(cross) < 1e-10) { + return VertexType::Regular; // Collinear point + } else if (cross > 0) { + return VertexType::Convex; // Convex vertex (outer angle < 180 degrees) + } else { + return VertexType::Concave; // Concave vertex (outer angle > 180 degrees) + } + } + + return VertexType::Regular; +} + +Shape close_inflated_elements( + const std::vector& inflated_elements, + const std::vector>& original_to_inflated_mapping, + bool is_deflating) +{ + if (inflated_elements.empty()) { + return Shape(); + } + + //std::cout << "\n------ Debug: close_inflated_elements ------" << std::endl; + //std::cout << "Is deflating: " << (is_deflating ? "true" : "false") << std::endl; + //std::cout << "Inflated elements: " << inflated_elements.size() << std::endl; + + // Create a new elements array with the first element copied to the end to ensure closure + std::vector closed_elements = inflated_elements; + if (inflated_elements.size() > 1) { + closed_elements.push_back(inflated_elements[0]); + } + + Shape inflated_shape; + inflated_shape.elements.reserve(closed_elements.size() * 2); // Reserve space for connector elements + + // Iterate through elements (excluding the last copied element) + for (size_t i = 0; i < closed_elements.size() - 1; ++i) { + const ShapeElement& current = closed_elements[i]; + const ShapeElement& next = closed_elements[i + 1]; + + // Add the current element + inflated_shape.elements.push_back(current); + + //std::cout << "Processing element " << i << " to " << (i+1) << std::endl; + //std::cout << " Current: type=" << (current.type == ShapeElementType::LineSegment ? "LineSegment" : "CircularArc") + // << ", start=(" << current.start.x << "," << current.start.y + // << "), end=(" << current.end.x << "," << current.end.y << ")" << std::endl; + //std::cout << " Next: type=" << (next.type == ShapeElementType::LineSegment ? "LineSegment" : "CircularArc") + // << ", start=(" << next.start.x << "," << next.start.y + // << "), end=(" << next.end.x << "," << next.end.y << ")" << std::endl; + + // Check if a connector element is needed + if (!equal(current.end, next.start)) { + // The end point of the current element and the start point of the next element don't coincide, need to add a connector + //std::cout << " Need connector (gap > 1e-6)" << std::endl; + ShapeElement connector; + connector.type = ShapeElementType::CircularArc; + connector.start = current.end; + connector.end = next.start; + const ShapeElement& current_orig_elem = original_to_inflated_mapping[i].first; + connector.center = current_orig_elem.end; + if (is_deflating) { + // for deflation, add a clockwise circular arc connector on corner points + connector.anticlockwise = false; + } else { + // For inflation, add a anti-clockwise circular arc connector on corner points + connector.anticlockwise = true; + } + inflated_shape.elements.push_back(connector); + } + } + + // Remove duplicate points or degenerate elements + if (inflated_shape.elements.size() > 1) { + std::vector cleaned_elements; + cleaned_elements.reserve(inflated_shape.elements.size()); + + for (const ShapeElement& element : inflated_shape.elements) { + if (!equal(element.start, element.end)) { + cleaned_elements.push_back(element); + } + } + + inflated_shape.elements = cleaned_elements; + } + + return inflated_shape; +} + +Shape inflate_shape_without_holes( + const Shape& original_shape, + LengthDbl value) +{ + if (original_shape.elements.empty()) { + return original_shape; + } + + bool is_deflating = value < 0; + + // Create new shape + Shape inflated_shape; + std::vector temp_elements; + temp_elements.reserve(original_shape.elements.size() * 2); // Reserve space for potential additional arcs + + // If shape is too small for deflation, return empty shape + if (is_deflating) { + // Calculate minimum edge length or radius to ensure shape won't disappear + LengthDbl min_size = std::numeric_limits::max(); + for (const auto& element : original_shape.elements) { + if (element.type == ShapeElementType::LineSegment) { + min_size = std::min(min_size, distance(element.start, element.end)); + } else if (element.type == ShapeElementType::CircularArc) { + min_size = std::min(min_size, distance(element.center, element.start)); + } + } + + // If deflation value is too large, return empty shape + if (std::abs(value) >= min_size / 2) { + return Shape(); + } + } + + // Step 1: Detect vertex types for all vertices + std::vector vertex_types; + vertex_types.reserve(original_shape.elements.size()); + + // Pre-process closed shape special cases + const size_t num_elements = original_shape.elements.size(); + for (size_t i = 0; i < num_elements; ++i) { + const ShapeElement& curr_element = original_shape.elements[i]; + const ShapeElement& prev_element = original_shape.elements[(i + num_elements - 1) % num_elements]; + + // Detect current vertex type + VertexType vtype = detect_vertex_type(prev_element.end, curr_element.start, curr_element.end); + vertex_types.push_back(vtype); + } + + // Step 2: Process each element for inflation/deflation + for (size_t i = 0; i < num_elements; ++i) { + const ShapeElement& element = original_shape.elements[i]; + const VertexType curr_vertex_type = vertex_types[i]; + const VertexType next_vertex_type = vertex_types[(i + 1) % num_elements]; + + // Offset current element + ShapeElement offset_elem = offset_element(element, value); + + // If element is valid, add to temporary elements list + if (!is_degenerate_element(offset_elem)) { + temp_elements.push_back(offset_elem); + + // Based on next vertex type, decide whether to add connecting arc + if (next_vertex_type != VertexType::Regular) { + bool need_arc = false; + bool arc_anticlockwise = true; + + if (is_deflating) { + // For deflation, add clockwise arc at concave points + if (next_vertex_type == VertexType::Concave) { + need_arc = true; + arc_anticlockwise = false; // Clockwise + } + } else { + // For inflation, add counterclockwise arc at convex points + if (next_vertex_type == VertexType::Convex) { + need_arc = true; + arc_anticlockwise = true; // Counterclockwise + } + } + + // If need to add arc + if (need_arc) { + // Get next offset element + const ShapeElement& next_element = original_shape.elements[(i + 1) % num_elements]; + ShapeElement next_offset = offset_element(next_element, value); + + // Check next element is degenerate + if (is_degenerate_element(next_offset)) { + // Find next non-degenerate element + size_t next_valid_idx = (i + 1) % num_elements; + ShapeElement next_valid_offset; + const ShapeElement* next_valid_element = nullptr; + bool found_valid = false; + + // Search backward until find a non-degenerate element + for (size_t j = 1; j < num_elements; ++j) { + next_valid_idx = (i + j) % num_elements; + const ShapeElement& candidate = original_shape.elements[next_valid_idx]; + next_valid_offset = offset_element(candidate, value); + + if (!is_degenerate_element(next_valid_offset)) { + found_valid = true; + next_valid_element = &candidate; + break; + } + } + + // If no valid next element, skip arc addition + if (!found_valid) { + continue; + } + + // Calculate vector from current point to next valid element start point + Point direction = { + next_valid_element->start.x - element.end.x, + next_valid_element->start.y - element.end.y + }; + LengthDbl dir_length = std::sqrt(direction.x * direction.x + direction.y * direction.y); + if (dir_length < 1e-10) continue; // If vector too short, skip + + // Calculate normal direction (right-hand normal) + Point normal = { + -direction.y / dir_length, + direction.x / dir_length + }; + + // Calculate first arc end point (intersection of ray and circle) + Point arc1_end = { + element.end.x + std::abs(value) * normal.x, + element.end.y + std::abs(value) * normal.y + }; + + // Add first arc + ShapeElement arc1; + arc1.type = ShapeElementType::CircularArc; + arc1.start = offset_elem.end; + arc1.end = arc1_end; + arc1.center = element.end; + arc1.anticlockwise = arc_anticlockwise; + + // Add second arc + ShapeElement arc2; + arc2.type = ShapeElementType::CircularArc; + arc2.start = arc1_end; + arc2.end = next_valid_offset.start; + arc2.center = next_valid_element->start; + arc2.anticlockwise = arc_anticlockwise; + + // Only add arc if it's not degenerate + if (!is_degenerate_element(arc1)) { + temp_elements.push_back(arc1); + } + if (!is_degenerate_element(arc2)) { + temp_elements.push_back(arc2); + } + } else { + // Normal processing logic + ShapeElement arc; + arc.type = ShapeElementType::CircularArc; + arc.start = offset_elem.end; + arc.end = next_offset.start; + arc.center = element.end; + arc.anticlockwise = arc_anticlockwise; + + // Only add arc if it's not degenerate + if (!is_degenerate_element(arc)) { + temp_elements.push_back(arc); + } + } + } + } + } + } + + // If no valid elements, return original shape + if (temp_elements.empty()) { + return original_shape; + } + + // Step 3: Handle self-intersections + std::vector non_intersecting_elements = + remove_intersections_segments(temp_elements, is_deflating); + + // Build final shape + inflated_shape.elements = non_intersecting_elements; + + return inflated_shape; +} + +std::pair> inflate( + const Shape& shape, + LengthDbl value, + const std::vector& holes) +{ + if (shape.elements.empty()) { + return {shape, {}}; + } + + // use the improved inflate_shape_without_holes function to process the main shape + // (internal implementation of intersection detection and closure processing) + Shape inflated_shape = inflate_shape_without_holes(shape, value); + + // process the original holes (using the opposite inflation value) + std::vector deflated_holes; + for (const Shape& hole : holes) { + // process the original holes (using the opposite inflation value) + Shape deflated_hole = inflate_shape_without_holes(hole, -value); + deflated_holes.push_back(deflated_hole); + } + + // return the inflated shape and processed holes + return {inflated_shape, deflated_holes}; +} + +// Helper function to offset an element by a given value +ShapeElement offset_element(const ShapeElement& element, LengthDbl value) +{ + ShapeElement new_element; + + // Process based on element type + if (element.type == ShapeElementType::LineSegment) { + // LineSegment processing + LengthDbl dx = element.end.x - element.start.x; + LengthDbl dy = element.end.y - element.start.y; + LengthDbl length = distance(element.start, element.end); + + // Avoid division by zero + if (length > 0) { + // Calculate normal vector - always pointing outward from the shape + LengthDbl nx = dy / length; + LengthDbl ny = -dx / length; + + // Create new line segment + new_element.type = ShapeElementType::LineSegment; + new_element.start = Point{ + element.start.x + nx * value, + element.start.y + ny * value + }; + new_element.end = Point{ + element.end.x + nx * value, + element.end.y + ny * value + }; + + //std::cout << "Offset LineSegment: normal=(" << nx << "," << ny + // << "), start=(" << new_element.start.x << "," << new_element.start.y + // << "), end=(" << new_element.end.x << "," << new_element.end.y << ")" << std::endl; + } + } else if (element.type == ShapeElementType::CircularArc) { + // CircularArc processing + + // Calculate the current radius of the arc + LengthDbl radius = distance(element.center, element.start); + + // Calculate new radius based on arc direction + LengthDbl new_radius; + if (element.anticlockwise) { + // If anticlockwise (convex), positive value increases radius (expands), + // negative value decreases radius (contracts) + new_radius = radius + value; + } else { + // If clockwise (concave), positive value decreases radius (contracts), + // negative value increases radius (expands) + new_radius = radius - value; + } + + + // Calculate the original arc's start and end angles + Angle start_angle = angle_radian(element.start - element.center); + Angle end_angle = angle_radian(element.end - element.center); + + // Ensure angles are in the correct range + if (element.anticlockwise && end_angle <= start_angle) { + end_angle += 2 * M_PI; + } else if (!element.anticlockwise && start_angle <= end_angle) { + start_angle += 2 * M_PI; + } + + // Create the inflated arc + new_element.type = ShapeElementType::CircularArc; + new_element.center = element.center; // Keep the center unchanged + new_element.anticlockwise = element.anticlockwise; // Keep rotation direction unchanged + + // Calculate new start and end points + new_element.start = Point{ + element.center.x + new_radius * std::cos(start_angle), + element.center.y + new_radius * std::sin(start_angle) + }; + new_element.end = Point{ + element.center.x + new_radius * std::cos(end_angle), + element.center.y + new_radius * std::sin(end_angle) + }; + + //std::cout << "Offset CircularArc: center=(" << new_element.center.x << "," << new_element.center.y + // << "), radius=" << new_radius + // << ", start=(" << new_element.start.x << "," << new_element.start.y + // << "), end=(" << new_element.end.x << "," << new_element.end.y + // << "), anticlockwise=" << (new_element.anticlockwise ? "true" : "false") << std::endl; + } + + return new_element; +} + +// Helper function to check if an element is degenerate (too small) +bool is_degenerate_element(const ShapeElement& element) +{ + if (element.type == ShapeElementType::LineSegment) { + return equal(element.start, element.end); + } else if (element.type == ShapeElementType::CircularArc) { + // If the radius is very small, consider it degenerate + if (equal(element.center, element.end)) { + return true; + } + // If start and end points are too close, consider it degenerate + if (equal(element.start, element.end)) { + return true; + } + } + + return false; +} + +} +} + + +/* + * Minkowski sum implementation: + * 1. First detect convex/concave vertices on the original shape + * 2. Then offset each original element along its normal direction + * 3. Based on inflation/deflation value: + * - For positive value (inflation): + * - Add counterclockwise arcs at convex points + * - No special handling for concave points + * - For negative value (deflation): + * - Add clockwise arcs at concave points + * - No special handling for convex points + * 4. Finally detect and remove self-intersections + */ diff --git a/src/irregular/shape_inflate.hpp b/src/irregular/shape_inflate.hpp new file mode 100644 index 000000000..ac49fa3bb --- /dev/null +++ b/src/irregular/shape_inflate.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "packingsolver/irregular/instance.hpp" + +namespace packingsolver +{ +namespace irregular +{ + +Shape inflate_shape_without_holes( + const Shape& shape, + LengthDbl value); + +std::pair> inflate( + const Shape& shape, + LengthDbl value, + const std::vector& holes = {}); + +ShapeElement offset_element(const ShapeElement& element, LengthDbl value); + +bool is_degenerate_element(const ShapeElement& element); + +bool is_arc_covered_by_adjacent_elements( + const ShapeElement& prev_element, + const ShapeElement& arc_element, + const ShapeElement& next_element, + LengthDbl value); + +} +} \ No newline at end of file diff --git a/src/irregular/shape_self_intersections_removal.cpp b/src/irregular/shape_self_intersections_removal.cpp index 721c88023..223f74059 100644 --- a/src/irregular/shape_self_intersections_removal.cpp +++ b/src/irregular/shape_self_intersections_removal.cpp @@ -5,6 +5,10 @@ using namespace packingsolver; using namespace packingsolver::irregular; +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + namespace { @@ -426,3 +430,258 @@ std::vector irregular::extract_all_holes_from_self_intersecting_hole( //std::cout << "extract_all_holes_from_self_intersecting_shape end" << std::endl; return new_holes; } + +std::vector packingsolver::irregular::remove_intersections_segments( + const std::vector& elements, + bool is_deflating) +{ + if (elements.empty()) { + return elements; + } + + //std::cout << "\n------ Debug: remove_intersections_segments ------" << std::endl; + //std::cout << "Input elements: " << elements.size() << std::endl; + //std::cout << "Is deflating: " << (is_deflating ? "true" : "false") << std::endl; + + // Copy input elements for processing + std::vector working_elements = elements; + + // Extend line segments to ensure intersection + for (size_t i = 0; i < working_elements.size(); ++i) { + if (working_elements[i].type != ShapeElementType::LineSegment) { + continue; + } + + LengthDbl dx = working_elements[i].end.x - working_elements[i].start.x; + LengthDbl dy = working_elements[i].end.y - working_elements[i].start.y; + // LengthDbl length = std::sqrt(dx*dx + dy*dy); + + if (!equal(working_elements[i].start, working_elements[i].end)) { + // Extend by 10% + LengthDbl extension_ratio = 0.1; + Point original_end = working_elements[i].end; + + working_elements[i].end.x = original_end.x + dx * extension_ratio; + working_elements[i].end.y = original_end.y + dy * extension_ratio; + + //std::cout << "Extended element " << i << " from (" + // << original_end.x << "," << original_end.y << ") to (" + // << working_elements[i].end.x << "," << working_elements[i].end.y << ")" << std::endl; + } + } + + // Find all intersections + std::vector> intersections(working_elements.size()); + + for (size_t i = 0; i < working_elements.size(); ++i) { + for (size_t j = i + 1; j < working_elements.size(); ++j) { + std::vector points = compute_intersections(working_elements[i], working_elements[j]); + + for (size_t k = 0; k < points.size(); ++k) { + intersections[i].push_back(points[k]); + intersections[j].push_back(points[k]); + //std::cout << "Found intersection between elements " << i << " and " << j + // << " at (" << points[k].x << "," << points[k].y << ")" << std::endl; + } + } + } + + // Find the bounding box of all intersection points + Point min_point = {std::numeric_limits::max(), std::numeric_limits::max()}; + Point max_point = {std::numeric_limits::lowest(), std::numeric_limits::lowest()}; + + bool has_intersections = false; + for (size_t i = 0; i < intersections.size(); ++i) { + for (size_t j = 0; j < intersections[i].size(); ++j) { + const Point& p = intersections[i][j]; + min_point.x = std::min(min_point.x, p.x); + min_point.y = std::min(min_point.y, p.y); + max_point.x = std::max(max_point.x, p.x); + max_point.y = std::max(max_point.y, p.y); + has_intersections = true; + } + } + + if (has_intersections) { + //std::cout << "Intersection bounding box: (" << min_point.x << "," << min_point.y + // << ") to (" << max_point.x << "," << max_point.y << ")" << std::endl; + } + + // Process results + std::vector result; + + for (size_t i = 0; i < elements.size(); ++i) { + const ShapeElement& element = elements[i]; + + // Skip degenerate elements + if (equal(element.start, element.end)) { + continue; + } + + // Get intersections for this element + std::vector element_intersections = intersections[i]; + + // If no intersections, handle based on deflation mode + if (element_intersections.empty()) { + if (is_deflating) { + // In deflation mode, need to check if this element should be included + bool is_inside_bbox = is_point_inside_box(element.start, min_point, max_point) && + is_point_inside_box(element.end, min_point, max_point); + if (is_inside_bbox) { + result.push_back(element); + //std::cout << "Kept element with no intersections (inside bbox)" << std::endl; + } else { + //std::cout << "Skipped element with no intersections (outside bbox)" << std::endl; + } + } else { + // For inflation, keep all elements + result.push_back(element); + } + continue; + } + + // Sort intersections by distance from start + std::sort(element_intersections.begin(), element_intersections.end(), + [&element](const Point& p1, const Point& p2) { + LengthDbl d1 = std::sqrt( + std::pow(p1.x - element.start.x, 2) + + std::pow(p1.y - element.start.y, 2)); + LengthDbl d2 = std::sqrt( + std::pow(p2.x - element.start.x, 2) + + std::pow(p2.y - element.start.y, 2)); + return d1 < d2; + }); + + // Remove endpoints + std::vector filtered_intersections; + for (size_t j = 0; j < element_intersections.size(); ++j) { + const Point& p = element_intersections[j]; + if (!equal(p, element.start) && !equal(p, element.end)) { + filtered_intersections.push_back(p); + } + } + + // Remove close points + std::vector final_intersections; + for (size_t j = 0; j < filtered_intersections.size(); ++j) { + bool should_add = true; + for (size_t k = 0; k < final_intersections.size(); ++k) { + if (equal(filtered_intersections[j], final_intersections[k])) { + should_add = false; + break; + } + } + if (should_add) { + final_intersections.push_back(filtered_intersections[j]); + } + } + + // If no intersections after filtering, handle same as earlier + if (final_intersections.empty()) { + if (is_deflating) { + // In deflation mode, need to check if this element should be included + bool is_inside_bbox = is_point_inside_box(element.start, min_point, max_point) && + is_point_inside_box(element.end, min_point, max_point); + if (is_inside_bbox) { + result.push_back(element); + //std::cout << "Kept element with no valid intersections (inside bbox)" << std::endl; + } else { + //std::cout << "Skipped element with no valid intersections (outside bbox)" << std::endl; + } + } else { + // For inflation, keep all elements + result.push_back(element); + } + continue; + } + + //std::cout << "Element " << i << " has " << final_intersections.size() << " valid intersection points" << std::endl; + + // Generate sub-segments + Point prev_point = element.start; + for (size_t j = 0; j < final_intersections.size(); ++j) { + const Point& current_point = final_intersections[j]; + + // Check if point is within original element + LengthDbl original_length = std::sqrt( + std::pow(element.end.x - element.start.x, 2) + + std::pow(element.end.y - element.start.y, 2)); + LengthDbl point_dist = std::sqrt( + std::pow(current_point.x - element.start.x, 2) + + std::pow(current_point.y - element.start.y, 2)); + + if (point_dist > original_length) { + //std::cout << "Skipping intersection point beyond original element end" << std::endl; + continue; + } + + // Create new segment + ShapeElement new_segment = element; + new_segment.start = prev_point; + new_segment.end = current_point; + + // Add only non-degenerate segments + if (!equal(new_segment.start, new_segment.end)) { + if (is_deflating) { + // For deflation, check if segment is inside the bounding box + // For partial segments, only keep the part inside the box + bool start_inside = is_point_inside_box(new_segment.start, min_point, max_point); + bool end_inside = is_point_inside_box(new_segment.end, min_point, max_point); + + if (start_inside && end_inside) { + // Segment is fully inside, keep it + result.push_back(new_segment); + //std::cout << "Added segment (fully inside bbox) from (" + // << new_segment.start.x << "," << new_segment.start.y + // << ") to (" << new_segment.end.x << "," << new_segment.end.y << ")" << std::endl; + } + // Note: For deflation, we skip segments that are partially or fully outside + } else { + // For inflation, keep all segments + result.push_back(new_segment); + //std::cout << "Added segment from (" << new_segment.start.x << "," << new_segment.start.y + // << ") to (" << new_segment.end.x << "," << new_segment.end.y << ")" << std::endl; + } + } + + prev_point = current_point; + } + + // Add final segment + ShapeElement last_segment = element; + last_segment.start = prev_point; + last_segment.end = element.end; + + // Add only non-degenerate segments + if (!equal(last_segment.start, last_segment.end)) { + if (is_deflating) { + // For deflation, check if segment is inside the bounding box + bool start_inside = is_point_inside_box(last_segment.start, min_point, max_point); + bool end_inside = is_point_inside_box(last_segment.end, min_point, max_point); + + if (start_inside && end_inside) { + // Segment is fully inside, keep it + result.push_back(last_segment); + //std::cout << "Added final segment (fully inside bbox) from (" + // << last_segment.start.x << "," << last_segment.start.y + // << ") to (" << last_segment.end.x << "," << last_segment.end.y << ")" << std::endl; + } + // Skip segments that are partially or fully outside + } else { + // For inflation, keep all segments + result.push_back(last_segment); + //std::cout << "Added final segment from (" << last_segment.start.x << "," << last_segment.start.y + // << ") to (" << last_segment.end.x << "," << last_segment.end.y << ")" << std::endl; + } + } + } + + //std::cout << "Final output elements: " << result.size() << std::endl; + return result; +} + +// Helper function to check if a point is inside a box +bool packingsolver::irregular::is_point_inside_box(const Point& p, const Point& min_corner, const Point& max_corner) { + return p.x >= min_corner.x && p.x <= max_corner.x && + p.y >= min_corner.y && p.y <= max_corner.y; +} diff --git a/src/irregular/shape_self_intersections_removal.hpp b/src/irregular/shape_self_intersections_removal.hpp index 49e62da09..a85445291 100644 --- a/src/irregular/shape_self_intersections_removal.hpp +++ b/src/irregular/shape_self_intersections_removal.hpp @@ -13,5 +13,13 @@ std::pair> remove_self_intersections( std::vector extract_all_holes_from_self_intersecting_hole( const Shape& shape); +// Remove intersections between shape elements, returning a set of non-intersecting segments +std::vector remove_intersections_segments( + const std::vector& elements, + bool is_deflating = false); + +// Helper function to check if a point is inside a box +bool is_point_inside_box(const Point& p, const Point& min_corner, const Point& max_corner); + } } diff --git a/test/irregular/CMakeLists.txt b/test/irregular/CMakeLists.txt index 6dffd6f71..b812e44cc 100644 --- a/test/irregular/CMakeLists.txt +++ b/test/irregular/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(PackingSolver_irregular_test PRIVATE shape_extract_borders_test.cpp shape_self_intersections_removal_test.cpp shape_trapezoidation_test.cpp + shape_inflate_test.cpp irregular_test.cpp) target_include_directories(PackingSolver_irregular_test PRIVATE ${PROJECT_SOURCE_DIR}/src) diff --git a/test/irregular/shape_inflate_test.cpp b/test/irregular/shape_inflate_test.cpp new file mode 100644 index 000000000..aee0bfe28 --- /dev/null +++ b/test/irregular/shape_inflate_test.cpp @@ -0,0 +1,496 @@ +#include "packingsolver/irregular/shape.hpp" +#include "irregular/shape_inflate.hpp" +#include "irregular/shape_self_intersections_removal.hpp" + +#include + +using namespace packingsolver; +using namespace packingsolver::irregular; + +// build square +Shape build_square(LengthDbl x, LengthDbl y, LengthDbl size) +{ + return build_shape({ + {x, y}, // bottom-left + {x + size, y}, // bottom-right + {x + size, y + size}, // top-right + {x, y + size} // top-left + }); +} + +// build rounded square (for inflated shapes) +Shape build_rounded_square(LengthDbl x, LengthDbl y, LengthDbl width, LengthDbl height, LengthDbl radius) +{ + // Directly create Shape object and its elements + Shape shape; + + // Element 0: Left edge - LineSegment + ShapeElement left; + left.type = ShapeElementType::LineSegment; + left.start = {x, y + height - radius}; + left.end = {x, y + radius}; + shape.elements.push_back(left); + + // Element 1: Bottom-left corner - CircularArc + ShapeElement bottom_left_corner; + bottom_left_corner.type = ShapeElementType::CircularArc; + bottom_left_corner.start = {x, y + radius}; + bottom_left_corner.end = {x + radius, y}; + bottom_left_corner.center = {x + radius, y + radius}; + bottom_left_corner.anticlockwise = true; // Counterclockwise + shape.elements.push_back(bottom_left_corner); + + // Element 2: Bottom edge - LineSegment + ShapeElement bottom; + bottom.type = ShapeElementType::LineSegment; + bottom.start = {x + radius, y}; + bottom.end = {x + width - radius, y}; + shape.elements.push_back(bottom); + + // Element 3: Bottom-right corner - CircularArc + ShapeElement bottom_right_corner; + bottom_right_corner.type = ShapeElementType::CircularArc; + bottom_right_corner.start = {x + width - radius, y}; + bottom_right_corner.end = {x + width, y + radius}; + bottom_right_corner.center = {x + width - radius, y + radius}; + bottom_right_corner.anticlockwise = true; // Counterclockwise + shape.elements.push_back(bottom_right_corner); + + // Element 4: Right edge - LineSegment + ShapeElement right; + right.type = ShapeElementType::LineSegment; + right.start = {x + width, y + radius}; + right.end = {x + width, y + height - radius}; + shape.elements.push_back(right); + + // Element 5: Top-right corner - CircularArc + ShapeElement top_right_corner; + top_right_corner.type = ShapeElementType::CircularArc; + top_right_corner.start = {x + width, y + height - radius}; + top_right_corner.end = {x + width - radius, y + height}; + top_right_corner.center = {x + width - radius, y + height - radius}; + top_right_corner.anticlockwise = true; // Counterclockwise + shape.elements.push_back(top_right_corner); + + // Element 6: Top edge - LineSegment + ShapeElement top; + top.type = ShapeElementType::LineSegment; + top.start = {x + width - radius, y + height}; + top.end = {x + radius, y + height}; + shape.elements.push_back(top); + + // Element 7: Top-left corner - CircularArc + ShapeElement top_left_corner; + top_left_corner.type = ShapeElementType::CircularArc; + top_left_corner.start = {x + radius, y + height}; + top_left_corner.end = {x, y + height - radius}; + top_left_corner.center = {x + radius, y + height - radius}; + top_left_corner.anticlockwise = true; // Counterclockwise + shape.elements.push_back(top_left_corner); + + return shape; +} + +// build triangle +Shape build_triangle(LengthDbl x, LengthDbl y, LengthDbl size) +{ + return build_shape({ + {x, y}, + {x + size, y}, + {x + size / 2, y + size} + }); +} + +// test 1: test basic inflation - no holes +TEST(IrregularShapeInflate, BasicInflate) +{ + // build a simple square + Shape square = build_square(0, 0, 10); + + // inflate shape + LengthDbl inflation_value = 2.0; + auto inflation_result = inflate(square, inflation_value); + Shape inflated_square = inflation_result.first; + + // verify the inflated shape + // the square's edges should be moved outwards by inflation_value units + + // Check element count - inflated square becomes a rounded rectangle + // 4 edges + 4 circular arcs + EXPECT_EQ(inflated_square.elements.size(), 8); + + // Build the expected inflated rounded rectangle shape + Shape expected_inflated_square = build_rounded_square( + -inflation_value, -inflation_value, + 10 + 2 * inflation_value, 10 + 2 * inflation_value, + inflation_value); + + + + // // Debug output: print each element + // std::cout << "\nActual shape elements:\n"; + // for (size_t i = 0; i < inflated_square.elements.size(); ++i) { + // const auto& element = inflated_square.elements[i]; + // std::cout << "Element " << i << ": "; + // if (element.type == ShapeElementType::CircularArc) { + // std::cout << "CircularArc start (" << element.start.x << ", " << element.start.y + // << ") end (" << element.end.x << ", " << element.end.y + // << ") center (" << element.center.x << ", " << element.center.y + // << ") " << (element.anticlockwise ? "anticlockwise" : "clockwise") << "\n"; + // } else { + // std::cout << "LineSegment start (" << element.start.x << ", " << element.start.y + // << ") end (" << element.end.x << ", " << element.end.y << ")\n"; + // } + // } + + // std::cout << "\nExpected shape elements:\n"; + // for (size_t i = 0; i < expected_inflated_square.elements.size(); ++i) { + // const auto& element = expected_inflated_square.elements[i]; + // std::cout << "Element " << i << ": "; + // if (element.type == ShapeElementType::CircularArc) { + // std::cout << "CircularArc start (" << element.start.x << ", " << element.start.y + // << ") end (" << element.end.x << ", " << element.end.y + // << ") center (" << element.center.x << ", " << element.center.y + // << ") " << (element.anticlockwise ? "anticlockwise" : "clockwise") << "\n"; + // } else { + // std::cout << "LineSegment start (" << element.start.x << ", " << element.start.y + // << ") end (" << element.end.x << ", " << element.end.y << ")\n"; + // } + // } + + // Compare shapes element by element using the equal() function + ASSERT_EQ(inflated_square.elements.size(), expected_inflated_square.elements.size()); + + for (size_t i = 0; i < inflated_square.elements.size(); ++i) { + EXPECT_TRUE(equal(inflated_square.elements[i], expected_inflated_square.elements[i])); + } + + // expected new boundary coordinates + LengthDbl expected_min_x = -inflation_value; + LengthDbl expected_min_y = -inflation_value; + LengthDbl expected_max_x = 10 + inflation_value; + LengthDbl expected_max_y = 10 + inflation_value; + + // check if the inflated shape contains the expected external points + bool contains_min_x = false; + bool contains_min_y = false; + bool contains_max_x = false; + bool contains_max_y = false; + + for (const ShapeElement& element : inflated_square.elements) { + if (element.start.x <= expected_min_x || element.end.x <= expected_min_x) { + contains_min_x = true; + } + if (element.start.y <= expected_min_y || element.end.y <= expected_min_y) { + contains_min_y = true; + } + if (element.start.x >= expected_max_x || element.end.x >= expected_max_x) { + contains_max_x = true; + } + if (element.start.y >= expected_max_y || element.end.y >= expected_max_y) { + contains_max_y = true; + } + } + + EXPECT_TRUE(contains_min_x); + EXPECT_TRUE(contains_min_y); + EXPECT_TRUE(contains_max_x); + EXPECT_TRUE(contains_max_y); +} + +// test 2: test inflation with holes +TEST(IrregularShapeInflate, InflateWithHoles) +{ + // build a square as the outer shape + Shape outer_square = build_square(0, 0, 20); + + // build a small square as the hole + Shape hole = build_square(5, 5, 10); + + // inflate the shape with the hole + LengthDbl inflation_value = 2.0; + auto inflation_result = inflate(outer_square, inflation_value, {hole}); + Shape inflated_shape = inflation_result.first; + std::vector inflated_holes = inflation_result.second; + + // Check the external shape after inflation + // Build the expected external inflated rounded rectangle shape + Shape expected_inflated_outer = build_rounded_square( + -inflation_value, -inflation_value, + 20 + 2 * inflation_value, 20 + 2 * inflation_value, + inflation_value); + + // Compare external shape element by element using the equal() function + ASSERT_EQ(inflated_shape.elements.size(), expected_inflated_outer.elements.size()); + for (size_t i = 0; i < inflated_shape.elements.size(); ++i) { + EXPECT_TRUE(equal(inflated_shape.elements[i], expected_inflated_outer.elements[i])); + } + + // Verify the hole shape - should still be a square after contraction + EXPECT_FALSE(inflated_holes.empty()); + if (!inflated_holes.empty()) { + // Contracted hole should still be a square, not rounded + Shape expected_inflated_hole = build_square( + 5 + inflation_value, + 5 + inflation_value, + 10 - 2 * inflation_value); + + // Compare hole shape element by element using the equal() function + ASSERT_EQ(inflated_holes[0].elements.size(), expected_inflated_hole.elements.size()); + for (size_t i = 0; i < inflated_holes[0].elements.size(); ++i) { + EXPECT_TRUE(equal(inflated_holes[0].elements[i], expected_inflated_hole.elements[i])); + } + } + + // verify the inflated shape + // the outer boundary should be expanded, and the hole should be contracted + + // expected new outer boundary coordinates + LengthDbl expected_outer_min_x = -inflation_value; + LengthDbl expected_outer_min_y = -inflation_value; + LengthDbl expected_outer_max_x = 20 + inflation_value; + LengthDbl expected_outer_max_y = 20 + inflation_value; + + // verify the inflated shape + bool contains_outer_min_x = false; + bool contains_outer_min_y = false; + bool contains_outer_max_x = false; + bool contains_outer_max_y = false; + + for (const ShapeElement& element : inflated_shape.elements) { + if (element.start.x <= expected_outer_min_x || element.end.x <= expected_outer_min_x) { + contains_outer_min_x = true; + } + if (element.start.y <= expected_outer_min_y || element.end.y <= expected_outer_min_y) { + contains_outer_min_y = true; + } + if (element.start.x >= expected_outer_max_x || element.end.x >= expected_outer_max_x) { + contains_outer_max_x = true; + } + if (element.start.y >= expected_outer_max_y || element.end.y >= expected_outer_max_y) { + contains_outer_max_y = true; + } + } + + EXPECT_TRUE(contains_outer_min_x); + EXPECT_TRUE(contains_outer_min_y); + EXPECT_TRUE(contains_outer_max_x); + EXPECT_TRUE(contains_outer_max_y); + + // Also verify the inflated holes if needed + EXPECT_FALSE(inflated_holes.empty()); +} + +// test 3: test shape deflation +TEST(IrregularShapeInflate, Deflate) +{ + // build a large square + Shape square = build_square(0, 0, 20); + + // deflate shape (use negative inflation value) + LengthDbl deflation_value = -5.0; + auto inflation_result = inflate(square, deflation_value); + Shape deflated_square = inflation_result.first; + + // Build the expected contracted rectangle shape (remains rectangular, not rounded) + Shape expected_deflated_square = build_square( + -deflation_value, + -deflation_value, + 20 + 2 * deflation_value); + + // Compare shapes element by element using the equal() function + ASSERT_EQ(deflated_square.elements.size(), expected_deflated_square.elements.size()); + for (size_t i = 0; i < deflated_square.elements.size(); ++i) { + EXPECT_TRUE(equal(deflated_square.elements[i], expected_deflated_square.elements[i])); + } + + // verify the deflated shape + // the square's edges should be moved inwards by |deflation_value| units + + // expected new boundary coordinates + LengthDbl expected_min_x = -deflation_value; + LengthDbl expected_min_y = -deflation_value; + LengthDbl expected_max_x = 20 + deflation_value; + LengthDbl expected_max_y = 20 + deflation_value; + + // check the boundary of the deflated shape + LengthDbl actual_min_x = std::numeric_limits::max(); + LengthDbl actual_min_y = std::numeric_limits::max(); + LengthDbl actual_max_x = std::numeric_limits::lowest(); + LengthDbl actual_max_y = std::numeric_limits::lowest(); + + for (const ShapeElement& element : deflated_square.elements) { + actual_min_x = std::min({actual_min_x, element.start.x, element.end.x}); + actual_min_y = std::min({actual_min_y, element.start.y, element.end.y}); + actual_max_x = std::max({actual_max_x, element.start.x, element.end.x}); + actual_max_y = std::max({actual_max_y, element.start.y, element.end.y}); + } + + EXPECT_TRUE(equal(actual_min_x, expected_min_x)); + EXPECT_TRUE(equal(actual_min_y, expected_min_y)); + EXPECT_TRUE(equal(actual_max_x, expected_max_x)); + EXPECT_TRUE(equal(actual_max_y, expected_max_y)); +} + +// test 4: test the case that the shape is too small +TEST(IrregularShapeInflate, TinyShape) +{ + // build a very small square + Shape tiny_square = build_square(0, 0, 0.1); + + // inflate shape + LengthDbl inflation_value = 0.5; + auto inflation_result = inflate(tiny_square, inflation_value); + Shape inflated_square = inflation_result.first; + + // Build the expected inflated rounded rectangle shape + Shape expected_inflated_square = build_rounded_square( + -inflation_value, -inflation_value, + 0.1 + 2 * inflation_value, 0.1 + 2 * inflation_value, + inflation_value); + + // Debug output: print each element + // std::cout << "\nActual shape elements:\n"; + // for (size_t i = 0; i < inflated_square.elements.size(); ++i) { + // const auto& element = inflated_square.elements[i]; + // std::cout << "Element " << i << ": "; + // if (element.type == ShapeElementType::CircularArc) { + // std::cout << "CircularArc start (" << element.start.x << ", " << element.start.y + // << ") end (" << element.end.x << ", " << element.end.y + // << ") center (" << element.center.x << ", " << element.center.y + // << ") " << (element.anticlockwise ? "anticlockwise" : "clockwise") << "\n"; + // } else { + // std::cout << "LineSegment start (" << element.start.x << ", " << element.start.y + // << ") end (" << element.end.x << ", " << element.end.y << ")\n"; + // } + // } + + // std::cout << "\nExpected shape elements:\n"; + // for (size_t i = 0; i < expected_inflated_square.elements.size(); ++i) { + // const auto& element = expected_inflated_square.elements[i]; + // std::cout << "Element " << i << ": "; + // if (element.type == ShapeElementType::CircularArc) { + // std::cout << "CircularArc start (" << element.start.x << ", " << element.start.y + // << ") end (" << element.end.x << ", " << element.end.y + // << ") center (" << element.center.x << ", " << element.center.y + // << ") " << (element.anticlockwise ? "anticlockwise" : "clockwise") << "\n"; + // } else { + // std::cout << "LineSegment start (" << element.start.x << ", " << element.start.y + // << ") end (" << element.end.x << ", " << element.end.y << ")\n"; + // } + // } + + // Compare shapes element by element using the equal() function + ASSERT_EQ(inflated_square.elements.size(), expected_inflated_square.elements.size()); + for (size_t i = 0; i < inflated_square.elements.size(); ++i) { + EXPECT_TRUE(equal(inflated_square.elements[i], expected_inflated_square.elements[i])); + } + + // expected new boundary coordinates + LengthDbl expected_min_x = -inflation_value; + LengthDbl expected_min_y = -inflation_value; + LengthDbl expected_max_x = 0.1 + inflation_value; + LengthDbl expected_max_y = 0.1 + inflation_value; + + // check if the inflated shape contains the expected external points + bool contains_min_x = false; + bool contains_min_y = false; + bool contains_max_x = false; + bool contains_max_y = false; + + for (const ShapeElement& element : inflated_square.elements) { + if (element.start.x <= expected_min_x || element.end.x <= expected_min_x) { + contains_min_x = true; + } + if (element.start.y <= expected_min_y || element.end.y <= expected_min_y) { + contains_min_y = true; + } + if (element.start.x >= expected_max_x || element.end.x >= expected_max_x) { + contains_max_x = true; + } + if (element.start.y >= expected_max_y || element.end.y >= expected_max_y) { + contains_max_y = true; + } + } + + EXPECT_TRUE(contains_min_x); + EXPECT_TRUE(contains_min_y); + EXPECT_TRUE(contains_max_x); + EXPECT_TRUE(contains_max_y); + + // Also check the actual boundary coordinates + LengthDbl actual_min_x = std::numeric_limits::max(); + LengthDbl actual_min_y = std::numeric_limits::max(); + LengthDbl actual_max_x = std::numeric_limits::lowest(); + LengthDbl actual_max_y = std::numeric_limits::lowest(); + + for (const ShapeElement& element : inflated_square.elements) { + actual_min_x = std::min({actual_min_x, element.start.x, element.end.x}); + actual_min_y = std::min({actual_min_y, element.start.y, element.end.y}); + actual_max_x = std::max({actual_max_x, element.start.x, element.end.x}); + actual_max_y = std::max({actual_max_y, element.start.y, element.end.y}); + } + + // std::cout << "Actual boundaries: min_x=" << actual_min_x << ", min_y=" << actual_min_y + // << ", max_x=" << actual_max_x << ", max_y=" << actual_max_y << std::endl; + // std::cout << "Expected boundaries: min_x=" << expected_min_x << ", min_y=" << expected_min_y + // << ", max_x=" << expected_max_x << ", max_y=" << expected_max_y << std::endl; + + EXPECT_TRUE(equal(actual_min_x, expected_min_x)); + EXPECT_TRUE(equal(actual_min_y, expected_min_y)); + EXPECT_TRUE(equal(actual_max_x, expected_max_x)); + EXPECT_TRUE(equal(actual_max_y, expected_max_y)); +} + +// test 5: test the case that the inflation value is too large +TEST(IrregularShapeInflate, LargeInflation) +{ + // build a square + Shape square = build_square(0, 0, 10); + + // use a large inflation value + LengthDbl inflation_value = 100.0; + auto inflation_result = inflate(square, inflation_value); + Shape inflated_square = inflation_result.first; + + // Build the expected inflated rounded rectangle shape + Shape expected_inflated_square = build_rounded_square( + -inflation_value, -inflation_value, + 10 + 2 * inflation_value, 10 + 2 * inflation_value, + inflation_value); + + // Compare shapes element by element using the equal() function + ASSERT_EQ(inflated_square.elements.size(), expected_inflated_square.elements.size()); + for (size_t i = 0; i < inflated_square.elements.size(); ++i) { + EXPECT_TRUE(equal(inflated_square.elements[i], expected_inflated_square.elements[i])); + } + + // verify the inflated shape + EXPECT_GT(inflated_square.elements.size(), 0); + + // after inflation, the square's edges should be moved outwards by inflation_value units + // expected new boundary coordinates + LengthDbl expected_min_x = -inflation_value; + LengthDbl expected_min_y = -inflation_value; + LengthDbl expected_max_x = 10 + inflation_value; + LengthDbl expected_max_y = 10 + inflation_value; + + // check the boundary of the inflated shape + LengthDbl actual_min_x = std::numeric_limits::max(); + LengthDbl actual_min_y = std::numeric_limits::max(); + LengthDbl actual_max_x = std::numeric_limits::lowest(); + LengthDbl actual_max_y = std::numeric_limits::lowest(); + + for (const ShapeElement& element : inflated_square.elements) { + actual_min_x = std::min({actual_min_x, element.start.x, element.end.x}); + actual_min_y = std::min({actual_min_y, element.start.y, element.end.y}); + actual_max_x = std::max({actual_max_x, element.start.x, element.end.x}); + actual_max_y = std::max({actual_max_y, element.start.y, element.end.y}); + } + + + EXPECT_TRUE(equal(actual_min_x, expected_min_x)); + EXPECT_TRUE(equal(actual_min_y, expected_min_y)); + EXPECT_TRUE(equal(actual_max_x, expected_max_x)); + EXPECT_TRUE(equal(actual_max_y, expected_max_y)); +} \ No newline at end of file