diff --git a/.gitignore b/.gitignore index 0db78dd..036c5c9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ install *.ts *.qm .vscode +.clangd package dist diff --git a/CMakeLists.txt b/CMakeLists.txt index afafa37..d6b023c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ if (NOT DEFINED PROJECT_NAME) set(NOT_SUBPROJECT ON) endif() -project(CavalierContours VERSION 0.1) +project(CavalierContours VERSION 0.1.0) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..247b96e --- /dev/null +++ b/Changelog.md @@ -0,0 +1,81 @@ +# Changelog + +## Unreleased + +- Add runtime-configurable geometry tolerances in `mathutils`: + - `setEpsilonConfig`, `getEpsilonConfig`, `resetEpsilonConfig` + - preserve existing defaults (`1e-8`, `1e-5`, `1e-4`, `1e-4`) +- Add point containment classification in C++ API: + - `PointContainment` enum + - `getPointContainment` (Outside/Inside/OnBoundary) +- Add polyline preprocessing in C++ API: + - `ClosedPolylineWinding` enum + - `normalizePolyline` (prune singularities + optional closed winding normalization) +- Expand C API surface with low-risk geometry helpers: + - `cavc_pline_invert_direction` + - `cavc_pline_prune_singularities` + - `cavc_pline_convert_arcs_to_lines` + - `cavc_pline_normalize` + - `cavc_spatial_index_create/delete/item_count/query_count/query` + - `cavc_get_point_containment` + - `cavc_get_tolerances`, `cavc_set_tolerances`, `cavc_reset_tolerances` +- Expand C API test coverage: + - add `TEST_cavc_api_regression` for legacy mutating chains and new API regressions + - fix path-length skip guard in `TEST_cavc_pline_function` + - harden `plineFromVertexes` helper to safely support empty input + - refactor `combine_with_self_invariants` into closed-only parameterized suite (remove stale + runtime skips on open half-circle cases) +- Unify parallel offset options shape for upcoming Join/EndCap work: + - C++: add `ParallelOffsetOptions`, `OffsetJoinType`, `OffsetEndCapType` + - C API: replace offset bit flags with `cavc_parallel_offset_options` + - add `cavc_parallel_offset_default_options` + - keep behavior unchanged: only `round` join/end cap are currently implemented +- Extend join behavior incrementally: + - add `miter/bevel` for line-line joins in `parallelOffset` + - add `miter_limit` option (C++ and C API) with line-line and arc-involved miter clipping + - add arc-involved non-round joins using endpoint tangent intersection + (collapsed-arc joins degrade to bevel) + - add regression coverage for `CAVC_OFFSET_JOIN_MITER` and `CAVC_OFFSET_JOIN_BEVEL`, including + arc-involved join scenarios + - add arc-heavy join stress matrix coverage for high-curvature / near-tangent / degenerate + repeated-vertex inputs (positive and negative deltas), including `miter_limit=1` bevel + equivalence and high-limit miter divergence checks +- Extend end cap behavior incrementally: + - add `square` end cap for open polylines with line start/end segments + (endpoint extension + dual-slice cap-circle shift) + - extend `square` end cap to arc start/end by preserving endpoint arc geometry and inserting + explicit cap line segments when needed + - separate `butt` from `round` for open polylines: + `butt` uses endpoint-normal line clipping while `round` keeps endpoint-circle clipping + - add regression coverage for `CAVC_OFFSET_END_CAP_SQUARE` on arc start/end for both + positive and negative deltas + - add regression coverage for `CAVC_OFFSET_END_CAP_BUTT` on arc start/end clip behavior + - add stress matrix coverage for `CAVC_OFFSET_END_CAP_BUTT` on open polylines: + high-curvature / near-tangent / multi-intersection, each with positive and negative offsets + - add degenerate open-polyline end cap stress matrix across `round/square/butt`: + repeated endpoints / near-zero-radius arcs / high-density self-intersection, each with positive + and negative offsets +- Extend offset-islands engineering: + - add stable geometry ordering for `ParallelOffsetIslands::compute` outputs + - add loop role/tag and topology helpers: + `OffsetLoopRole`, `OffsetLoopTopologyNode`, `buildOffsetLoopTopology` + - expose topology output in C API with ownership-safe handle: + `cavc_offset_loop_topology_build/delete/count/get` + - add dedicated regression test target `TEST_cavc_offset_islands` +- Performance-oriented tuning (no behavior change): + - reuse query buffers for open-polyline end-cap circle clipping in dual-slice offset path + - pre-reserve frequently reused containers in offset-islands slicing/topology flow + - add bounding-box early reject before expensive containment checks in topology parent search + - add batch spatial-index helper `createApproxSpatialIndices` + - switch `ParallelOffsetIslands` loop-index construction to batch path for + offset loops and stitched loops + - add `offsetislandsbenchmarks` for: + `scalar vs batch index build`, large-scale `buildOffsetLoopTopology`, + and large-scale `ParallelOffsetIslands::compute` + +## 0.1.0 + +- Add option to enable clipper library for benchmarks: `CAVC_ENABLE_CLIPPER_BENCHMARKS` +- Rename variables whose declarations shadow local/previous variables + - if parameter shadows the local variable, the local variable is renamed to `p_` + - if variable in function shadows the local variable, the parameter is renamed to `l_` diff --git a/README.md b/README.md index c0b9f7b..112ac8f 100644 --- a/README.md +++ b/README.md @@ -133,73 +133,73 @@ All offsets were performed with rounded joins (maintaining exact offset distance 1e-2 (0.01) and 1e-3 (0.001) at the end of "arcs approx." refers to the error when approximating an arc as a series of line segments, it is the maximum allowed distance between the approximating line segments and the original arc. Note the profile vertex distances are in the range of 0-50 so the 1e-2 (0.01) error is intolerable for many applications, but significantly cuts down on the number of generated segments. -| benchmark | vertex count | arcs approx. 1e-2 vertex count | arcs approx. 1e-3 vertex count | -|-----------------------------|--------------|--------------------------------|--------------------------------| -| BM_square | 4 | 4 | 4 | -| BM_circle | 2 | 142 | 446 | -| BM_roundedRectangle | 8 | 56 | 164 | -| BM_Profile1 | 6 | 80 | 241 | -| BM_Profile2 | 11 | 162 | 494 | -| BM_PathologicalProfile1/10 | 10 | 400 | 1240 | -| BM_PathologicalProfile1/25 | 25 | 625 | 1975 | -| BM_PathologicalProfile1/50 | 50 | 900 | 2800 | -| BM_PathologicalProfile1/100 | 100 | 1300 | 4000 | +| benchmark | vertex count | arcs approx. 1e-2 vertex count | arcs approx. 1e-3 vertex count | +| --------------------------- | ------------ | ------------------------------ | ------------------------------ | +| BM_square | 4 | 4 | 4 | +| BM_circle | 2 | 142 | 446 | +| BM_roundedRectangle | 8 | 56 | 164 | +| BM_Profile1 | 6 | 80 | 241 | +| BM_Profile2 | 11 | 162 | 494 | +| BM_PathologicalProfile1/10 | 10 | 400 | 1240 | +| BM_PathologicalProfile1/25 | 25 | 625 | 1975 | +| BM_PathologicalProfile1/50 | 50 | 900 | 2800 | +| BM_PathologicalProfile1/100 | 100 | 1300 | 4000 | All times are wall time. Exact details of the benchmark profiles and offsets run can be found under the dev project [here](https://github.com/jbuckmccready/CavalierContoursDev). For more on the clipper library see [[11]](#references). ### CavalierContours (Arcs Approximated) vs. Clipper -| benchmark | cavc arcs approx. 1e-2 (ms) | clipper arcs approx. 1e-2 (ms) | cavc vs. clipper | -|-----------------------------|-----------------------------|--------------------------------|------------------| -| BM_square | **0.21** | 0.72 | 3.5x | -| BM_circle | **9.11** | 9.47 | 1.0x | -| BM_roundedRectangle | **3.92** | 8.46 | 2.2x | -| BM_Profile1 | 8.55 | **6.33** | 0.7x | -| BM_Profile2 | 17.57 | **16.59** | 0.9x | -| BM_PathologicalProfile1/10 | **34.97** | 136.46 | 3.9x | -| BM_PathologicalProfile1/25 | **70.93** | 241.95 | 3.4x | -| BM_PathologicalProfile1/50 | **138.17** | 514.95 | 3.7x | -| BM_PathologicalProfile1/100 | **336.37** | 1347.09 | 4.0x | - -| benchmark | cavc arcs approx. 1e-3 (ms) | clipper arcs approx. 1e-3 (ms) | cavc vs. clipper | -|-----------------------------|-----------------------------|--------------------------------|------------------| -| BM_square | **0.21** | 1.71 | 8.1x | -| BM_circle | **38.04** | 61.37 | 1.6x | -| BM_roundedRectangle | **13.14** | 61.61 | 4.7x | -| BM_Profile1 | 27.07 | **24.44** | 0.9x | -| BM_Profile2 | **52.64** | 73.64 | 1.4x | -| BM_PathologicalProfile1/10 | **109.96** | 1383.46 | 12.6x | -| BM_PathologicalProfile1/25 | **227.51** | 2824.67 | 12.4x | -| BM_PathologicalProfile1/50 | **385.46** | 6170.26 | 16.0x | -| BM_PathologicalProfile1/100 | **821.15** | 15797.60 | 19.2x | +| benchmark | cavc arcs approx. 1e-2 (ms) | clipper arcs approx. 1e-2 (ms) | cavc vs. clipper | +| --------------------------- | --------------------------- | ------------------------------ | ---------------- | +| BM_square | **0.21** | 0.72 | 3.5x | +| BM_circle | **9.11** | 9.47 | 1.0x | +| BM_roundedRectangle | **3.92** | 8.46 | 2.2x | +| BM_Profile1 | 8.55 | **6.33** | 0.7x | +| BM_Profile2 | 17.57 | **16.59** | 0.9x | +| BM_PathologicalProfile1/10 | **34.97** | 136.46 | 3.9x | +| BM_PathologicalProfile1/25 | **70.93** | 241.95 | 3.4x | +| BM_PathologicalProfile1/50 | **138.17** | 514.95 | 3.7x | +| BM_PathologicalProfile1/100 | **336.37** | 1347.09 | 4.0x | + +| benchmark | cavc arcs approx. 1e-3 (ms) | clipper arcs approx. 1e-3 (ms) | cavc vs. clipper | +| --------------------------- | --------------------------- | ------------------------------ | ---------------- | +| BM_square | **0.21** | 1.71 | 8.1x | +| BM_circle | **38.04** | 61.37 | 1.6x | +| BM_roundedRectangle | **13.14** | 61.61 | 4.7x | +| BM_Profile1 | 27.07 | **24.44** | 0.9x | +| BM_Profile2 | **52.64** | 73.64 | 1.4x | +| BM_PathologicalProfile1/10 | **109.96** | 1383.46 | 12.6x | +| BM_PathologicalProfile1/25 | **227.51** | 2824.67 | 12.4x | +| BM_PathologicalProfile1/50 | **385.46** | 6170.26 | 16.0x | +| BM_PathologicalProfile1/100 | **821.15** | 15797.60 | 19.2x | The above benchmarks were taken by converting all arcs to line segments before running the profile through the offsetting algorithm. Even with no arcs in the input CavalierContours is competitive with a noticeable speedup in many cases. This is in part due to CavalierContours not having to construct rounded joins from line segments (an exact arc can be constructed instead). ### CavalierContours (Arcs Included) vs. Clipper -| benchmark | cavc w/ arcs (ms) | clipper arcs approx. 1e-2 (ms) | cavc vs. clipper | -|-----------------------------|-------------------|--------------------------------|------------------| -| BM_square | **0.21** | 0.72 | 3.4x | -| BM_circle | **0.12** | 9.47 | 76.4x | -| BM_roundedRectangle | **0.49** | 8.46 | 17.3x | -| BM_Profile1 | **0.76** | 6.33 | 8.4x | -| BM_Profile2 | **1.55** | 16.59 | 10.7x | -| BM_PathologicalProfile1/10 | **1.61** | 136.46 | 84.9x | -| BM_PathologicalProfile1/25 | **7.17** | 241.95 | 33.8x | -| BM_PathologicalProfile1/50 | **23.98** | 514.95 | 21.5x | -| BM_PathologicalProfile1/100 | **85.10** | 1347.09 | 15.8x | - -| benchmark | cavc w/ arcs (ms) | clipper arcs approx. 1e-3 (ms) | cavc vs. clipper | -|-----------------------------|-------------------|--------------------------------|------------------| -| BM_square | **0.21** | 1.71 | 8.1x | -| BM_circle | **0.12** | 61.37 | 494.7x | -| BM_roundedRectangle | **0.49** | 61.61 | 126.1x | -| BM_Profile1 | **0.76** | 24.44 | 32.3x | -| BM_Profile2 | **1.55** | 73.64 | 47.6x | -| BM_PathologicalProfile1/10 | **1.61** | 1383.46 | 861.0x | -| BM_PathologicalProfile1/25 | **7.17** | 2824.67 | 394.2x | -| BM_PathologicalProfile1/50 | **23.98** | 6170.26 | 257.3x | -| BM_PathologicalProfile1/100 | **85.10** | 15797.60 | 185.6x | +| benchmark | cavc w/ arcs (ms) | clipper arcs approx. 1e-2 (ms) | cavc vs. clipper | +| --------------------------- | ----------------- | ------------------------------ | ---------------- | +| BM_square | **0.21** | 0.72 | 3.4x | +| BM_circle | **0.12** | 9.47 | 76.4x | +| BM_roundedRectangle | **0.49** | 8.46 | 17.3x | +| BM_Profile1 | **0.76** | 6.33 | 8.4x | +| BM_Profile2 | **1.55** | 16.59 | 10.7x | +| BM_PathologicalProfile1/10 | **1.61** | 136.46 | 84.9x | +| BM_PathologicalProfile1/25 | **7.17** | 241.95 | 33.8x | +| BM_PathologicalProfile1/50 | **23.98** | 514.95 | 21.5x | +| BM_PathologicalProfile1/100 | **85.10** | 1347.09 | 15.8x | + +| benchmark | cavc w/ arcs (ms) | clipper arcs approx. 1e-3 (ms) | cavc vs. clipper | +| --------------------------- | ----------------- | ------------------------------ | ---------------- | +| BM_square | **0.21** | 1.71 | 8.1x | +| BM_circle | **0.12** | 61.37 | 494.7x | +| BM_roundedRectangle | **0.49** | 61.61 | 126.1x | +| BM_Profile1 | **0.76** | 24.44 | 32.3x | +| BM_Profile2 | **1.55** | 73.64 | 47.6x | +| BM_PathologicalProfile1/10 | **1.61** | 1383.46 | 861.0x | +| BM_PathologicalProfile1/25 | **7.17** | 2824.67 | 394.2x | +| BM_PathologicalProfile1/50 | **23.98** | 6170.26 | 257.3x | +| BM_PathologicalProfile1/100 | **85.10** | 15797.60 | 185.6x | The above benchmarks compare CavalierContours taking in the original input (arcs included) vs. Clipper (arcs must be approximated). The performance benefits of processing the arcs directly is quickly realized. diff --git a/c_api_include/cavaliercontours.h b/c_api_include/cavaliercontours.h index 91bbd96..79cecdb 100644 --- a/c_api_include/cavaliercontours.h +++ b/c_api_include/cavaliercontours.h @@ -43,6 +43,10 @@ typedef struct cavc_pline cavc_pline; typedef struct cavc_pline_list cavc_pline_list; +typedef struct cavc_spatial_index cavc_spatial_index; + +typedef struct cavc_offset_loop_topology cavc_offset_loop_topology; + typedef struct cavc_vertex { cavc_real x; cavc_real y; @@ -54,6 +58,57 @@ typedef struct cavc_point { cavc_real y; } cavc_point; +typedef struct cavc_tolerances { + cavc_real real_threshold; + cavc_real real_precision; + cavc_real slice_join_threshold; + cavc_real offset_threshold; +} cavc_tolerances; + +typedef enum cavc_point_containment { + CAVC_POINT_OUTSIDE = 0, + CAVC_POINT_INSIDE = 1, + CAVC_POINT_ON_BOUNDARY = 2 +} cavc_point_containment; + +typedef enum cavc_closed_winding { + CAVC_WINDING_KEEP = 0, + CAVC_WINDING_COUNTER_CLOCKWISE = 1, + CAVC_WINDING_CLOCKWISE = 2 +} cavc_closed_winding; + +typedef enum cavc_offset_join_type { + CAVC_OFFSET_JOIN_ROUND = 0, + CAVC_OFFSET_JOIN_MITER = 1, + CAVC_OFFSET_JOIN_BEVEL = 2 +} cavc_offset_join_type; + +typedef enum cavc_offset_end_cap_type { + CAVC_OFFSET_END_CAP_ROUND = 0, + CAVC_OFFSET_END_CAP_SQUARE = 1, + CAVC_OFFSET_END_CAP_BUTT = 2 +} cavc_offset_end_cap_type; + +typedef enum cavc_offset_loop_role { + CAVC_OFFSET_LOOP_ROLE_OUTER = 0, + CAVC_OFFSET_LOOP_ROLE_HOLE = 1 +} cavc_offset_loop_role; + +#define CAVC_OFFSET_LOOP_NO_PARENT UINT32_MAX + +typedef struct cavc_offset_loop_topology_node { + cavc_offset_loop_role role; + uint32_t source_index; + uint32_t parent_index; +} cavc_offset_loop_topology_node; + +typedef struct cavc_parallel_offset_options { + int may_have_self_intersects; + cavc_offset_join_type join_type; + cavc_offset_end_cap_type end_cap_type; + cavc_real miter_limit; +} cavc_parallel_offset_options; + // Functions for working with cavc_pline // Create/alloc a new cavc_pline initialized with data given. vertex_data may be null (vertex_count @@ -104,6 +159,22 @@ CAVC_API void cavc_pline_clear(cavc_pline *pline); // it will be closed. CAVC_API void cavc_pline_set_is_closed(cavc_pline *pline, int is_closed); +// Invert the direction of the polyline (preserving geometric shape). +CAVC_API void cavc_pline_invert_direction(cavc_pline *pline); + +// Create a new polyline with repeated positions removed from the input. +CAVC_API void cavc_pline_prune_singularities(cavc_pline const *pline, cavc_real epsilon, + cavc_pline **output); + +// Create a new polyline with all arc segments approximated by line segments. +CAVC_API void cavc_pline_convert_arcs_to_lines(cavc_pline const *pline, cavc_real error, + cavc_pline **output); + +// Create a normalized polyline by pruning singularities and optionally normalizing winding. +// Winding normalization only applies to closed polylines. +CAVC_API void cavc_pline_normalize(cavc_pline const *pline, cavc_real epsilon, + cavc_closed_winding closed_winding, cavc_pline **output); + // Functions for working with cavc_pline_list // Delete/free a cavc_pline_list, this will also delete all elements in the list. @@ -121,15 +192,73 @@ CAVC_API cavc_pline *cavc_pline_list_get(cavc_pline_list const *pline_list, uint // released cavc_pline! CAVC_API cavc_pline *cavc_pline_list_release(cavc_pline_list *pline_list, uint32_t index); +// Functions for working with cavc_spatial_index + +// Build an approximate segment spatial index from the input polyline. Polyline must have at least +// 2 vertexes. +CAVC_API cavc_spatial_index *cavc_spatial_index_create(cavc_pline const *pline); + +// Delete/free a cavc_spatial_index. +CAVC_API void cavc_spatial_index_delete(cavc_spatial_index *spatial_index); + +// Returns the number of indexed segments in cavc_spatial_index. +CAVC_API uint32_t cavc_spatial_index_item_count(cavc_spatial_index const *spatial_index); + +// Query the spatial index and return how many segment indexes overlap the given bounding box. +CAVC_API uint32_t cavc_spatial_index_query_count(cavc_spatial_index const *spatial_index, + cavc_real min_x, cavc_real min_y, cavc_real max_x, + cavc_real max_y); + +// Query the spatial index and write overlapping segment indexes to results_out. +// results_out must have enough storage for cavc_spatial_index_query_count(...) values. +CAVC_API void cavc_spatial_index_query(cavc_spatial_index const *spatial_index, cavc_real min_x, + cavc_real min_y, cavc_real max_x, cavc_real max_y, + uint32_t *results_out); + +// Build stable topology ordering and parent-child mapping for offset loops. +// The input loops are split by role (ccw=outer, cw=hole). +// - output order is stable geometry order (area-desc + bbox key) +// - source_index is within the corresponding input role array +// - parent_index is index in the returned topology order, or CAVC_OFFSET_LOOP_NO_PARENT +// All input loops must be closed and contain at least 2 vertices. +// boundary_epsilon must be >= 0. +CAVC_API cavc_offset_loop_topology * +cavc_offset_loop_topology_build(cavc_pline const *const *ccw_loops, uint32_t ccw_loop_count, + cavc_pline const *const *cw_loops, uint32_t cw_loop_count, + cavc_real boundary_epsilon); + +// Delete/free a cavc_offset_loop_topology. +CAVC_API void cavc_offset_loop_topology_delete(cavc_offset_loop_topology *topology); + +// Get total node count from cavc_offset_loop_topology. +CAVC_API uint32_t cavc_offset_loop_topology_count(cavc_offset_loop_topology const *topology); + +// Get a topology node at index. +CAVC_API cavc_offset_loop_topology_node +cavc_offset_loop_topology_get(cavc_offset_loop_topology const *topology, uint32_t index); + // Algorithm functions -// Generates the parallel offset of a polyline. delta is the offset delta, output is filled with the -// result, option_flags are bit flags that allow for indicating information about the polyline or -// forcing certain behaviors in the offset generation, more flags may be added in future versions. -// If no flags are set then the polyline is assumed to have no self intersects. -// 0x1 = Indicates the polyline may have self intersects. +// Returns default parallel offset options: +// - may_have_self_intersects = 0 +// - join_type = CAVC_OFFSET_JOIN_ROUND +// - end_cap_type = CAVC_OFFSET_END_CAP_ROUND +// - miter_limit = 4.0 +CAVC_API cavc_parallel_offset_options cavc_parallel_offset_default_options(void); + +// Generates the parallel offset of a polyline. delta is the offset delta, output is filled with +// the result. +// Join support: +// - round: lines and arcs +// - miter/bevel: line-line joins and arc-involved joins +// (arc-involved joins use endpoint tangents; collapsed-arc joins degrade to bevel) +// - miter_limit applies when join_type is miter (must be >= 1) +// End cap support: +// - round/butt: supported +// (butt uses endpoint-normal line clipping, round uses endpoint circles) +// - square: open polylines with line/arc start/end segments CAVC_API void cavc_parallel_offset(cavc_pline const *pline, cavc_real delta, - cavc_pline_list **output, int option_flags); + cavc_pline_list **output, cavc_parallel_offset_options options); // Combines two non-self intersecting closed polylines, pline_a and pline_b. // For union combine_mode = 0 @@ -159,6 +288,12 @@ CAVC_API cavc_real cavc_get_area(cavc_pline const *pline); // https://en.wikipedia.org/wiki/Winding_number CAVC_API int cavc_get_winding_number(cavc_pline const *pline, cavc_point point); +// Classify a point against a polyline as Outside/Inside/OnBoundary. +// For open polylines, only Outside or OnBoundary are returned. +CAVC_API cavc_point_containment cavc_get_point_containment(cavc_pline const *pline, + cavc_point point, + cavc_real boundary_epsilon); + // Compute the axis aligned extents of the pline, results are written to min_x, min_y, max_x, and // max_y output parameters. If pline is empty then min_x and min_y are filled with positive infinity // and max_x and max_y are filled with negative infinity. @@ -173,6 +308,15 @@ CAVC_API void cavc_get_closest_point(cavc_pline const *pline, cavc_point input_p uint32_t *closest_start_index, cavc_point *closest_point, cavc_real *distance); +// Get the current geometry tolerances used by algorithms. +CAVC_API void cavc_get_tolerances(cavc_tolerances *tolerances_out); + +// Override geometry tolerances used by algorithms. +CAVC_API void cavc_set_tolerances(cavc_tolerances const *tolerances); + +// Reset geometry tolerances to default values. +CAVC_API void cavc_reset_tolerances(void); + #ifdef __cplusplus } #endif diff --git a/include/cavc/internal/diagnostics.hpp b/include/cavc/internal/diagnostics.hpp index 30919f9..5a58878 100644 --- a/include/cavc/internal/diagnostics.hpp +++ b/include/cavc/internal/diagnostics.hpp @@ -4,6 +4,7 @@ #include #include #include + namespace cavc { namespace internal { template diff --git a/include/cavc/intrcircle2circle2.hpp b/include/cavc/intrcircle2circle2.hpp index 34d6444..70f4d77 100644 --- a/include/cavc/intrcircle2circle2.hpp +++ b/include/cavc/intrcircle2circle2.hpp @@ -1,6 +1,7 @@ #ifndef CAVC_INTRCIRCLE2CIRCLE2_HPP #define CAVC_INTRCIRCLE2CIRCLE2_HPP #include "vector2.hpp" + namespace cavc { enum class Circle2Circle2IntrType { // no intersect between circles diff --git a/include/cavc/intrlineseg2circle2.hpp b/include/cavc/intrlineseg2circle2.hpp index 51e183a..f2e82b3 100644 --- a/include/cavc/intrlineseg2circle2.hpp +++ b/include/cavc/intrlineseg2circle2.hpp @@ -1,6 +1,7 @@ #ifndef CAVC_INTRLINESEG2CIRCLE2_HPP #define CAVC_INTRLINESEG2CIRCLE2_HPP #include "vector2.hpp" + namespace cavc { template struct IntrLineSeg2Circle2Result { // number of interescts found (0, 1, or 2) diff --git a/include/cavc/mathutils.hpp b/include/cavc/mathutils.hpp index f97273d..d4ff175 100644 --- a/include/cavc/mathutils.hpp +++ b/include/cavc/mathutils.hpp @@ -4,20 +4,62 @@ #include "internal/common.hpp" #include #include + namespace cavc { namespace utils { +template constexpr Real defaultRealThreshold() { return Real(1e-8); } + +template constexpr Real defaultRealPrecision() { return Real(1e-5); } + +template constexpr Real defaultSliceJoinThreshold() { return Real(1e-4); } + +template constexpr Real defaultOffsetThreshold() { return Real(1e-4); } + +template struct EpsilonConfig { + Real realThreshold; + Real realPrecision; + Real sliceJoinThreshold; + Real offsetThreshold; +}; + +template constexpr EpsilonConfig defaultEpsilonConfig() { + return {defaultRealThreshold(), defaultRealPrecision(), + defaultSliceJoinThreshold(), defaultOffsetThreshold()}; +} + +template EpsilonConfig &epsilonConfig() { + static EpsilonConfig config = defaultEpsilonConfig(); + return config; +} + +template EpsilonConfig getEpsilonConfig() { return epsilonConfig(); } + +template void setEpsilonConfig(EpsilonConfig const &config) { + CAVC_ASSERT(config.realThreshold > Real(0), "realThreshold must be > 0"); + CAVC_ASSERT(config.realPrecision > Real(0), "realPrecision must be > 0"); + CAVC_ASSERT(config.sliceJoinThreshold > Real(0), "sliceJoinThreshold must be > 0"); + CAVC_ASSERT(config.offsetThreshold > Real(0), "offsetThreshold must be > 0"); + epsilonConfig() = config; +} + +template void resetEpsilonConfig() { + epsilonConfig() = defaultEpsilonConfig(); +} + // absolute threshold to be used for comparing reals generally -template constexpr Real realThreshold() { return Real(1e-8); } +template Real realThreshold() { return epsilonConfig().realThreshold; } // absolute threshold to be used for reals in common geometric computation (e.g. to check for // singularities) -template constexpr Real realPrecision() { return Real(1e-5); } +template Real realPrecision() { return epsilonConfig().realPrecision; } // absolute threshold to be used for joining slices together at end points -template constexpr Real sliceJoinThreshold() { return Real(1e-4); } +template Real sliceJoinThreshold() { + return epsilonConfig().sliceJoinThreshold; +} // absolute threshold to be used for pruning invalid slices for offset -template constexpr Real offsetThreshold() { return Real(1e-4); } +template Real offsetThreshold() { return epsilonConfig().offsetThreshold; } template constexpr Real pi() { return Real(3.14159265358979323846264338327950288); } diff --git a/include/cavc/plinesegment.hpp b/include/cavc/plinesegment.hpp index 5412278..6179415 100644 --- a/include/cavc/plinesegment.hpp +++ b/include/cavc/plinesegment.hpp @@ -343,19 +343,19 @@ IntrPlineSegsResult intrPlineSegs(PlineVertex const &v1, PlineVertex } } else { CAVC_ASSERT(intrResult.numIntersects == 2, "shouldn't get here without 2 intersects"); - auto p1 = pointInSweep(intrResult.t0); - auto p2 = pointInSweep(intrResult.t1); + auto l_p1 = pointInSweep(intrResult.t0); + auto l_p2 = pointInSweep(intrResult.t1); - if (p1.first && p2.first) { + if (l_p1.first && l_p2.first) { result.intrType = PlineSegIntrType::TwoIntersects; - result.point1 = p1.second; - result.point2 = p2.second; - } else if (p1.first) { + result.point1 = l_p1.second; + result.point2 = l_p2.second; + } else if (l_p1.first) { result.intrType = PlineSegIntrType::OneIntersect; - result.point1 = p1.second; - } else if (p2.first) { + result.point1 = l_p1.second; + } else if (l_p2.first) { result.intrType = PlineSegIntrType::OneIntersect; - result.point1 = p2.second; + result.point1 = l_p2.second; } else { result.intrType = PlineSegIntrType::NoIntersect; } diff --git a/include/cavc/polyline.hpp b/include/cavc/polyline.hpp index d0044e5..ef000a6 100644 --- a/include/cavc/polyline.hpp +++ b/include/cavc/polyline.hpp @@ -1,8 +1,6 @@ #ifndef CAVC_POLYLINE_HPP #define CAVC_POLYLINE_HPP -#include "intrcircle2circle2.hpp" -#include "intrlineseg2circle2.hpp" -#include "intrlineseg2lineseg2.hpp" + #include "plinesegment.hpp" #include "staticspatialindex.hpp" #include "vector2.hpp" @@ -296,7 +294,7 @@ Polyline convertArcsToLines(Polyline const &pline, Real error) { Real segmentSubAngle = std::abs(Real(2) * std::acos(Real(1) - error / arc.radius)); std::size_t segmentCount = static_cast(std::ceil(deltaAngle / segmentSubAngle)); // update segment subangle for equal length segments - segmentSubAngle = deltaAngle / segmentCount; + segmentSubAngle = deltaAngle / static_cast(segmentCount); if (v1.bulgeIsNeg()) { segmentSubAngle = -segmentSubAngle; @@ -304,8 +302,8 @@ Polyline convertArcsToLines(Polyline const &pline, Real error) { // add the start point result.addVertex(v1.x(), v1.y(), 0.0); // add the remaining points - for (std::size_t i = 1; i < segmentCount; ++i) { - Real angle = i * segmentSubAngle + startAngle; + for (std::size_t k = 1; k < segmentCount; ++k) { + Real angle = static_cast(k) * segmentSubAngle + startAngle; result.addVertex(arc.radius * std::cos(angle) + arc.center.x(), arc.radius * std::sin(angle) + arc.center.y(), 0); } @@ -375,6 +373,35 @@ template void invertDirection(Polyline &pline) { pline.lastVertex().bulge() = -firstBulge; } +enum class ClosedPolylineWinding { + Keep = 0, + CounterClockwise = 1, + Clockwise = 2, +}; + +/// Normalize polyline geometry by pruning singularities and optionally normalizing winding. +/// - Repeating positions are removed using pruneSingularities. +/// - Winding normalization only applies to closed polylines with at least 2 vertexes. +template +Polyline +normalizePolyline(Polyline const &pline, Real epsilon = utils::realPrecision(), + ClosedPolylineWinding closedWinding = ClosedPolylineWinding::Keep) { + Polyline result = pruneSingularities(pline, epsilon); + + if (!result.isClosed() || result.size() < 2 || closedWinding == ClosedPolylineWinding::Keep) { + return result; + } + + Real area = getArea(result); + if (closedWinding == ClosedPolylineWinding::CounterClockwise && area < Real(0)) { + invertDirection(result); + } else if (closedWinding == ClosedPolylineWinding::Clockwise && area > Real(0)) { + invertDirection(result); + } + + return result; +} + /// Creates an approximate spatial index for all the segments in the polyline given using /// createFastApproxBoundingBox. template @@ -400,6 +427,18 @@ StaticSpatialIndex createApproxSpatialIndex(Polyline const &pline) { return result; } +/// Creates approximate spatial indexes for a batch of closed or open polylines. +template +std::vector> +createApproxSpatialIndices(std::vector> const &plines) { + std::vector> result; + result.reserve(plines.size()); + for (auto const &pline : plines) { + result.push_back(createApproxSpatialIndex(pline)); + } + return result; +} + /// Calculate the total path length of a polyline. template Real getPathLength(Polyline const &pline) { if (pline.size() < 2) { @@ -546,6 +585,35 @@ int getWindingNumber(Polyline const &pline, Vector2 const &point) { return windingNumber; } +enum class PointContainment { + Outside = 0, + Inside = 1, + OnBoundary = 2, +}; + +/// Classify point containment against a polyline. +/// - Open polyline: only OnBoundary or Outside is possible. +/// - Closed polyline: returns Inside/Outside/OnBoundary. +template +PointContainment getPointContainment(Polyline const &pline, Vector2 const &point, + Real boundaryEpsilon = utils::realPrecision()) { + CAVC_ASSERT(boundaryEpsilon >= Real(0), "boundaryEpsilon must be >= 0"); + if (pline.size() < 2) { + return PointContainment::Outside; + } + + ClosestPoint closestPoint(pline, point); + if (closestPoint.distance() <= boundaryEpsilon) { + return PointContainment::OnBoundary; + } + + if (!pline.isClosed()) { + return PointContainment::Outside; + } + + return getWindingNumber(pline, point) == 0 ? PointContainment::Outside : PointContainment::Inside; +} + namespace internal { template void addOrReplaceIfSamePos(Polyline &pline, PlineVertex const &vertex, diff --git a/include/cavc/polylinecombine.hpp b/include/cavc/polylinecombine.hpp index 884eafe..3d68c83 100644 --- a/include/cavc/polylinecombine.hpp +++ b/include/cavc/polylinecombine.hpp @@ -63,8 +63,8 @@ template struct SlicePoint { Vector2 pos; // indicates slice point is the start of a coincident slice bool startOfCoincidentSlice; - SlicePoint(Vector2 const &pos, bool startOfCoincidentSlice) - : pos(pos), startOfCoincidentSlice(startOfCoincidentSlice) {} + SlicePoint(Vector2 const &p_pos, bool p_startOfCoincidentSlice) + : pos(p_pos), startOfCoincidentSlice(p_startOfCoincidentSlice) {} }; /// Slice the given pline at all of its intersects for combining. If useSecondIndex is true then the @@ -196,10 +196,10 @@ void sliceAtIntersects(Polyline const &pline, // trim last added vertex and add final intersect position PlineVertex endVertex = PlineVertex(intersectPos, Real(0)); - std::size_t nextIndex = utils::nextWrappingIndex(index, pline); - SplitResult split = - splitAtPoint(currSlice.lastVertex(), pline[nextIndex], intersectPos); - currSlice.lastVertex() = split.updatedStart; + std::size_t l_nextIndex = utils::nextWrappingIndex(index, pline); + SplitResult l_split = + splitAtPoint(currSlice.lastVertex(), pline[l_nextIndex], intersectPos); + currSlice.lastVertex() = l_split.updatedStart; internal::addOrReplaceIfSamePos(currSlice, endVertex); break; } @@ -429,7 +429,8 @@ CombineResult combinePolylines(Polyline const &plineA, Polyline std::function const &)> { return [=](std::size_t currSliceIndex, std::vector const &available) { // attempt to select noncoincident slice auto idx = std::find_if(available.begin(), available.end(), diff --git a/include/cavc/polylineintersects.hpp b/include/cavc/polylineintersects.hpp index 9c6490a..ee65495 100644 --- a/include/cavc/polylineintersects.hpp +++ b/include/cavc/polylineintersects.hpp @@ -35,9 +35,9 @@ template struct PlineCoincidentIntersect { /// Other end point of the coincident slice Vector2 point2; PlineCoincidentIntersect() = default; - PlineCoincidentIntersect(std::size_t si1, std::size_t si2, Vector2 const &point1, - Vector2 const &point2) - : sIndex1(si1), sIndex2(si2), point1(point1), point2(point2) {} + PlineCoincidentIntersect(std::size_t si1, std::size_t si2, Vector2 const &p_point1, + Vector2 const &p_point2) + : sIndex1(si1), sIndex2(si2), point1(p_point1), point2(p_point2) {} }; /// Holds a collection of intersects found @@ -404,7 +404,24 @@ void findIntersects(Polyline const &pline1, Polyline const &pline2, PlineVertex const &p1v2 = pline1[j1]; auto intrAtStartPt = [&](Vector2 const &intr) { - return fuzzyEqual(p1v1.pos(), intr) || fuzzyEqual(p2v1.pos(), intr); + // We normally drop intersects exactly at a segment's start point to avoid duplicates + // reported by adjacent segments. However, for open polylines, the very first segment + // (start index 0) has no previous segment, so an intersect at pline2's starting + // vertex must be kept. Same logic applies to pline1's first segment. + bool isPline1FirstOpenSeg = (!pline1.isClosed()) && (i1 == 0); + bool isPline2FirstOpenSeg = (!pline2.isClosed()) && (i2 == 0); + + bool atPline1Start = fuzzyEqual(p1v1.pos(), intr); + bool atPline2Start = fuzzyEqual(p2v1.pos(), intr); + + if (atPline1Start && isPline1FirstOpenSeg) { + return false; + } + if (atPline2Start && isPline2FirstOpenSeg) { + return false; + } + + return atPline1Start || atPline2Start; }; auto intrResult = intrPlineSegs(p1v1, p1v2, p2v1, p2v2); diff --git a/include/cavc/polylineoffset.hpp b/include/cavc/polylineoffset.hpp index d674e8c..5c62b72 100644 --- a/include/cavc/polylineoffset.hpp +++ b/include/cavc/polylineoffset.hpp @@ -2,13 +2,25 @@ #define CAVC_POLYLINEOFFSET_HPP #include "polyline.hpp" #include "polylineintersects.hpp" -#include +#include #include +#include #include // This header has functions for offsetting polylines namespace cavc { +enum class OffsetJoinType { Round = 0, Miter = 1, Bevel = 2 }; + +enum class OffsetEndCapType { Round = 0, Square = 1, Butt = 2 }; + +template struct ParallelOffsetOptions { + bool hasSelfIntersects = false; + OffsetJoinType joinType = OffsetJoinType::Round; + OffsetEndCapType endCapType = OffsetEndCapType::Round; + Real miterLimit = Real(4); +}; + namespace internal { /// Represents a raw polyline offset segment. template struct PlineOffsetSegment { @@ -106,12 +118,17 @@ Real bulgeForConnection(Vector2 const &arcCenter, Vector2 const &sp, template void lineToLineJoin(PlineOffsetSegment const &s1, PlineOffsetSegment const &s2, - bool connectionArcsAreCCW, Polyline &result) { + bool connectionArcsAreCCW, OffsetJoinType joinType, Real miterLimit, + Polyline &result) { const auto &v1 = s1.v1; const auto &v2 = s1.v2; const auto &u1 = s2.v1; const auto &u2 = s2.v2; CAVC_ASSERT(v1.bulgeIsZero() && u1.bulgeIsZero(), "both segs should be lines"); + CAVC_ASSERT(joinType == OffsetJoinType::Round || !s1.collapsedArc, + "non-round join with collapsed arc is not supported"); + CAVC_ASSERT(joinType == OffsetJoinType::Round || !s2.collapsedArc, + "non-round join with collapsed arc is not supported"); auto connectUsingArc = [&] { auto const &arcCenter = s1.origV2Pos; @@ -122,6 +139,16 @@ void lineToLineJoin(PlineOffsetSegment const &s1, PlineOffsetSegment addOrReplaceIfSamePos(result, PlineVertex(ep, Real(0))); }; + auto miterRatio = [&](Vector2 const &miter_point) { + Real offset_dist = length(v2.pos() - s1.origV2Pos); + if (offset_dist <= utils::realThreshold()) { + return std::numeric_limits::infinity(); + } + + Real miter_dist = length(miter_point - s1.origV2Pos); + return miter_dist / offset_dist; + }; + if (s1.collapsedArc || s2.collapsedArc) { // connecting to/from collapsed arc, always connect using arc connectUsingArc(); @@ -141,9 +168,17 @@ void lineToLineJoin(PlineOffsetSegment const &s1, PlineOffsetSegment addOrReplaceIfSamePos(result, PlineVertex(v2.pos(), Real(0))); break; case LineSeg2LineSeg2IntrType::False: - if (intrResult.t0 > Real(1) && falseIntersect(intrResult.t1)) { - // extend and join the lines together using an arc - connectUsingArc(); + if (joinType == OffsetJoinType::Round) { + if (intrResult.t0 > Real(1) && falseIntersect(intrResult.t1)) { + // extend and join the lines together using an arc + connectUsingArc(); + } else { + addOrReplaceIfSamePos(result, PlineVertex(v2.pos(), Real(0))); + addOrReplaceIfSamePos(result, u1); + } + } else if (joinType == OffsetJoinType::Miter && intrResult.t0 > Real(1) && + intrResult.t1 < Real(0) && miterRatio(intrResult.point) <= miterLimit) { + addOrReplaceIfSamePos(result, PlineVertex(intrResult.point, Real(0))); } else { addOrReplaceIfSamePos(result, PlineVertex(v2.pos(), Real(0))); addOrReplaceIfSamePos(result, u1); @@ -153,6 +188,206 @@ void lineToLineJoin(PlineOffsetSegment const &s1, PlineOffsetSegment } } +template +Vector2 joinSegmentTangent(PlineOffsetSegment const &segment, bool atEndPoint) { + if (segment.v1.bulgeIsZero()) { + return segment.v2.pos() - segment.v1.pos(); + } + + Vector2 pointOnSeg = atEndPoint ? segment.v2.pos() : segment.v1.pos(); + return segTangentVector(segment.v1, segment.v2, pointOnSeg); +} + +template +bool tryComputeMiterPointForArcJoin(PlineOffsetSegment const &s1, + PlineOffsetSegment const &s2, Vector2 &miterPoint) { + Vector2 t1 = joinSegmentTangent(s1, true); + Vector2 t2 = joinSegmentTangent(s2, false); + if (fuzzyZero(t1) || fuzzyZero(t2)) { + return false; + } + + normalize(t1); + normalize(t2); + Vector2 p = s1.v2.pos(); + Vector2 q = s2.v1.pos(); + Vector2 r = t1; + Vector2 s = -t2; + Real denom = perpDot(r, s); + if (std::abs(denom) <= utils::realThreshold()) { + return false; + } + + Vector2 qp = q - p; + Real t = perpDot(qp, s) / denom; + Real u = perpDot(qp, r) / denom; + Real eps = utils::realPrecision(); + if (t < -eps || u < -eps) { + return false; + } + + miterPoint = p + t * r; + return true; +} + +template +void trimPreviousJoinSegmentAtPoint(PlineOffsetSegment const &s1, Vector2 const &point, + Polyline &result) { + CAVC_ASSERT(result.size() > 0, "join result must contain previous segment start"); + + if (!s1.v1.bulgeIsZero()) { + PlineVertex &prevVertex = result.lastVertex(); + if (!prevVertex.bulgeIsZero() && !fuzzyEqual(prevVertex.pos(), s1.v2.pos())) { + auto prevArc = arcRadiusAndCenter(prevVertex, s1.v2); + Real prevArcStartAngle = angle(prevArc.center, prevVertex.pos()); + Real updatedPrevTheta = utils::deltaAngle(prevArcStartAngle, angle(prevArc.center, point)); + if ((updatedPrevTheta > Real(0)) == prevVertex.bulgeIsPos()) { + prevVertex.bulge() = std::tan(updatedPrevTheta / Real(4)); + } + } + } + + addOrReplaceIfSamePos(result, PlineVertex(point, Real(0))); +} + +template +PlineVertex createTrimmedNextJoinStart(PlineOffsetSegment const &s2, + Vector2 const &point) { + if (s2.v1.bulgeIsZero()) { + return PlineVertex(point, Real(0)); + } + + auto arc = arcRadiusAndCenter(s2.v1, s2.v2); + Real a = angle(arc.center, point); + Real endAngle = angle(arc.center, s2.v2.pos()); + Real theta = utils::deltaAngle(a, endAngle); + if ((theta > Real(0)) == s2.v1.bulgeIsPos()) { + return PlineVertex(point, std::tan(theta / Real(4))); + } + + return PlineVertex(point, s2.v1.bulge()); +} + +template +void nonRoundArcJoin(PlineOffsetSegment const &s1, PlineOffsetSegment const &s2, + OffsetJoinType joinType, Real miterLimit, Polyline &result) { + CAVC_ASSERT(joinType != OffsetJoinType::Round, "use round join functions for round joins"); + + auto connectUsingBevel = [&] { + addOrReplaceIfSamePos(result, PlineVertex(s1.v2.pos(), Real(0))); + addOrReplaceIfSamePos(result, s2.v1); + }; + + if (joinType == OffsetJoinType::Bevel || s1.collapsedArc || s2.collapsedArc) { + connectUsingBevel(); + return; + } + + CAVC_ASSERT(joinType == OffsetJoinType::Miter, "unsupported non-round join type"); + Vector2 miterPoint; + if (!tryComputeMiterPointForArcJoin(s1, s2, miterPoint)) { + connectUsingBevel(); + return; + } + + Real offsetDist = length(s1.v2.pos() - s1.origV2Pos); + if (offsetDist <= utils::realThreshold()) { + connectUsingBevel(); + return; + } + + Real miterDist = length(miterPoint - s1.origV2Pos); + if (miterDist / offsetDist > miterLimit + utils::realPrecision()) { + connectUsingBevel(); + return; + } + + trimPreviousJoinSegmentAtPoint(s1, miterPoint, result); + addOrReplaceIfSamePos(result, createTrimmedNextJoinStart(s2, miterPoint)); +} + +template +Vector2 openPolylineEndpointTangent(Polyline const &pline, bool atStart) { + CAVC_ASSERT(!pline.isClosed(), "endpoint tangent is only for open polyline"); + CAVC_ASSERT(pline.size() >= 2, "open polyline must have at least 2 vertexes"); + + auto tangentAlongSegment = [](PlineVertex const &segStart, PlineVertex const &segEnd, + bool atSegmentStartPoint) { + if (segStart.bulgeIsZero()) { + Vector2 d = segEnd.pos() - segStart.pos(); + normalize(d); + return d; + } + + auto arc = arcRadiusAndCenter(segStart, segEnd); + Vector2 radial = + atSegmentStartPoint ? (segStart.pos() - arc.center) : (segEnd.pos() - arc.center); + normalize(radial); + Vector2 tangent = unitPerp(radial); + if (segStart.bulgeIsNeg()) { + tangent = -tangent; + } + return tangent; + }; + + if (atStart) { + return tangentAlongSegment(pline[0], pline[1], true); + } + + std::size_t const prev_index = pline.size() - 2; + return tangentAlongSegment(pline[prev_index], pline.lastVertex(), false); +} + +template +Vector2 openPolylineEndCapCircleCenter(Polyline const &pline, Real offset, bool atStart, + OffsetEndCapType endCapType) { + CAVC_ASSERT(!pline.isClosed(), "end cap circle center is only for open polyline"); + Vector2 center = atStart ? pline[0].pos() : pline.lastVertex().pos(); + if (endCapType == OffsetEndCapType::Square) { + Real extension = std::abs(offset); + Vector2 tangent = openPolylineEndpointTangent(pline, atStart); + if (atStart) { + center = center - extension * tangent; + } else { + center = center + extension * tangent; + } + } + return center; +} + +template +void applySquareEndCapToRawOpenOffset(Polyline const &originalPline, Real offset, + Polyline &rawOffsetPline) { + CAVC_ASSERT(!originalPline.isClosed(), "square end cap helper is only for open polyline"); + if (rawOffsetPline.size() < 2) { + return; + } + + Real extension = std::abs(offset); + Vector2 start_tangent = openPolylineEndpointTangent(originalPline, true); + Vector2 end_tangent = openPolylineEndpointTangent(originalPline, false); + + // Preserve arc geometry at endpoints by adding explicit cap line segments. + if (rawOffsetPline[0].bulgeIsZero()) { + rawOffsetPline[0].pos() = rawOffsetPline[0].pos() - extension * start_tangent; + } else { + PlineVertex cap_start = rawOffsetPline[0]; + cap_start.pos() = cap_start.pos() - extension * start_tangent; + cap_start.bulge() = Real(0); + rawOffsetPline.vertexes().insert(rawOffsetPline.vertexes().begin(), cap_start); + } + + if (rawOffsetPline[rawOffsetPline.size() - 2].bulgeIsZero()) { + rawOffsetPline.lastVertex().pos() = rawOffsetPline.lastVertex().pos() + extension * end_tangent; + } else { + rawOffsetPline.lastVertex().bulge() = Real(0); + PlineVertex cap_end = rawOffsetPline.lastVertex(); + cap_end.pos() = cap_end.pos() + extension * end_tangent; + cap_end.bulge() = Real(0); + rawOffsetPline.addVertex(cap_end); + } +} + template void lineToArcJoin(PlineOffsetSegment const &s1, PlineOffsetSegment const &s2, bool connectionArcsAreCCW, Polyline &result) { @@ -385,15 +620,16 @@ template void offsetCircleIntersectsWithPline(Polyline const &pline, Real offset, Vector2 const &circleCenter, StaticSpatialIndex const &spatialIndex, - std::vector>> &output) { + std::vector>> &output, + std::vector &queryResults, + std::vector &queryStack) { const Real circleRadius = std::abs(offset); - std::vector queryResults; - + queryResults.clear(); spatialIndex.query(circleCenter.x() - circleRadius, circleCenter.y() - circleRadius, - circleCenter.x() + circleRadius, circleCenter.y() + circleRadius, - queryResults); + circleCenter.x() + circleRadius, circleCenter.y() + circleRadius, queryResults, + queryStack); auto validLineSegIntersect = [](Real t) { return !falseIntersect(t) && std::abs(t) > utils::realPrecision(); @@ -454,6 +690,81 @@ void offsetCircleIntersectsWithPline(Polyline const &pline, Real offset, } } +template +void offsetLineIntersectsWithPline(Polyline const &pline, Vector2 const &linePoint, + Vector2 const &lineNormal, + std::vector>> &output) { + if (pline.size() < 2) { + return; + } + + Vector2 normalizedNormal = lineNormal; + CAVC_ASSERT(!fuzzyZero(normalizedNormal), "line normal must be non-zero"); + normalize(normalizedNormal); + Vector2 lineDirection = unitPerp(normalizedNormal); + + auto validLineSegIntersect = [](Real t) { + return !falseIntersect(t) && std::abs(t) > utils::realPrecision(); + }; + + auto validArcSegIntersect = [](Vector2 const &arcCenter, Vector2 const &arcStart, + Vector2 const &arcEnd, Real bulge, + Vector2 const &intrPoint) { + return !fuzzyEqual(arcStart, intrPoint, utils::realPrecision()) && + pointWithinArcSweepAngle(arcCenter, arcStart, arcEnd, bulge, intrPoint); + }; + + std::size_t const segCount = pline.isClosed() ? pline.size() : pline.size() - 1; + for (std::size_t sIndex = 0; sIndex < segCount; ++sIndex) { + std::size_t const nextIndex = + pline.isClosed() ? utils::nextWrappingIndex(sIndex, pline) : sIndex + 1; + PlineVertex const &v1 = pline[sIndex]; + PlineVertex const &v2 = pline[nextIndex]; + + if (v1.bulgeIsZero()) { + Vector2 segDelta = v2.pos() - v1.pos(); + Real const denom = dot(segDelta, normalizedNormal); + if (std::abs(denom) <= utils::realThreshold()) { + continue; + } + + Real const t = dot(linePoint - v1.pos(), normalizedNormal) / denom; + if (validLineSegIntersect(t)) { + output.emplace_back(sIndex, pointFromParametric(v1.pos(), v2.pos(), t)); + } + continue; + } + + auto arc = arcRadiusAndCenter(v1, v2); + Real signedDistance = dot(arc.center - linePoint, normalizedNormal); + Real absDistance = std::abs(signedDistance); + if (absDistance > arc.radius + utils::realPrecision()) { + continue; + } + + Vector2 projectedPoint = arc.center - signedDistance * normalizedNormal; + + auto addArcIntersectIfValid = [&](Vector2 const &intersectPoint) { + if (validArcSegIntersect(arc.center, v1.pos(), v2.pos(), v1.bulge(), intersectPoint)) { + output.emplace_back(sIndex, intersectPoint); + } + }; + + if (utils::fuzzyEqual(absDistance, arc.radius, utils::realPrecision())) { + addArcIntersectIfValid(projectedPoint); + continue; + } + + Real offsetSquared = arc.radius * arc.radius - absDistance * absDistance; + if (offsetSquared < Real(0)) { + offsetSquared = Real(0); + } + Real offsetAlongLine = std::sqrt(offsetSquared); + addArcIntersectIfValid(projectedPoint + offsetAlongLine * lineDirection); + addArcIntersectIfValid(projectedPoint - offsetAlongLine * lineDirection); + } +} + /// Function to test if a point is a valid distance from the original polyline. template bool pointValidForOffset(Polyline const &pline, Real offset, @@ -480,7 +791,8 @@ bool pointValidForOffset(Polyline const &pline, Real offset, /// Creates the raw offset polyline. template -Polyline createRawOffsetPline(Polyline const &pline, Real offset) { +Polyline createRawOffsetPline(Polyline const &pline, Real offset, + ParallelOffsetOptions const &options) { Polyline result; if (pline.size() < 2) { @@ -503,19 +815,36 @@ Polyline createRawOffsetPline(Polyline const &pline, Real offset) { const bool connectionArcsAreCCW = offset < Real(0); - auto joinResultVisitor = [connectionArcsAreCCW](PlineOffsetSegment const &s1, - PlineOffsetSegment const &s2, - Polyline &result) { + auto joinResultVisitor = [&options, connectionArcsAreCCW](PlineOffsetSegment const &s1, + PlineOffsetSegment const &s2, + Polyline &p_result) { const bool s1IsLine = s1.v1.bulgeIsZero(); const bool s2IsLine = s2.v1.bulgeIsZero(); if (s1IsLine && s2IsLine) { - internal::lineToLineJoin(s1, s2, connectionArcsAreCCW, result); + if (options.joinType == OffsetJoinType::Round || (!s1.collapsedArc && !s2.collapsedArc)) { + internal::lineToLineJoin(s1, s2, connectionArcsAreCCW, options.joinType, options.miterLimit, + p_result); + } else { + internal::nonRoundArcJoin(s1, s2, options.joinType, options.miterLimit, p_result); + } } else if (s1IsLine) { - internal::lineToArcJoin(s1, s2, connectionArcsAreCCW, result); + if (options.joinType == OffsetJoinType::Round) { + internal::lineToArcJoin(s1, s2, connectionArcsAreCCW, p_result); + } else { + internal::nonRoundArcJoin(s1, s2, options.joinType, options.miterLimit, p_result); + } } else if (s2IsLine) { - internal::arcToLineJoin(s1, s2, connectionArcsAreCCW, result); + if (options.joinType == OffsetJoinType::Round) { + internal::arcToLineJoin(s1, s2, connectionArcsAreCCW, p_result); + } else { + internal::nonRoundArcJoin(s1, s2, options.joinType, options.miterLimit, p_result); + } } else { - internal::arcToArcJoin(s1, s2, connectionArcsAreCCW, result); + if (options.joinType == OffsetJoinType::Round) { + internal::arcToArcJoin(s1, s2, connectionArcsAreCCW, p_result); + } else { + internal::nonRoundArcJoin(s1, s2, options.joinType, options.miterLimit, p_result); + } } }; @@ -589,6 +918,10 @@ Polyline createRawOffsetPline(Polyline const &pline, Real offset) { internal::addOrReplaceIfSamePos(result, rawOffsets.back().v2); } + if (!pline.isClosed() && options.endCapType == OffsetEndCapType::Square && result.size() > 1) { + internal::applySquareEndCapToRawOpenOffset(pline, offset, result); + } + // if due to joining of segments we are left with only 1 vertex then return no raw offset (empty // polyline) if (result.size() == 1) { @@ -609,9 +942,10 @@ template struct OpenPolylineSlice { /// Slices a raw offset polyline at all of its self intersects. template -std::vector> slicesFromRawOffset(Polyline const &originalPline, - Polyline const &rawOffsetPline, - Real offset) { +std::vector> +slicesFromRawOffset(Polyline const &originalPline, Polyline const &rawOffsetPline, + Real offset, bool enforceMinDistance = true, + bool skipOrigIntersectionCheck = false) { CAVC_ASSERT(originalPline.isClosed(), "use dual slice at intersects for open polylines"); std::vector> result; @@ -627,9 +961,15 @@ std::vector> slicesFromRawOffset(Polyline const &o std::vector queryStack; queryStack.reserve(8); + auto pointValid = [&](Vector2 const &p) { + if (!enforceMinDistance) { + return true; + } + return pointValidForOffset(originalPline, offset, origPlineSpatialIndex, p, queryStack); + }; + if (selfIntersects.size() == 0) { - if (!pointValidForOffset(originalPline, offset, origPlineSpatialIndex, rawOffsetPline[0].pos(), - queryStack)) { + if (!pointValid(rawOffsetPline[0].pos())) { return result; } // copy and convert raw offset into open polyline @@ -662,6 +1002,10 @@ std::vector> slicesFromRawOffset(Polyline const &o } auto intersectsOrigPline = [&](PlineVertex const &v1, PlineVertex const &v2) { + if (skipOrigIntersectionCheck) { + return false; + } + AABB approxBB = createFastApproxBoundingBox(v1, v2); bool hasIntersect = false; auto visitor = [&](std::size_t i) { @@ -705,21 +1049,18 @@ std::vector> slicesFromRawOffset(Polyline const &o } // test start point - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, - split.updatedStart.pos(), queryStack)) { + if (!pointValid(split.updatedStart.pos())) { continue; } // test end point - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, - split.splitVertex.pos(), queryStack)) { + if (!pointValid(split.splitVertex.pos())) { continue; } // test mid point auto midpoint = segMidpoint(split.updatedStart, split.splitVertex); - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, midpoint, - queryStack)) { + if (!pointValid(midpoint)) { continue; } @@ -738,8 +1079,7 @@ std::vector> slicesFromRawOffset(Polyline const &o // build the segment between the last intersect in siList and the next intersect found // check that the first point is valid - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, siList.back(), - queryStack)) { + if (!pointValid(siList.back())) { continue; } @@ -758,8 +1098,7 @@ std::vector> slicesFromRawOffset(Polyline const &o break; } // check that vertex point is valid - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, - rawOffsetPline[index].pos(), queryStack)) { + if (!pointValid(rawOffsetPline[index].pos())) { isValidPline = false; break; } @@ -780,27 +1119,25 @@ std::vector> slicesFromRawOffset(Polyline const &o // check intersect pos is valid (which will also be end vertex position) Vector2 const &intersectPos = nextIntr->second[0]; - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, - intersectPos, queryStack)) { + if (!pointValid(intersectPos)) { isValidPline = false; break; } - std::size_t nextIndex = utils::nextWrappingIndex(index, rawOffsetPline); - SplitResult split = - splitAtPoint(currSlice.lastVertex(), rawOffsetPline[nextIndex], intersectPos); + std::size_t l_nextIndex = utils::nextWrappingIndex(index, rawOffsetPline); + SplitResult l_split = + splitAtPoint(currSlice.lastVertex(), rawOffsetPline[l_nextIndex], intersectPos); PlineVertex sliceEndVertex = PlineVertex(intersectPos, Real(0)); // check mid point is valid - Vector2 mp = segMidpoint(split.updatedStart, sliceEndVertex); - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, mp, - queryStack)) { + Vector2 mp = segMidpoint(l_split.updatedStart, sliceEndVertex); + if (!pointValid(mp)) { isValidPline = false; break; } // trim last added vertex and add final intersect position - currSlice.lastVertex() = split.updatedStart; + currSlice.lastVertex() = l_split.updatedStart; internal::addOrReplaceIfSamePos(currSlice, sliceEndVertex); break; @@ -830,7 +1167,8 @@ template std::vector> dualSliceAtIntersectsForOffset(Polyline const &originalPline, Polyline const &rawOffsetPline, - Polyline const &dualRawOffsetPline, Real offset) { + Polyline const &dualRawOffsetPline, Real offset, + ParallelOffsetOptions const &options) { std::vector> result; if (rawOffsetPline.size() < 2) { return result; @@ -850,16 +1188,44 @@ dualSliceAtIntersectsForOffset(Polyline const &originalPline, // together, because slices may not all form closed loops/polylines so must go in order of // indexes to ensure longest sitched results are formed) std::map>> intersectsLookup; + std::vector queryStack; + queryStack.reserve(8); + // Keep open-polyline clipping conservative to avoid fragment loss around caps and near-tangent + // turns. Relaxation is only allowed for closed, non-round join handling. + const bool enforceMinDistance = + !originalPline.isClosed() || options.joinType == OffsetJoinType::Round; + auto pointValid = [&](Vector2 const &p) { + if (!enforceMinDistance) { + return true; + } + return pointValidForOffset(originalPline, offset, origPlineSpatialIndex, p, queryStack); + }; if (!originalPline.isClosed()) { - // find intersects between circles generated at original open polyline end points and raw offset - // polyline + // find intersects between raw offset polyline and end-cap clip geometry std::vector>> intersects; - internal::offsetCircleIntersectsWithPline(rawOffsetPline, offset, originalPline[0].pos(), - rawOffsetPlineSpatialIndex, intersects); - internal::offsetCircleIntersectsWithPline(rawOffsetPline, offset, - originalPline.lastVertex().pos(), - rawOffsetPlineSpatialIndex, intersects); + intersects.reserve(rawOffsetPline.size()); + if (options.endCapType == OffsetEndCapType::Butt) { + Vector2 start_tangent = internal::openPolylineEndpointTangent(originalPline, true); + Vector2 end_tangent = internal::openPolylineEndpointTangent(originalPline, false); + internal::offsetLineIntersectsWithPline(rawOffsetPline, originalPline[0].pos(), start_tangent, + intersects); + internal::offsetLineIntersectsWithPline(rawOffsetPline, originalPline.lastVertex().pos(), + end_tangent, intersects); + } else { + Vector2 start_circle_center = + internal::openPolylineEndCapCircleCenter(originalPline, offset, true, options.endCapType); + Vector2 end_circle_center = internal::openPolylineEndCapCircleCenter( + originalPline, offset, false, options.endCapType); + std::vector circleQueryResults; + circleQueryResults.reserve(rawOffsetPline.size()); + internal::offsetCircleIntersectsWithPline(rawOffsetPline, offset, start_circle_center, + rawOffsetPlineSpatialIndex, intersects, + circleQueryResults, queryStack); + internal::offsetCircleIntersectsWithPline(rawOffsetPline, offset, end_circle_center, + rawOffsetPlineSpatialIndex, intersects, + circleQueryResults, queryStack); + } for (auto const &pair : intersects) { intersectsLookup[pair.first].push_back(pair.second); } @@ -879,11 +1245,8 @@ dualSliceAtIntersectsForOffset(Polyline const &originalPline, intersectsLookup[intr.sIndex1].push_back(intr.point2); } - std::vector queryStack; - queryStack.reserve(8); if (intersectsLookup.size() == 0) { - if (!pointValidForOffset(originalPline, offset, origPlineSpatialIndex, rawOffsetPline[0].pos(), - queryStack)) { + if (!pointValid(rawOffsetPline[0].pos())) { return result; } // copy and convert raw offset into open polyline @@ -939,8 +1302,7 @@ dualSliceAtIntersectsForOffset(Polyline const &originalPline, auto iter = intersectsLookup.find(index); if (iter == intersectsLookup.end()) { // no intersect found, test segment will be valid before adding the vertex - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, - rawOffsetPline[index].pos(), queryStack)) { + if (!pointValid(rawOffsetPline[index].pos())) { break; } @@ -953,8 +1315,7 @@ dualSliceAtIntersectsForOffset(Polyline const &originalPline, } else { // intersect found, test segment will be valid before finishing first open polyline Vector2 const &intersectPos = iter->second[0]; - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, - intersectPos, queryStack)) { + if (!pointValid(intersectPos)) { break; } @@ -963,8 +1324,7 @@ dualSliceAtIntersectsForOffset(Polyline const &originalPline, PlineVertex sliceEndVertex = PlineVertex(intersectPos, Real(0)); auto midpoint = segMidpoint(split.updatedStart, sliceEndVertex); - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, midpoint, - queryStack)) { + if (!pointValid(midpoint)) { break; } @@ -1008,21 +1368,18 @@ dualSliceAtIntersectsForOffset(Polyline const &originalPline, } // test start point - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, - split.updatedStart.pos(), queryStack)) { + if (!pointValid(split.updatedStart.pos())) { continue; } // test end point - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, - split.splitVertex.pos(), queryStack)) { + if (!pointValid(split.splitVertex.pos())) { continue; } // test mid point auto midpoint = segMidpoint(split.updatedStart, split.splitVertex); - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, midpoint, - queryStack)) { + if (!pointValid(midpoint)) { continue; } @@ -1041,8 +1398,7 @@ dualSliceAtIntersectsForOffset(Polyline const &originalPline, // build the segment between the last intersect in siList and the next intersect found // check that the first point is valid - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, siList.back(), - queryStack)) { + if (!pointValid(siList.back())) { continue; } @@ -1061,8 +1417,7 @@ dualSliceAtIntersectsForOffset(Polyline const &originalPline, break; } // check that vertex point is valid - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, - rawOffsetPline[index].pos(), queryStack)) { + if (!pointValid(rawOffsetPline[index].pos())) { isValidPline = false; break; } @@ -1083,27 +1438,25 @@ dualSliceAtIntersectsForOffset(Polyline const &originalPline, // check intersect pos is valid (which will also be end vertex position) Vector2 const &intersectPos = nextIntr->second[0]; - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, - intersectPos, queryStack)) { + if (!pointValid(intersectPos)) { isValidPline = false; break; } - std::size_t nextIndex = utils::nextWrappingIndex(index, rawOffsetPline); - SplitResult split = - splitAtPoint(currSlice.lastVertex(), rawOffsetPline[nextIndex], intersectPos); + std::size_t l_nextIndex = utils::nextWrappingIndex(index, rawOffsetPline); + SplitResult l_split = + splitAtPoint(currSlice.lastVertex(), rawOffsetPline[l_nextIndex], intersectPos); PlineVertex sliceEndVertex = PlineVertex(intersectPos, Real(0)); // check mid point is valid - Vector2 mp = segMidpoint(split.updatedStart, sliceEndVertex); - if (!internal::pointValidForOffset(originalPline, offset, origPlineSpatialIndex, mp, - queryStack)) { + Vector2 mp = segMidpoint(l_split.updatedStart, sliceEndVertex); + if (!pointValid(mp)) { isValidPline = false; break; } // trim last added vertex and add final intersect position - currSlice.lastVertex() = split.updatedStart; + currSlice.lastVertex() = l_split.updatedStart; internal::addOrReplaceIfSamePos(currSlice, sliceEndVertex); break; @@ -1255,21 +1608,68 @@ stitchOffsetSlicesTogether(std::vector> const &slices, b /// Creates the paralell offset polylines to the polyline given. template std::vector> parallelOffset(Polyline const &pline, Real offset, - bool hasSelfIntersects = false) { + ParallelOffsetOptions const &options = {}) { using namespace internal; - if (pline.size() < 2) { + CAVC_ASSERT(options.miterLimit >= Real(1), "miterLimit must be >= 1"); + + Polyline cleaned = pruneSingularities(pline, utils::realPrecision()); + if (cleaned.size() < 2) { + return std::vector>(); + } + auto rawOffset = createRawOffsetPline(cleaned, offset, options); + if (rawOffset.size() < 2) { return std::vector>(); } - auto rawOffset = createRawOffsetPline(pline, offset); - if (pline.isClosed() && !hasSelfIntersects) { - auto slices = slicesFromRawOffset(pline, rawOffset, offset); - return stitchOffsetSlicesTogether(slices, pline.isClosed(), rawOffset.size() - 1); + if (cleaned.isClosed() && !options.hasSelfIntersects) { + auto slices = slicesFromRawOffset(cleaned, rawOffset, offset); + auto result = stitchOffsetSlicesTogether(slices, cleaned.isClosed(), rawOffset.size() - 1); + + auto filterSimpleClosedLoops = [](std::vector> const &candidates) { + std::vector> filtered; + filtered.reserve(candidates.size()); + for (auto const &candidate : candidates) { + if (!candidate.isClosed() || candidate.size() < 3) { + continue; + } + + auto spatialIndex = createApproxSpatialIndex(candidate); + std::vector> intersects; + allSelfIntersects(candidate, intersects, spatialIndex); + if (!intersects.empty()) { + continue; + } + + filtered.push_back(candidate); + } + return filtered; + }; + + auto filteredResult = filterSimpleClosedLoops(result); + if (!filteredResult.empty()) { + return filteredResult; + } + + auto tryRoundFallback = [&]() { + ParallelOffsetOptions fallbackOptions = options; + fallbackOptions.joinType = OffsetJoinType::Round; + auto fallbackResult = parallelOffset(cleaned, offset, fallbackOptions); + return filterSimpleClosedLoops(fallbackResult); + }; + + if (options.joinType != OffsetJoinType::Round) { + auto roundFallback = tryRoundFallback(); + if (!roundFallback.empty()) { + return roundFallback; + } + } + + return result; } // not closed polyline or has self intersects, must apply dual clipping - auto dualRawOffset = createRawOffsetPline(pline, -offset); - auto slices = dualSliceAtIntersectsForOffset(pline, rawOffset, dualRawOffset, offset); - return stitchOffsetSlicesTogether(slices, pline.isClosed(), rawOffset.size() - 1); + auto dualRawOffset = createRawOffsetPline(cleaned, -offset, options); + auto slices = dualSliceAtIntersectsForOffset(cleaned, rawOffset, dualRawOffset, offset, options); + return stitchOffsetSlicesTogether(slices, cleaned.isClosed(), rawOffset.size() - 1); } } // namespace cavc diff --git a/include/cavc/polylineoffsetislands.hpp b/include/cavc/polylineoffsetislands.hpp index 5a79b3a..3670bbf 100644 --- a/include/cavc/polylineoffsetislands.hpp +++ b/include/cavc/polylineoffsetislands.hpp @@ -3,6 +3,8 @@ #include "polyline.hpp" #include "polylinecombine.hpp" #include "polylineoffset.hpp" +#include +#include #include #include @@ -18,6 +20,162 @@ template struct OffsetLoopSet { std::vector> cwLoops; }; +enum class OffsetLoopRole { + Outer = 0, + Hole = 1, +}; + +constexpr std::size_t kNoParentOffsetLoop = std::numeric_limits::max(); + +template struct OffsetLoopTopologyNode { + OffsetLoopRole role = OffsetLoopRole::Outer; + std::size_t sourceIndex = 0; + std::size_t parentIndex = kNoParentOffsetLoop; +}; + +template void sortOffsetLoopsStable(std::vector> &loops) { + auto absArea = [](OffsetLoop const &loop) { return std::abs(getArea(loop.polyline)); }; + + std::stable_sort(loops.begin(), loops.end(), + [&](OffsetLoop const &lhs, OffsetLoop const &rhs) { + Real lhsAbsArea = absArea(lhs); + Real rhsAbsArea = absArea(rhs); + if (lhsAbsArea != rhsAbsArea) { + return lhsAbsArea > rhsAbsArea; + } + + if (lhs.spatialIndex.minX() != rhs.spatialIndex.minX()) { + return lhs.spatialIndex.minX() < rhs.spatialIndex.minX(); + } + if (lhs.spatialIndex.minY() != rhs.spatialIndex.minY()) { + return lhs.spatialIndex.minY() < rhs.spatialIndex.minY(); + } + if (lhs.spatialIndex.maxX() != rhs.spatialIndex.maxX()) { + return lhs.spatialIndex.maxX() < rhs.spatialIndex.maxX(); + } + if (lhs.spatialIndex.maxY() != rhs.spatialIndex.maxY()) { + return lhs.spatialIndex.maxY() < rhs.spatialIndex.maxY(); + } + + return lhs.parentLoopIndex < rhs.parentLoopIndex; + }); +} + +template void sortOffsetLoopSetStable(OffsetLoopSet &loopSet) { + sortOffsetLoopsStable(loopSet.ccwLoops); + sortOffsetLoopsStable(loopSet.cwLoops); +} + +template +std::vector> +buildOffsetLoopTopology(OffsetLoopSet const &loopSet, + Real boundaryEpsilon = utils::realPrecision()) { + CAVC_ASSERT(boundaryEpsilon >= Real(0), "boundaryEpsilon must be >= 0"); + + struct FlattenedLoopRef { + OffsetLoopRole role; + std::size_t sourceIndex; + OffsetLoop const *loop; + Real absArea; + }; + + std::vector flattened; + flattened.reserve(loopSet.ccwLoops.size() + loopSet.cwLoops.size()); + for (std::size_t i = 0; i < loopSet.ccwLoops.size(); ++i) { + flattened.push_back({OffsetLoopRole::Outer, i, &loopSet.ccwLoops[i], + std::abs(getArea(loopSet.ccwLoops[i].polyline))}); + } + + for (std::size_t i = 0; i < loopSet.cwLoops.size(); ++i) { + flattened.push_back({OffsetLoopRole::Hole, i, &loopSet.cwLoops[i], + std::abs(getArea(loopSet.cwLoops[i].polyline))}); + } + + std::stable_sort(flattened.begin(), flattened.end(), + [](FlattenedLoopRef const &lhs, FlattenedLoopRef const &rhs) { + if (lhs.absArea != rhs.absArea) { + return lhs.absArea > rhs.absArea; + } + + if (lhs.loop->spatialIndex.minX() != rhs.loop->spatialIndex.minX()) { + return lhs.loop->spatialIndex.minX() < rhs.loop->spatialIndex.minX(); + } + if (lhs.loop->spatialIndex.minY() != rhs.loop->spatialIndex.minY()) { + return lhs.loop->spatialIndex.minY() < rhs.loop->spatialIndex.minY(); + } + if (lhs.loop->spatialIndex.maxX() != rhs.loop->spatialIndex.maxX()) { + return lhs.loop->spatialIndex.maxX() < rhs.loop->spatialIndex.maxX(); + } + if (lhs.loop->spatialIndex.maxY() != rhs.loop->spatialIndex.maxY()) { + return lhs.loop->spatialIndex.maxY() < rhs.loop->spatialIndex.maxY(); + } + + if (lhs.role != rhs.role) { + return lhs.role == OffsetLoopRole::Outer; + } + + return lhs.sourceIndex < rhs.sourceIndex; + }); + + std::vector> result(flattened.size()); + for (std::size_t i = 0; i < flattened.size(); ++i) { + result[i].role = flattened[i].role; + result[i].sourceIndex = flattened[i].sourceIndex; + } + + auto pointInsideExpandedLoopBounds = [&](FlattenedLoopRef const &loop_ref, + Vector2 const &point) { + Real const min_x = loop_ref.loop->spatialIndex.minX() - boundaryEpsilon; + Real const min_y = loop_ref.loop->spatialIndex.minY() - boundaryEpsilon; + Real const max_x = loop_ref.loop->spatialIndex.maxX() + boundaryEpsilon; + Real const max_y = loop_ref.loop->spatialIndex.maxY() + boundaryEpsilon; + return point.x() >= min_x && point.x() <= max_x && point.y() >= min_y && point.y() <= max_y; + }; + + for (std::size_t i = 0; i < flattened.size(); ++i) { + auto const &child = flattened[i]; + if (child.loop->polyline.size() == 0) { + continue; + } + + Vector2 samplePoint = child.loop->polyline[0].pos(); + std::size_t parentIndex = kNoParentOffsetLoop; + Real parentAbsArea = std::numeric_limits::max(); + + for (std::size_t j = 0; j < flattened.size(); ++j) { + if (i == j) { + continue; + } + + auto const &candidate = flattened[j]; + if (candidate.role == child.role) { + continue; + } + if (candidate.absArea <= child.absArea) { + continue; + } + if (!pointInsideExpandedLoopBounds(candidate, samplePoint)) { + continue; + } + + PointContainment containment = + getPointContainment(candidate.loop->polyline, samplePoint, boundaryEpsilon); + if (containment == PointContainment::Outside) { + continue; + } + + if (candidate.absArea < parentAbsArea) { + parentIndex = j; + parentAbsArea = candidate.absArea; + } + } + + result[i].parentIndex = parentIndex; + } + + return result; +} + template class ParallelOffsetIslands { public: ParallelOffsetIslands() {} @@ -103,34 +261,63 @@ template class ParallelOffsetIslands { template void ParallelOffsetIslands::createOffsetLoops(const OffsetLoopSet &input, Real absDelta) { + std::size_t const expected_loop_count = input.ccwLoops.size() + input.cwLoops.size(); // create counter clockwise offset loops m_ccwOffsetLoops.clear(); + m_ccwOffsetLoops.reserve(expected_loop_count); + m_cwOffsetLoops.clear(); + m_cwOffsetLoops.reserve(expected_loop_count); + + auto appendOffsetsWithBatchIndex = [&](std::vector> &offsets, + std::size_t parent_index, + std::vector> &target) { + if (offsets.size() == 0) { + return; + } + + std::vector> indexes = createApproxSpatialIndices(offsets); + CAVC_ASSERT(indexes.size() == offsets.size(), "batch index size mismatch"); + + target.reserve(target.size() + offsets.size()); + for (std::size_t i = 0; i < offsets.size(); ++i) { + target.push_back({parent_index, std::move(offsets[i]), std::move(indexes[i])}); + } + }; + std::size_t parentIndex = 0; for (auto const &loop : input.ccwLoops) { auto offsets = parallelOffset(loop.polyline, absDelta); + std::vector> ccwOffsets; + ccwOffsets.reserve(offsets.size()); for (auto &offset : offsets) { // must check if orientation inverted (due to collapse of very narrow or small input) if (getArea(offset) < Real(0)) { continue; } - auto index = createApproxSpatialIndex(offset); - m_ccwOffsetLoops.push_back({parentIndex, std::move(offset), std::move(index)}); + ccwOffsets.push_back(std::move(offset)); } + + appendOffsetsWithBatchIndex(ccwOffsets, parentIndex, m_ccwOffsetLoops); parentIndex += 1; } // create clockwise offset loops (note counter clockwise loops may result from outward offset) - m_cwOffsetLoops.clear(); for (auto const &loop : input.cwLoops) { auto offsets = parallelOffset(loop.polyline, absDelta); + std::vector> cwOffsets; + std::vector> ccwOffsets; + cwOffsets.reserve(offsets.size()); + ccwOffsets.reserve(offsets.size()); for (auto &offset : offsets) { - auto index = createApproxSpatialIndex(offset); if (getArea(offset) < Real(0)) { - m_cwOffsetLoops.push_back({parentIndex, std::move(offset), std::move(index)}); + cwOffsets.push_back(std::move(offset)); } else { - m_ccwOffsetLoops.push_back({parentIndex, std::move(offset), std::move(index)}); + ccwOffsets.push_back(std::move(offset)); } } + + appendOffsetsWithBatchIndex(cwOffsets, parentIndex, m_cwOffsetLoops); + appendOffsetsWithBatchIndex(ccwOffsets, parentIndex, m_ccwOffsetLoops); parentIndex += 1; } } @@ -151,12 +338,17 @@ template void ParallelOffsetIslands::createOffsetLoopsInde } template void ParallelOffsetIslands::createSlicePoints() { + std::size_t const totalOffsetCount = totalOffsetLoopsCount(); + m_visitedLoopPairs.clear(); + m_visitedLoopPairs.reserve(totalOffsetCount * 2); m_slicePointSets.clear(); + m_slicePointSets.reserve(totalOffsetCount); m_slicePointsLookup.clear(); + m_queryResults.clear(); + m_queryResults.reserve(totalOffsetCount); // find all intersects between all offsets - std::size_t totalOffsetCount = totalOffsetLoopsCount(); m_slicePointsLookup.resize(totalOffsetCount); PlineIntersectsResult intrsResults; for (std::size_t i = 0; i < totalOffsetCount; ++i) { @@ -259,7 +451,6 @@ void ParallelOffsetIslands::createSlicesFromLoop(std::size_t loopIndex, Re std::sort(kvp.second.begin(), kvp.second.end(), cmp); } - for (auto const &kvp : m_loopDissectionPoints) { // start index for the slice we're about to build std::size_t sIndex = kvp.first; @@ -332,10 +523,10 @@ void ParallelOffsetIslands::createSlicesFromLoop(std::size_t loopIndex, Re // trim last added vertex and add final intersect position PlineVertex endVertex = PlineVertex(intersectPos, Real(0)); - std::size_t nextIndex = utils::nextWrappingIndex(index, pline); - SplitResult split = - splitAtPoint(currSlice.pline.lastVertex(), pline[nextIndex], intersectPos); - currSlice.pline.lastVertex() = split.updatedStart; + std::size_t l_nextIndex = utils::nextWrappingIndex(index, pline); + SplitResult l_split = + splitAtPoint(currSlice.pline.lastVertex(), pline[l_nextIndex], intersectPos); + currSlice.pline.lastVertex() = l_split.updatedStart; internal::addOrReplaceIfSamePos(currSlice.pline, endVertex); currSlice.endLoopIndex = nextIntr->second[0].otherLoopIndex; break; @@ -372,8 +563,10 @@ OffsetLoopSet ParallelOffsetIslands::compute(const OffsetLoopSet slices; std::size_t totalOffsetsCount = totalOffsetLoopsCount(); + slices.reserve(totalOffsetsCount * 2); std::vector> resultSlices; + resultSlices.reserve(totalOffsetsCount * 2); for (std::size_t i = 0; i < totalOffsetsCount; ++i) { if (m_slicePointsLookup[i].size() == 0) { @@ -399,20 +592,45 @@ OffsetLoopSet ParallelOffsetIslands::compute(const OffsetLoopSet> stitched = internal::stitchOrderedSlicesIntoClosedPolylines(resultSlices); + result.ccwLoops.reserve(result.ccwLoops.size() + stitched.size()); + result.cwLoops.reserve(result.cwLoops.size() + stitched.size()); + + std::vector> stitchedCcwLoops; + std::vector> stitchedCwLoops; + stitchedCcwLoops.reserve(stitched.size()); + stitchedCwLoops.reserve(stitched.size()); for (auto &r : stitched) { Real area = getArea(r); if (std::abs(area) < 1e-4) { continue; } - auto spatialIndex = createApproxSpatialIndex(r); if (area < Real(0)) { - result.cwLoops.push_back({0, std::move(r), std::move(spatialIndex)}); + stitchedCwLoops.push_back(std::move(r)); } else { - result.ccwLoops.push_back({0, std::move(r), std::move(spatialIndex)}); + stitchedCcwLoops.push_back(std::move(r)); } } + auto appendStitchedWithBatchIndex = [&](std::vector> &loops, + std::vector> &target) { + if (loops.size() == 0) { + return; + } + + std::vector> indexes = createApproxSpatialIndices(loops); + CAVC_ASSERT(indexes.size() == loops.size(), "batch index size mismatch"); + + target.reserve(target.size() + loops.size()); + for (std::size_t i = 0; i < loops.size(); ++i) { + target.push_back({0, std::move(loops[i]), std::move(indexes[i])}); + } + }; + + appendStitchedWithBatchIndex(stitchedCcwLoops, result.ccwLoops); + appendStitchedWithBatchIndex(stitchedCwLoops, result.cwLoops); + + sortOffsetLoopSetStable(result); return result; } diff --git a/include/cavc/staticspatialindex.hpp b/include/cavc/staticspatialindex.hpp index bfaa4fd..7d6fe46 100644 --- a/include/cavc/staticspatialindex.hpp +++ b/include/cavc/staticspatialindex.hpp @@ -5,7 +5,6 @@ #include #include #include -#include #include namespace cavc { @@ -83,9 +82,33 @@ template class StaticSpatialIndex { Real width = m_maxX - m_minX; Real height = m_maxY - m_minY; std::unique_ptr hilbertValues(new std::uint32_t[m_numItems]); + // hilbert max input value for x and y + const Real hilbertMax = static_cast((1u << 16) - 1u); std::size_t pos = 0; + // convert coordinate to hilbert space while guarding against degenerate extents + auto quantize = [&](Real minCoord, Real maxCoord, Real extent, Real extentMin) { + if (extent <= Real(0)) { + return static_cast(0); + } + + Real value = std::floor(hilbertMax * ((minCoord + maxCoord) / Real(2) - extentMin) / extent); + if (!std::isfinite(value)) { + return static_cast(0); + } + + if (value < Real(0)) { + return static_cast(0); + } + + if (value > hilbertMax) { + return static_cast(hilbertMax); + } + + return static_cast(value); + }; + for (std::size_t i = 0; i < m_numItems; ++i) { pos = 4 * i; Real minX = m_boxes[pos++]; @@ -93,16 +116,9 @@ template class StaticSpatialIndex { Real maxX = m_boxes[pos++]; Real maxY = m_boxes[pos++]; - // hilbert max input value for x and y - const Real hilbertMax = static_cast((1 << 16) - 1); - // mapping the x and y coordinates of the center of the box to values in the range - // [0 -> n - 1] such that the min of the entire set of bounding boxes maps to 0 and the max of - // the entire set of bounding boxes maps to n - 1 our 2d space is x: [0 -> n-1] and - // y: [0 -> n-1], our 1d hilbert curve value space is d: [0 -> n^2 - 1] - Real x = std::floor(hilbertMax * ((minX + maxX) / 2 - m_minX) / width); - std::uint32_t hx = static_cast(x); - Real y = std::floor(hilbertMax * ((minY + maxY) / 2 - m_minY) / height); - std::uint32_t hy = static_cast(y); + // map box center to hilbert coordinates in [0, hilbertMax] + std::uint32_t hx = quantize(minX, maxX, width, m_minX); + std::uint32_t hy = quantize(minY, maxY, height, m_minY); hilbertValues[i] = hilbertXYToIndex(hx, hy); } diff --git a/include/cavc/vector.hpp b/include/cavc/vector.hpp index e2f6b65..f530b39 100644 --- a/include/cavc/vector.hpp +++ b/include/cavc/vector.hpp @@ -3,6 +3,7 @@ #include "mathutils.hpp" #include #include + namespace cavc { template class Vector { public: diff --git a/include/cavc/vector2.hpp b/include/cavc/vector2.hpp index 6edd932..8b8cd75 100644 --- a/include/cavc/vector2.hpp +++ b/include/cavc/vector2.hpp @@ -3,6 +3,7 @@ #include "mathutils.hpp" #include "vector.hpp" #include + namespace cavc { template using Vector2 = Vector; diff --git a/src/cavaliercontours.cpp b/src/cavaliercontours.cpp index 5ab2a65..1364fe5 100644 --- a/src/cavaliercontours.cpp +++ b/src/cavaliercontours.cpp @@ -1,9 +1,11 @@ #include "cavaliercontours.h" #include "cavc/polylinecombine.hpp" #include "cavc/polylineoffset.hpp" +#include "cavc/polylineoffsetislands.hpp" #include +#include #include -#include +#include #define CAVC_BEGIN_TRY_CATCH try { @@ -17,13 +19,84 @@ struct cavc_pline { cavc::Polyline data; cavc_pline() = default; - cavc_pline(cavc::Polyline &&data) noexcept : data(data) {} + cavc_pline(cavc::Polyline &&p_data) noexcept : data(std::move(p_data)) {} }; struct cavc_pline_list { std::vector> data; }; +struct cavc_spatial_index { + cavc::StaticSpatialIndex data; + uint32_t item_count; + cavc_spatial_index(cavc::StaticSpatialIndex &&p_data, uint32_t p_item_count) noexcept + : data(std::move(p_data)), item_count(p_item_count) {} +}; + +struct cavc_offset_loop_topology { + std::vector nodes; +}; + +static cavc_tolerances to_api_tolerances(cavc::utils::EpsilonConfig const &config) { + return {config.realThreshold, config.realPrecision, config.sliceJoinThreshold, + config.offsetThreshold}; +} + +static cavc::utils::EpsilonConfig to_cpp_tolerances(cavc_tolerances const &config) { + return {config.real_threshold, config.real_precision, config.slice_join_threshold, + config.offset_threshold}; +} + +static cavc::ClosedPolylineWinding to_cpp_closed_winding(cavc_closed_winding winding) { + switch (winding) { + case CAVC_WINDING_KEEP: + return cavc::ClosedPolylineWinding::Keep; + case CAVC_WINDING_COUNTER_CLOCKWISE: + return cavc::ClosedPolylineWinding::CounterClockwise; + case CAVC_WINDING_CLOCKWISE: + return cavc::ClosedPolylineWinding::Clockwise; + } + CAVC_ASSERT(false, "Unhandled closed winding enum"); + return cavc::ClosedPolylineWinding::Keep; +} + +static cavc::OffsetJoinType to_cpp_offset_join_type(cavc_offset_join_type join_type) { + switch (join_type) { + case CAVC_OFFSET_JOIN_ROUND: + return cavc::OffsetJoinType::Round; + case CAVC_OFFSET_JOIN_MITER: + return cavc::OffsetJoinType::Miter; + case CAVC_OFFSET_JOIN_BEVEL: + return cavc::OffsetJoinType::Bevel; + } + CAVC_ASSERT(false, "Unhandled offset join enum"); + return cavc::OffsetJoinType::Round; +} + +static cavc::OffsetEndCapType to_cpp_offset_end_cap_type(cavc_offset_end_cap_type end_cap_type) { + switch (end_cap_type) { + case CAVC_OFFSET_END_CAP_ROUND: + return cavc::OffsetEndCapType::Round; + case CAVC_OFFSET_END_CAP_SQUARE: + return cavc::OffsetEndCapType::Square; + case CAVC_OFFSET_END_CAP_BUTT: + return cavc::OffsetEndCapType::Butt; + } + CAVC_ASSERT(false, "Unhandled offset end cap enum"); + return cavc::OffsetEndCapType::Round; +} + +static cavc::ParallelOffsetOptions +to_cpp_parallel_offset_options(cavc_parallel_offset_options const &options) { + CAVC_ASSERT(options.miter_limit >= cavc_real(1), "miter_limit must be >= 1"); + cavc::ParallelOffsetOptions result; + result.hasSelfIntersects = options.may_have_self_intersects != 0; + result.joinType = to_cpp_offset_join_type(options.join_type); + result.endCapType = to_cpp_offset_end_cap_type(options.end_cap_type); + result.miterLimit = options.miter_limit; + return result; +} + // helper to move vector of plines to cavc_pline_list static void move_to_list(std::vector> &&plines, cavc_pline_list *list) { list->data.reserve(plines.size()); @@ -39,8 +112,12 @@ static void copy_to_pline(cavc_pline *api_pline, cavc_vertex const *vertex_data, api_pline->data.vertexes().clear(); api_pline->data.vertexes().reserve(vertex_count); - for (uint32_t i = 0; i < vertex_count; ++i) { - api_pline->data.addVertex(vertex_data[i].x, vertex_data[i].y, vertex_data[i].bulge); + // Use std::span for safe buffer access + if (vertex_data != nullptr && vertex_count > 0) { + std::span vertices_span(vertex_data, vertex_count); + for (const auto &vertex : vertices_span) { + api_pline->data.addVertex(vertex.x, vertex.y, vertex.bulge); + } } } @@ -48,10 +125,68 @@ static void copy_to_pline(cavc_pline *api_pline, cavc_vertex const *vertex_data, static void copy_to_vertex_data(cavc_pline const *api_pline, cavc_vertex *vertex_data) { auto const &pline = api_pline->data; uint32_t vertex_count = static_cast(pline.size()); - for (uint32_t i = 0; i < vertex_count; ++i) { - auto const &v = pline[i]; - vertex_data[i] = cavc_vertex{v.x(), v.y(), v.bulge()}; + + if (vertex_data != nullptr && vertex_count > 0) { + std::span output_span(vertex_data, vertex_count); + for (uint32_t i = 0; i < vertex_count; ++i) { + auto const &v = pline[i]; + output_span[i] = cavc_vertex{v.x(), v.y(), v.bulge()}; + } + } +} + +static std::vector query_spatial_index(cavc_spatial_index const *spatial_index, + cavc_real min_x, cavc_real min_y, + cavc_real max_x, cavc_real max_y) { + std::vector results; + spatial_index->data.query(min_x, min_y, max_x, max_y, results); + return results; +} + +static cavc_offset_loop_role to_api_offset_loop_role(cavc::OffsetLoopRole role) { + switch (role) { + case cavc::OffsetLoopRole::Outer: + return CAVC_OFFSET_LOOP_ROLE_OUTER; + case cavc::OffsetLoopRole::Hole: + return CAVC_OFFSET_LOOP_ROLE_HOLE; + } + CAVC_ASSERT(false, "Unhandled offset loop role"); + return CAVC_OFFSET_LOOP_ROLE_OUTER; +} + +static cavc_offset_loop_topology_node +to_api_topology_node(cavc::OffsetLoopTopologyNode const &node) { + CAVC_ASSERT(node.sourceIndex <= std::numeric_limits::max(), + "source index exceeds uint32_t"); + + uint32_t parent_index = CAVC_OFFSET_LOOP_NO_PARENT; + if (node.parentIndex != cavc::kNoParentOffsetLoop) { + CAVC_ASSERT(node.parentIndex <= std::numeric_limits::max(), + "parent index exceeds uint32_t"); + parent_index = static_cast(node.parentIndex); + } + + return {to_api_offset_loop_role(node.role), static_cast(node.sourceIndex), + parent_index}; +} + +static std::vector> to_cpp_offset_loops(cavc_pline const *const *loops, + uint32_t loop_count) { + CAVC_ASSERT(loop_count == 0 || loops != nullptr, "non-zero loop count requires loop pointer"); + + std::vector> result; + result.reserve(loop_count); + + for (uint32_t i = 0; i < loop_count; ++i) { + CAVC_ASSERT(loops[i] != nullptr, "null loop pointer not allowed"); + auto const &loop = loops[i]->data; + CAVC_ASSERT(loop.isClosed(), "offset topology requires closed loops"); + CAVC_ASSERT(loop.size() > 1, "offset topology requires loops with at least 2 vertices"); + + result.push_back({static_cast(i), loop, cavc::createApproxSpatialIndex(loop)}); } + + return result; } cavc_pline *cavc_pline_new(const cavc_vertex *vertex_data, uint32_t vertex_count, int is_closed) { @@ -148,6 +283,41 @@ void cavc_pline_set_is_closed(cavc_pline *pline, int is_closed) { CAVC_END_TRY_CATCH } +void cavc_pline_invert_direction(cavc_pline *pline) { + CAVC_ASSERT(pline, "null pline not allowed"); + CAVC_BEGIN_TRY_CATCH + cavc::invertDirection(pline->data); + CAVC_END_TRY_CATCH +} + +void cavc_pline_prune_singularities(cavc_pline const *pline, cavc_real epsilon, + cavc_pline **output) { + CAVC_ASSERT(pline, "null pline not allowed"); + CAVC_ASSERT(output, "null output not allowed"); + CAVC_BEGIN_TRY_CATCH + *output = new cavc_pline(cavc::pruneSingularities(pline->data, epsilon)); + CAVC_END_TRY_CATCH +} + +void cavc_pline_convert_arcs_to_lines(cavc_pline const *pline, cavc_real error, + cavc_pline **output) { + CAVC_ASSERT(pline, "null pline not allowed"); + CAVC_ASSERT(output, "null output not allowed"); + CAVC_BEGIN_TRY_CATCH + *output = new cavc_pline(cavc::convertArcsToLines(pline->data, error)); + CAVC_END_TRY_CATCH +} + +void cavc_pline_normalize(cavc_pline const *pline, cavc_real epsilon, + cavc_closed_winding closed_winding, cavc_pline **output) { + CAVC_ASSERT(pline, "null pline not allowed"); + CAVC_ASSERT(output, "null output not allowed"); + CAVC_BEGIN_TRY_CATCH + auto winding = to_cpp_closed_winding(closed_winding); + *output = new cavc_pline(cavc::normalizePolyline(pline->data, epsilon, winding)); + CAVC_END_TRY_CATCH +} + void cavc_pline_list_delete(cavc_pline_list *pline_list) { CAVC_BEGIN_TRY_CATCH delete pline_list; @@ -179,13 +349,114 @@ cavc_pline *cavc_pline_list_release(cavc_pline_list *pline_list, uint32_t index) CAVC_END_TRY_CATCH } +// cavc_spatial_index APIs +// ------------------------- +cavc_spatial_index *cavc_spatial_index_create(cavc_pline const *pline) { + CAVC_ASSERT(pline, "null pline not allowed"); + CAVC_ASSERT(pline->data.size() > 1, + "need at least 2 vertexes to form segments for spatial index"); + CAVC_BEGIN_TRY_CATCH + uint32_t segment_count = + static_cast(pline->data.isClosed() ? pline->data.size() : pline->data.size() - 1); + return new cavc_spatial_index(cavc::createApproxSpatialIndex(pline->data), segment_count); + CAVC_END_TRY_CATCH +} + +void cavc_spatial_index_delete(cavc_spatial_index *spatial_index) { + CAVC_BEGIN_TRY_CATCH + delete spatial_index; + CAVC_END_TRY_CATCH +} + +uint32_t cavc_spatial_index_item_count(cavc_spatial_index const *spatial_index) { + CAVC_ASSERT(spatial_index, "null spatial_index not allowed"); + CAVC_BEGIN_TRY_CATCH + return spatial_index->item_count; + CAVC_END_TRY_CATCH +} + +uint32_t cavc_spatial_index_query_count(cavc_spatial_index const *spatial_index, cavc_real min_x, + cavc_real min_y, cavc_real max_x, cavc_real max_y) { + CAVC_ASSERT(spatial_index, "null spatial_index not allowed"); + CAVC_BEGIN_TRY_CATCH + auto results = query_spatial_index(spatial_index, min_x, min_y, max_x, max_y); + return static_cast(results.size()); + CAVC_END_TRY_CATCH +} + +void cavc_spatial_index_query(cavc_spatial_index const *spatial_index, cavc_real min_x, + cavc_real min_y, cavc_real max_x, cavc_real max_y, + uint32_t *results_out) { + CAVC_ASSERT(spatial_index, "null spatial_index not allowed"); + CAVC_ASSERT(results_out, "null results_out not allowed"); + CAVC_BEGIN_TRY_CATCH + auto results = query_spatial_index(spatial_index, min_x, min_y, max_x, max_y); + for (std::size_t i = 0; i < results.size(); ++i) { + results_out[i] = static_cast(results[i]); + } + CAVC_END_TRY_CATCH +} + +cavc_offset_loop_topology *cavc_offset_loop_topology_build(cavc_pline const *const *ccw_loops, + uint32_t ccw_loop_count, + cavc_pline const *const *cw_loops, + uint32_t cw_loop_count, + cavc_real boundary_epsilon) { + CAVC_ASSERT(boundary_epsilon >= cavc_real(0), "boundary_epsilon must be >= 0"); + CAVC_BEGIN_TRY_CATCH + cavc::OffsetLoopSet loop_set; + loop_set.ccwLoops = to_cpp_offset_loops(ccw_loops, ccw_loop_count); + loop_set.cwLoops = to_cpp_offset_loops(cw_loops, cw_loop_count); + + auto cpp_topology = cavc::buildOffsetLoopTopology(loop_set, boundary_epsilon); + + auto *result = new cavc_offset_loop_topology(); + result->nodes.reserve(cpp_topology.size()); + for (auto const &node : cpp_topology) { + result->nodes.push_back(to_api_topology_node(node)); + } + + return result; + CAVC_END_TRY_CATCH +} + +void cavc_offset_loop_topology_delete(cavc_offset_loop_topology *topology) { + CAVC_BEGIN_TRY_CATCH + delete topology; + CAVC_END_TRY_CATCH +} + +uint32_t cavc_offset_loop_topology_count(cavc_offset_loop_topology const *topology) { + CAVC_ASSERT(topology, "null topology not allowed"); + CAVC_BEGIN_TRY_CATCH + CAVC_ASSERT(topology->nodes.size() <= std::numeric_limits::max(), + "node count exceeds uint32_t"); + return static_cast(topology->nodes.size()); + CAVC_END_TRY_CATCH +} + +cavc_offset_loop_topology_node +cavc_offset_loop_topology_get(cavc_offset_loop_topology const *topology, uint32_t index) { + CAVC_ASSERT(topology, "null topology not allowed"); + CAVC_ASSERT(index < topology->nodes.size(), "index is out of topology node range"); + CAVC_BEGIN_TRY_CATCH + return topology->nodes[index]; + CAVC_END_TRY_CATCH +} + +cavc_parallel_offset_options cavc_parallel_offset_default_options(void) { + CAVC_BEGIN_TRY_CATCH + return cavc_parallel_offset_options{0, CAVC_OFFSET_JOIN_ROUND, CAVC_OFFSET_END_CAP_ROUND, 4.0}; + CAVC_END_TRY_CATCH +} + void cavc_parallel_offset(cavc_pline const *pline, cavc_real delta, cavc_pline_list **output, - int option_flags) { + cavc_parallel_offset_options options) { CAVC_ASSERT(pline, "null pline not allowed"); CAVC_ASSERT(output, "null output not allowed"); CAVC_BEGIN_TRY_CATCH - bool mayHaveSelfIntersects = (option_flags & 0x1) != 0; - auto results = cavc::parallelOffset(pline->data, delta, mayHaveSelfIntersects); + auto cpp_options = to_cpp_parallel_offset_options(options); + auto results = cavc::parallelOffset(pline->data, delta, cpp_options); *output = new cavc_pline_list(); move_to_list(std::move(results), *output); CAVC_END_TRY_CATCH @@ -245,6 +516,25 @@ int cavc_get_winding_number(cavc_pline const *pline, cavc_point point) { CAVC_END_TRY_CATCH } +cavc_point_containment cavc_get_point_containment(cavc_pline const *pline, cavc_point point, + cavc_real boundary_epsilon) { + CAVC_ASSERT(pline, "null pline not allowed"); + CAVC_BEGIN_TRY_CATCH + auto containment = cavc::getPointContainment( + pline->data, cavc::Vector2(point.x, point.y), boundary_epsilon); + switch (containment) { + case cavc::PointContainment::Outside: + return CAVC_POINT_OUTSIDE; + case cavc::PointContainment::Inside: + return CAVC_POINT_INSIDE; + case cavc::PointContainment::OnBoundary: + return CAVC_POINT_ON_BOUNDARY; + } + CAVC_ASSERT(false, "Unhandled point containment enum"); + return CAVC_POINT_OUTSIDE; + CAVC_END_TRY_CATCH +} + void cavc_get_extents(cavc_pline const *pline, cavc_real *min_x, cavc_real *min_y, cavc_real *max_x, cavc_real *max_y) { CAVC_ASSERT(pline, "null pline not allowed"); @@ -270,3 +560,23 @@ void cavc_get_closest_point(cavc_pline const *pline, cavc_point input_point, *distance = closestPoint.distance(); CAVC_END_TRY_CATCH } + +void cavc_get_tolerances(cavc_tolerances *tolerances_out) { + CAVC_ASSERT(tolerances_out, "null tolerances_out not allowed"); + CAVC_BEGIN_TRY_CATCH + *tolerances_out = to_api_tolerances(cavc::utils::getEpsilonConfig()); + CAVC_END_TRY_CATCH +} + +void cavc_set_tolerances(cavc_tolerances const *tolerances) { + CAVC_ASSERT(tolerances, "null tolerances not allowed"); + CAVC_BEGIN_TRY_CATCH + cavc::utils::setEpsilonConfig(to_cpp_tolerances(*tolerances)); + CAVC_END_TRY_CATCH +} + +void cavc_reset_tolerances(void) { + CAVC_BEGIN_TRY_CATCH + cavc::utils::resetEpsilonConfig(); + CAVC_END_TRY_CATCH +} diff --git a/tests/benchmarks/CMakeLists.txt b/tests/benchmarks/CMakeLists.txt index dff9bea..9614bfd 100644 --- a/tests/benchmarks/CMakeLists.txt +++ b/tests/benchmarks/CMakeLists.txt @@ -1,6 +1,11 @@ -cmake_minimum_required(VERSION 3.11) +cmake_minimum_required(VERSION 3.21) + +option(CAVC_ENABLE_CLIPPER_BENCHMARKS "Enable clipper library for benchmarks (requires internet connection)" OFF) + +if(CAVC_ENABLE_CLIPPER_BENCHMARKS) + include(clipper.cmake) +endif() -include(clipper.cmake) find_package(benchmark REQUIRED) macro(add_benchmark name) @@ -24,6 +29,7 @@ macro(add_benchmark name) endmacro() add_benchmark(offsetbenchmarks) +add_benchmark(offsetislandsbenchmarks) add_benchmark(spatialindexbenchmarks) add_benchmark(extentsbenchmarks) add_benchmark(areabenchmarks) @@ -31,6 +37,8 @@ add_benchmark(pathlengthbenchmarks) add_benchmark(windingnumberbenchmarks) add_benchmark(combinebenchmarks) -add_benchmark(clipperbenchmarks) -target_link_libraries(clipperbenchmarks - PRIVATE clipper_static) +if(CAVC_ENABLE_CLIPPER_BENCHMARKS) + add_benchmark(clipperbenchmarks) + target_link_libraries(clipperbenchmarks + PRIVATE clipper_static) +endif() diff --git a/tests/benchmarks/areabenchmarks.cpp b/tests/benchmarks/areabenchmarks.cpp index f2733aa..394cd3b 100644 --- a/tests/benchmarks/areabenchmarks.cpp +++ b/tests/benchmarks/areabenchmarks.cpp @@ -2,10 +2,8 @@ #include "cavc/polyline.hpp" #include -static void area(NoSetup, TestProfile const &profile) { - cavc::getArea(profile.pline); -} -CAVC_CREATE_BENCHMARKS(area, NoSetup,area, benchmark::kNanosecond) +static void area(NoSetup, TestProfile const &profile) { cavc::getArea(profile.pline); } +CAVC_CREATE_BENCHMARKS(area, NoSetup, area, benchmark::kNanosecond) CAVC_CREATE_NO_ARCS_BENCHMARKS(area, NoSetup, area, 0.01, benchmark::kNanosecond) BENCHMARK_MAIN(); diff --git a/tests/benchmarks/benchmarkprofiles.h b/tests/benchmarks/benchmarkprofiles.h index 2204cda..1b9fb00 100644 --- a/tests/benchmarks/benchmarkprofiles.h +++ b/tests/benchmarks/benchmarkprofiles.h @@ -1,5 +1,6 @@ #include "cavc/polyline.hpp" #include + struct TestProfile { std::size_t offsetCount; double offsetDelta; diff --git a/tests/benchmarks/clipperbenchmarks.cpp b/tests/benchmarks/clipperbenchmarks.cpp index d6d433e..818ed97 100644 --- a/tests/benchmarks/clipperbenchmarks.cpp +++ b/tests/benchmarks/clipperbenchmarks.cpp @@ -1,6 +1,6 @@ -#include "clipper/clipper.hpp" #include "benchmarkprofiles.h" #include "cavc/polyline.hpp" +#include "clipper/clipper.hpp" #include const double clipperScaleFactor = 1e8; diff --git a/tests/benchmarks/combinebenchmarks.cpp b/tests/benchmarks/combinebenchmarks.cpp index 52c68f7..a3d8dd1 100644 --- a/tests/benchmarks/combinebenchmarks.cpp +++ b/tests/benchmarks/combinebenchmarks.cpp @@ -21,7 +21,8 @@ struct CombineShiftedSetup { std::size_t shiftedCount = 16; shiftedProfiles.reserve(shiftedCount); for (std::size_t i = 0; i < shiftedCount; ++i) { - double angle = static_cast(i) / shiftedCount * cavc::utils::tau(); + double angle = + static_cast(i) / static_cast(shiftedCount) * cavc::utils::tau(); shiftedProfiles.push_back(createShifted(angle)); } } diff --git a/tests/benchmarks/extentsbenchmarks.cpp b/tests/benchmarks/extentsbenchmarks.cpp index bdc8db9..f1c2532 100644 --- a/tests/benchmarks/extentsbenchmarks.cpp +++ b/tests/benchmarks/extentsbenchmarks.cpp @@ -2,9 +2,7 @@ #include "cavc/polyline.hpp" #include -static void extents(NoSetup, TestProfile const &profile) { - cavc::getExtents(profile.pline); -} +static void extents(NoSetup, TestProfile const &profile) { cavc::getExtents(profile.pline); } CAVC_CREATE_BENCHMARKS(extents, NoSetup, extents, benchmark::kNanosecond) CAVC_CREATE_NO_ARCS_BENCHMARKS(extents, NoSetup, extents, 0.01, benchmark::kNanosecond) diff --git a/tests/benchmarks/offsetislandsbenchmarks.cpp b/tests/benchmarks/offsetislandsbenchmarks.cpp new file mode 100644 index 0000000..70985bb --- /dev/null +++ b/tests/benchmarks/offsetislandsbenchmarks.cpp @@ -0,0 +1,185 @@ +#include "cavc/polylineoffsetislands.hpp" +#include +#include +#include +#include + +namespace { +using cavc::OffsetLoop; +using cavc::OffsetLoopSet; +using cavc::ParallelOffsetIslands; +using cavc::Polyline; +using cavc::StaticSpatialIndex; + +Polyline makeNgonLoop(std::size_t vertex_count, double radius, double center_x, + double center_y) { + CAVC_ASSERT(vertex_count >= 3, "vertex_count must be >= 3"); + + Polyline loop; + loop.isClosed() = true; + for (std::size_t i = 0; i < vertex_count; ++i) { + double angle = + static_cast(i) * cavc::utils::tau() / static_cast(vertex_count); + loop.addVertex(center_x + radius * std::cos(angle), center_y + radius * std::sin(angle), 0.0); + } + return loop; +} + +Polyline makeAxisAlignedRect(double min_x, double min_y, double max_x, double max_y, + bool clockwise) { + Polyline loop; + loop.addVertex(min_x, min_y, 0.0); + loop.addVertex(max_x, min_y, 0.0); + loop.addVertex(max_x, max_y, 0.0); + loop.addVertex(min_x, max_y, 0.0); + loop.isClosed() = true; + if (clockwise) { + cavc::invertDirection(loop); + } + return loop; +} + +OffsetLoop makeOffsetLoop(Polyline &&polyline, std::size_t parent_loop_index) { + auto spatial_index = cavc::createApproxSpatialIndex(polyline); + return {parent_loop_index, std::move(polyline), std::move(spatial_index)}; +} + +OffsetLoopSet createGridOffsetLoopSet(std::size_t grid_size) { + OffsetLoopSet result; + std::size_t loop_count = grid_size * grid_size; + result.ccwLoops.reserve(loop_count); + result.cwLoops.reserve(loop_count); + + double const pitch = 32.0; + double const outer_size = 24.0; + double const hole_margin = 5.0; + + for (std::size_t y = 0; y < grid_size; ++y) { + for (std::size_t x = 0; x < grid_size; ++x) { + std::size_t parent_index = y * grid_size + x; + double min_x = static_cast(x) * pitch; + double min_y = static_cast(y) * pitch; + double max_x = min_x + outer_size; + double max_y = min_y + outer_size; + + auto outer = makeAxisAlignedRect(min_x, min_y, max_x, max_y, false); + auto hole = makeAxisAlignedRect(min_x + hole_margin, min_y + hole_margin, max_x - hole_margin, + max_y - hole_margin, true); + + result.ccwLoops.push_back(makeOffsetLoop(std::move(outer), parent_index)); + result.cwLoops.push_back(makeOffsetLoop(std::move(hole), parent_index)); + } + } + + return result; +} + +struct BatchIndexSetup { + std::vector> loops; + + BatchIndexSetup(std::size_t loop_count, std::size_t vertex_count) { + loops.reserve(loop_count); + std::size_t cols = + static_cast(std::ceil(std::sqrt(static_cast(loop_count)))); + double const pitch = 96.0; + + for (std::size_t i = 0; i < loop_count; ++i) { + std::size_t row = i / cols; + std::size_t col = i % cols; + double radius = 20.0 + static_cast(i % 7); + double center_x = static_cast(col) * pitch; + double center_y = static_cast(row) * pitch; + loops.push_back(makeNgonLoop(vertex_count, radius, center_x, center_y)); + } + } +}; + +static void BM_createApproxSpatialIndicesScalar(benchmark::State &state) { + std::size_t loop_count = static_cast(state.range(0)); + std::size_t vertex_count = static_cast(state.range(1)); + BatchIndexSetup setup(loop_count, vertex_count); + + state.counters["loopCount"] = static_cast(loop_count); + state.counters["vertexCount"] = static_cast(vertex_count); + + for (auto _ : state) { + (void)_; + std::vector> indexes; + indexes.reserve(setup.loops.size()); + for (auto const &loop : setup.loops) { + indexes.push_back(cavc::createApproxSpatialIndex(loop)); + } + benchmark::DoNotOptimize(indexes); + } +} + +static void BM_createApproxSpatialIndicesBatch(benchmark::State &state) { + std::size_t loop_count = static_cast(state.range(0)); + std::size_t vertex_count = static_cast(state.range(1)); + BatchIndexSetup setup(loop_count, vertex_count); + + state.counters["loopCount"] = static_cast(loop_count); + state.counters["vertexCount"] = static_cast(vertex_count); + + for (auto _ : state) { + (void)_; + auto indexes = cavc::createApproxSpatialIndices(setup.loops); + benchmark::DoNotOptimize(indexes); + } +} + +static void BM_buildOffsetLoopTopologyLargeScale(benchmark::State &state) { + std::size_t grid_size = static_cast(state.range(0)); + OffsetLoopSet input = createGridOffsetLoopSet(grid_size); + + state.counters["outerLoopCount"] = static_cast(input.ccwLoops.size()); + state.counters["holeLoopCount"] = static_cast(input.cwLoops.size()); + + for (auto _ : state) { + (void)_; + auto topology = cavc::buildOffsetLoopTopology(input); + benchmark::DoNotOptimize(topology); + } +} + +static void BM_parallelOffsetIslandsComputeLargeScale(benchmark::State &state) { + std::size_t grid_size = static_cast(state.range(0)); + OffsetLoopSet input = createGridOffsetLoopSet(grid_size); + ParallelOffsetIslands algorithm; + + state.counters["outerLoopCount"] = static_cast(input.ccwLoops.size()); + state.counters["holeLoopCount"] = static_cast(input.cwLoops.size()); + + for (auto _ : state) { + (void)_; + auto result = algorithm.compute(input, 1.0); + benchmark::DoNotOptimize(result); + } +} + +BENCHMARK(BM_createApproxSpatialIndicesScalar) + ->Args({32, 16}) + ->Args({64, 32}) + ->Args({128, 64}) + ->Unit(benchmark::kMillisecond); + +BENCHMARK(BM_createApproxSpatialIndicesBatch) + ->Args({32, 16}) + ->Args({64, 32}) + ->Args({128, 64}) + ->Unit(benchmark::kMillisecond); + +BENCHMARK(BM_buildOffsetLoopTopologyLargeScale) + ->Arg(8) + ->Arg(12) + ->Arg(16) + ->Unit(benchmark::kMillisecond); + +BENCHMARK(BM_parallelOffsetIslandsComputeLargeScale) + ->Arg(4) + ->Arg(8) + ->Arg(12) + ->Unit(benchmark::kMillisecond); +} // namespace + +BENCHMARK_MAIN(); diff --git a/tests/benchmarks/pathlengthbenchmarks.cpp b/tests/benchmarks/pathlengthbenchmarks.cpp index b8629d8..1c959be 100644 --- a/tests/benchmarks/pathlengthbenchmarks.cpp +++ b/tests/benchmarks/pathlengthbenchmarks.cpp @@ -2,9 +2,7 @@ #include "cavc/polyline.hpp" #include -static void pathLength(NoSetup, TestProfile const &profile) { - cavc::getPathLength(profile.pline); -} +static void pathLength(NoSetup, TestProfile const &profile) { cavc::getPathLength(profile.pline); } CAVC_CREATE_BENCHMARKS(pathLength, NoSetup, pathLength, benchmark::kNanosecond) CAVC_CREATE_NO_ARCS_BENCHMARKS(pathLength, NoSetup, pathLength, 0.01, benchmark::kNanosecond) diff --git a/tests/polylinefactory/include/polylinefactory.hpp b/tests/polylinefactory/include/polylinefactory.hpp index 8235010..f4c5d1d 100644 --- a/tests/polylinefactory/include/polylinefactory.hpp +++ b/tests/polylinefactory/include/polylinefactory.hpp @@ -1,7 +1,9 @@ #ifndef CAVC_POLYLINEFACTORY_HPP #define CAVC_POLYLINEFACTORY_HPP + #include "cavaliercontours.h" -#include "cavc/polyline.hpp" +#include +#include struct cavc_pline_deleter { void operator()(cavc_pline *pline) { cavc_pline_delete(pline); } diff --git a/tests/polylinefactory/src/polylinefactory.cpp b/tests/polylinefactory/src/polylinefactory.cpp index 3751b45..4ba64f6 100644 --- a/tests/polylinefactory/src/polylinefactory.cpp +++ b/tests/polylinefactory/src/polylinefactory.cpp @@ -1,4 +1,5 @@ #include "polylinefactory.hpp" +#include static cavc_point pointOnCircle(cavc_real radius, cavc_point center, cavc_real angle) { return {center.x + radius * std::cos(angle), center.y + radius * std::sin(angle)}; diff --git a/tests/tests/CMakeLists.txt b/tests/tests/CMakeLists.txt index b2b02e2..5ea198c 100644 --- a/tests/tests/CMakeLists.txt +++ b/tests/tests/CMakeLists.txt @@ -23,7 +23,9 @@ macro(cavc_add_test name) GTest::gtest GTest::gmock) - gtest_add_tests(TARGET ${name} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) + gtest_discover_tests(${name} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + DISCOVERY_MODE PRE_TEST) endmacro() cavc_add_test(TEST_sample) @@ -32,3 +34,5 @@ cavc_add_test(TEST_cavc_pline_function) cavc_add_test(TEST_cavc_parallel_offset) cavc_add_test(TEST_cavc_combine_plines) cavc_add_test(TEST_staticspatialindex) +cavc_add_test(TEST_cavc_api_regression) +cavc_add_test(TEST_cavc_offset_islands) diff --git a/tests/tests/TEST_cavc_api_regression.cpp b/tests/tests/TEST_cavc_api_regression.cpp new file mode 100644 index 0000000..e1b450b --- /dev/null +++ b/tests/tests/TEST_cavc_api_regression.cpp @@ -0,0 +1,1546 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "c_api_include/cavaliercontours.h" +#include "c_api_test_helpers.hpp" + +namespace t = testing; + +namespace { +struct PlineDeleter { + void operator()(cavc_pline *pline) const { + if (pline != nullptr) { + cavc_pline_delete(pline); + } + } +}; + +struct PlineListDeleter { + void operator()(cavc_pline_list *list) const { + if (list != nullptr) { + cavc_pline_list_delete(list); + } + } +}; + +struct TopologyDeleter { + void operator()(cavc_offset_loop_topology *topology) const { + if (topology != nullptr) { + cavc_offset_loop_topology_delete(topology); + } + } +}; + +using PlinePtr = std::unique_ptr; +using PlineListPtr = std::unique_ptr; +using SpatialIndexPtr = std::unique_ptr; +using TopologyPtr = std::unique_ptr; + +std::vector readVertexes(cavc_pline const *pline) { + uint32_t count = cavc_pline_vertex_count(pline); + std::vector result(count); + if (count != 0) { + cavc_pline_vertex_data(pline, result.data()); + } + return result; +} + +cavc::Polyline toCppPolyline(cavc_pline const *pline) { + cavc::Polyline result; + result.isClosed() = cavc_pline_is_closed(pline) != 0; + + const std::vector vertexes = readVertexes(pline); + result.vertexes().reserve(vertexes.size()); + for (cavc_vertex const &v : vertexes) { + result.addVertex(v.x, v.y, v.bulge); + } + + return result; +} + +bool hasSelfIntersect(cavc_pline const *pline) { + const cavc::Polyline cpp_pline = toCppPolyline(pline); + if (cpp_pline.size() < 2) { + return false; + } + + auto spatial_index = cavc::createApproxSpatialIndex(cpp_pline); + std::vector> intersects; + cavc::allSelfIntersects(cpp_pline, intersects, spatial_index); + return !intersects.empty(); +} + +std::vector querySpatialIndexSorted(cavc_spatial_index const *spatial_index, + cavc_real min_x, cavc_real min_y, cavc_real max_x, + cavc_real max_y) { + uint32_t count = cavc_spatial_index_query_count(spatial_index, min_x, min_y, max_x, max_y); + std::vector results(count); + if (count != 0) { + cavc_spatial_index_query(spatial_index, min_x, min_y, max_x, max_y, results.data()); + std::sort(results.begin(), results.end()); + } + return results; +} + +bool vertexListAllFinite(std::vector const &vertexes) { + return std::all_of(vertexes.begin(), vertexes.end(), [](cavc_vertex const &v) { + return std::isfinite(v.x) && std::isfinite(v.y) && std::isfinite(v.bulge); + }); +} + +bool closedVertexListsFuzzyEqual(std::vector const &left, + std::vector const &right) { + if (left.size() != right.size()) { + return false; + } + if (left.empty()) { + return true; + } + + std::size_t const vertex_count = left.size(); + std::size_t start_index = 0; + for (; start_index < vertex_count; ++start_index) { + if (vertexesFuzzyEqual(left[0], right[start_index])) { + break; + } + } + if (start_index == vertex_count) { + return false; + } + + std::size_t index = start_index; + for (std::size_t i = 0; i < vertex_count; ++i) { + if (!vertexesFuzzyEqual(left[i], right[index])) { + return false; + } + index = nextWrappingIndex(right, index); + } + return true; +} + +std::vector makeAxisAlignedRectLoopVertexes(cavc_real min_x, cavc_real min_y, + cavc_real max_x, cavc_real max_y, + bool clockwise) { + std::vector result = { + {min_x, min_y, 0.0}, {max_x, min_y, 0.0}, {max_x, max_y, 0.0}, {min_x, max_y, 0.0}}; + if (clockwise) { + reverseDirection(result); + } + return result; +} + +std::vector makeOffsetCase1Vertexes() { + return { + {0.0, 25.0, 1.0}, + {0.0, 0.0, 0.0}, + {2.0, 0.0, 1.0}, + {10.0, 0.0, -0.5}, + {8.0, 9.0, 0.374794619217547}, + {21.0, 0.0, 0.0}, + {23.0, 0.0, 1.0}, + {32.0, 0.0, -0.5}, + {28.0, 0.0, 0.5}, + {39.0, 21.0, 0.0}, + {28.0, 12.0, 0.5}, + }; +} + +uint32_t findTopologyNodeIndex(cavc_offset_loop_topology const *topology, + cavc_offset_loop_role role, uint32_t source_index) { + uint32_t node_count = cavc_offset_loop_topology_count(topology); + for (uint32_t i = 0; i < node_count; ++i) { + cavc_offset_loop_topology_node node = cavc_offset_loop_topology_get(topology, i); + if (node.role == role && node.source_index == source_index) { + return i; + } + } + return CAVC_OFFSET_LOOP_NO_PARENT; +} +} // namespace + +TEST(CApiRegression, SetIsClosedRoundTripDoesNotChangeVertices) { + std::vector vertexes = {{0.0, 0.0, 0.0}, {2.0, 0.0, 0.0}, {2.0, 1.0, 0.0}}; + PlinePtr pline(plineFromVertexes(vertexes, false)); + + EXPECT_EQ(cavc_pline_is_closed(pline.get()), 0); + cavc_pline_set_is_closed(pline.get(), 1); + EXPECT_EQ(cavc_pline_is_closed(pline.get()), 1); + cavc_pline_set_is_closed(pline.get(), 0); + EXPECT_EQ(cavc_pline_is_closed(pline.get()), 0); + + std::vector actual = readVertexes(pline.get()); + EXPECT_THAT(actual, t::Pointwise(VertexEqual(), vertexes)); +} + +TEST(CApiRegression, NewWithNullVertexDataReservesCapacityAndStartsEmpty) { + PlinePtr pline(cavc_pline_new(nullptr, 6, 1)); + + ASSERT_NE(pline.get(), nullptr); + EXPECT_EQ(cavc_pline_is_closed(pline.get()), 1); + EXPECT_EQ(cavc_pline_vertex_count(pline.get()), 0u); + EXPECT_GE(cavc_pline_capacity(pline.get()), 6u); + + cavc_pline_add_vertex(pline.get(), cavc_vertex{0.0, 0.0, 0.0}); + cavc_pline_add_vertex(pline.get(), cavc_vertex{2.0, 0.0, 0.0}); + EXPECT_EQ(cavc_pline_vertex_count(pline.get()), 2u); +} + +TEST(CApiRegression, ClearLeavesCapacityAndAllowsReuse) { + std::vector vertexes = {{0.0, 0.0, 0.0}, {2.0, 0.0, 0.0}, {2.0, 1.0, 0.0}}; + PlinePtr pline(plineFromVertexes(vertexes, false)); + + cavc_pline_set_capacity(pline.get(), 16); + const uint32_t capacity_before_clear = cavc_pline_capacity(pline.get()); + ASSERT_GE(capacity_before_clear, 16u); + + cavc_pline_clear(pline.get()); + EXPECT_EQ(cavc_pline_vertex_count(pline.get()), 0u); + EXPECT_EQ(cavc_pline_capacity(pline.get()), capacity_before_clear); + + cavc_pline_add_vertex(pline.get(), cavc_vertex{5.0, 6.0, 0.0}); + cavc_pline_add_vertex(pline.get(), cavc_vertex{7.0, 8.0, 0.0}); + std::vector expected = {{5.0, 6.0, 0.0}, {7.0, 8.0, 0.0}}; + std::vector actual = readVertexes(pline.get()); + EXPECT_THAT(actual, t::Pointwise(VertexEqual(), expected)); +} + +TEST(CApiRegression, RemoveRangeWithZeroCountIsNoOp) { + std::vector vertexes = {{0.0, 0.0, 0.0}, {2.0, 0.0, 0.0}, {2.0, 1.0, 0.0}}; + PlinePtr pline(plineFromVertexes(vertexes, false)); + + cavc_pline_remove_range(pline.get(), 1, 0); + std::vector actual = readVertexes(pline.get()); + EXPECT_THAT(actual, t::Pointwise(VertexEqual(), vertexes)); +} + +TEST(CApiRegression, SetVertexDataNullClearsAndReserves) { + std::vector vertexes = {{1.0, 2.0, 0.1}, {3.0, 4.0, 0.2}, {5.0, 6.0, 0.3}}; + PlinePtr pline(plineFromVertexes(vertexes, false)); + + cavc_pline_set_vertex_data(pline.get(), nullptr, 8); + EXPECT_EQ(cavc_pline_vertex_count(pline.get()), 0u); + EXPECT_GE(cavc_pline_capacity(pline.get()), 8u); + EXPECT_EQ(cavc_pline_is_closed(pline.get()), 0); + + std::vector sentinel(4, cavc_vertex{-1.0, -1.0, -1.0}); + std::vector expected = sentinel; + cavc_pline_vertex_data(pline.get(), sentinel.data()); + EXPECT_THAT(sentinel, t::Pointwise(VertexEqual(), expected)); +} + +TEST(CApiRegression, PlineListReleaseTransfersOwnership) { + std::vector square = { + {0.0, 0.0, 0.0}, {10.0, 0.0, 0.0}, {10.0, 5.0, 0.0}, {0.0, 5.0, 0.0}}; + PlinePtr pline(plineFromVertexes(square, true)); + + cavc_pline_list *raw_results = nullptr; + cavc_parallel_offset(pline.get(), -1.0, &raw_results, defaultParallelOffsetOptions()); + PlineListPtr results(raw_results); + + ASSERT_NE(results.get(), nullptr); + ASSERT_EQ(cavc_pline_list_count(results.get()), 1u); + + cavc_pline *released_raw = cavc_pline_list_release(results.get(), 0); + PlinePtr released(released_raw); + EXPECT_NE(released.get(), nullptr); + EXPECT_EQ(cavc_pline_list_count(results.get()), 0u); + EXPECT_GT(cavc_pline_vertex_count(released.get()), 0u); + + cavc_pline_list_delete(results.release()); + EXPECT_GT(cavc_pline_vertex_count(released.get()), 0u); + EXPECT_GT(cavc_get_path_length(released.get()), 0.0); +} + +TEST(CApiRegression, ParallelOffsetDefaultOptionsUseRoundJoinAndRoundEndCap) { + cavc_parallel_offset_options options = cavc_parallel_offset_default_options(); + EXPECT_EQ(options.may_have_self_intersects, 0); + EXPECT_EQ(options.join_type, CAVC_OFFSET_JOIN_ROUND); + EXPECT_EQ(options.end_cap_type, CAVC_OFFSET_END_CAP_ROUND); + EXPECT_EQ(options.miter_limit, 4.0); +} + +TEST(CApiRegression, ParallelOffsetStructuredOptionsCanEnableSelfIntersectHint) { + std::vector square = { + {0.0, 0.0, 0.0}, {10.0, 0.0, 0.0}, {10.0, 5.0, 0.0}, {0.0, 5.0, 0.0}}; + PlinePtr pline(plineFromVertexes(square, true)); + + cavc_parallel_offset_options options = cavc_parallel_offset_default_options(); + options.may_have_self_intersects = 1; + + cavc_pline_list *raw_results = nullptr; + cavc_parallel_offset(pline.get(), -1.0, &raw_results, options); + PlineListPtr results(raw_results); + + ASSERT_NE(results.get(), nullptr); + ASSERT_EQ(cavc_pline_list_count(results.get()), 1u); + EXPECT_GT(cavc_get_area(cavc_pline_list_get(results.get(), 0)), 0.0); +} + +TEST(CApiRegression, ParallelOffsetMiterJoinForLinePolyline) { + std::vector square = { + {0.0, 0.0, 0.0}, {20.0, 0.0, 0.0}, {20.0, 10.0, 0.0}, {0.0, 10.0, 0.0}}; + PlinePtr pline(plineFromVertexes(square, true)); + + cavc_parallel_offset_options options = cavc_parallel_offset_default_options(); + options.join_type = CAVC_OFFSET_JOIN_MITER; + + cavc_pline_list *raw_results = nullptr; + cavc_parallel_offset(pline.get(), -2.0, &raw_results, options); + PlineListPtr results(raw_results); + + ASSERT_NE(results.get(), nullptr); + ASSERT_EQ(cavc_pline_list_count(results.get()), 1u); + cavc_pline *offset = cavc_pline_list_get(results.get(), 0); + + EXPECT_EQ(cavc_pline_vertex_count(offset), 4u); + std::vector vertexes = readVertexes(offset); + EXPECT_TRUE(std::all_of(vertexes.begin(), vertexes.end(), + [](cavc_vertex const &v) { return v.bulge == 0.0; })); + + EXPECT_NEAR(cavc_get_area(offset), 336.0, 1e-9); + cavc_real min_x = 0.0; + cavc_real min_y = 0.0; + cavc_real max_x = 0.0; + cavc_real max_y = 0.0; + cavc_get_extents(offset, &min_x, &min_y, &max_x, &max_y); + EXPECT_NEAR(min_x, -2.0, 1e-9); + EXPECT_NEAR(min_y, -2.0, 1e-9); + EXPECT_NEAR(max_x, 22.0, 1e-9); + EXPECT_NEAR(max_y, 12.0, 1e-9); +} + +TEST(CApiRegression, ParallelOffsetMiterLimitCanForceBevelEquivalentResult) { + std::vector square = { + {0.0, 0.0, 0.0}, {20.0, 0.0, 0.0}, {20.0, 10.0, 0.0}, {0.0, 10.0, 0.0}}; + PlinePtr pline(plineFromVertexes(square, true)); + + cavc_parallel_offset_options miter_options = cavc_parallel_offset_default_options(); + miter_options.join_type = CAVC_OFFSET_JOIN_MITER; + miter_options.miter_limit = 1.0; + + cavc_parallel_offset_options bevel_options = cavc_parallel_offset_default_options(); + bevel_options.join_type = CAVC_OFFSET_JOIN_BEVEL; + + cavc_pline_list *raw_miter = nullptr; + cavc_parallel_offset(pline.get(), -2.0, &raw_miter, miter_options); + PlineListPtr miter_results(raw_miter); + + cavc_pline_list *raw_bevel = nullptr; + cavc_parallel_offset(pline.get(), -2.0, &raw_bevel, bevel_options); + PlineListPtr bevel_results(raw_bevel); + + ASSERT_EQ(cavc_pline_list_count(miter_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(bevel_results.get()), 1u); + + cavc_pline *miter_pline = cavc_pline_list_get(miter_results.get(), 0); + cavc_pline *bevel_pline = cavc_pline_list_get(bevel_results.get(), 0); + + EXPECT_EQ(cavc_pline_vertex_count(miter_pline), 8u); + EXPECT_EQ(cavc_pline_vertex_count(bevel_pline), 8u); + + std::vector miter_vertexes = readVertexes(miter_pline); + std::vector bevel_vertexes = readVertexes(bevel_pline); + EXPECT_THAT(miter_vertexes, t::Pointwise(VertexEqual(), bevel_vertexes)); + EXPECT_NEAR(cavc_get_area(miter_pline), 328.0, 1e-9); +} + +TEST(CApiRegression, ParallelOffsetBevelJoinForLinePolyline) { + std::vector square = { + {0.0, 0.0, 0.0}, {20.0, 0.0, 0.0}, {20.0, 10.0, 0.0}, {0.0, 10.0, 0.0}}; + PlinePtr pline(plineFromVertexes(square, true)); + + cavc_parallel_offset_options options = cavc_parallel_offset_default_options(); + options.join_type = CAVC_OFFSET_JOIN_BEVEL; + + cavc_pline_list *raw_results = nullptr; + cavc_parallel_offset(pline.get(), -2.0, &raw_results, options); + PlineListPtr results(raw_results); + + ASSERT_NE(results.get(), nullptr); + ASSERT_EQ(cavc_pline_list_count(results.get()), 1u); + cavc_pline *offset = cavc_pline_list_get(results.get(), 0); + + EXPECT_EQ(cavc_pline_vertex_count(offset), 8u); + std::vector vertexes = readVertexes(offset); + EXPECT_TRUE(std::all_of(vertexes.begin(), vertexes.end(), + [](cavc_vertex const &v) { return v.bulge == 0.0; })); + + EXPECT_NEAR(cavc_get_area(offset), 328.0, 1e-9); + cavc_real min_x = 0.0; + cavc_real min_y = 0.0; + cavc_real max_x = 0.0; + cavc_real max_y = 0.0; + cavc_get_extents(offset, &min_x, &min_y, &max_x, &max_y); + EXPECT_NEAR(min_x, -2.0, 1e-9); + EXPECT_NEAR(min_y, -2.0, 1e-9); + EXPECT_NEAR(max_x, 22.0, 1e-9); + EXPECT_NEAR(max_y, 12.0, 1e-9); +} + +TEST(CApiRegression, ParallelOffsetMiterJoinSupportsArcInvolvedJoins) { + std::vector arc_join_shape = { + {0.0, 0.0, 0.0}, {4.0, 0.0, 0.5}, {8.0, 0.0, 0.0}, {8.0, 4.0, 0.0}, {0.0, 4.0, 0.0}}; + PlinePtr pline(plineFromVertexes(arc_join_shape, true)); + + cavc_parallel_offset_options options = cavc_parallel_offset_default_options(); + options.join_type = CAVC_OFFSET_JOIN_MITER; + options.miter_limit = 8.0; + + cavc_pline_list *raw_results = nullptr; + cavc_parallel_offset(pline.get(), -0.5, &raw_results, options); + PlineListPtr results(raw_results); + + ASSERT_NE(results.get(), nullptr); + ASSERT_EQ(cavc_pline_list_count(results.get()), 1u); + cavc_pline *offset = cavc_pline_list_get(results.get(), 0); + + std::vector vertexes = readVertexes(offset); + ASSERT_FALSE(vertexes.empty()); + + bool has_arc_segment = false; + for (cavc_vertex const &v : vertexes) { + EXPECT_TRUE(std::isfinite(v.x)); + EXPECT_TRUE(std::isfinite(v.y)); + EXPECT_TRUE(std::isfinite(v.bulge)); + has_arc_segment = has_arc_segment || std::abs(v.bulge) > 1e-12; + } + EXPECT_TRUE(has_arc_segment); +} + +TEST(CApiRegression, ParallelOffsetArcInvolvedMiterLimitCanForceBevelEquivalentResult) { + std::vector arc_join_shape = { + {0.0, 0.0, 0.0}, {4.0, 0.0, 0.5}, {8.0, 0.0, 0.0}, {8.0, 4.0, 0.0}, {0.0, 4.0, 0.0}}; + PlinePtr pline(plineFromVertexes(arc_join_shape, true)); + + cavc_parallel_offset_options miter_options = cavc_parallel_offset_default_options(); + miter_options.join_type = CAVC_OFFSET_JOIN_MITER; + miter_options.miter_limit = 1.0; + + cavc_parallel_offset_options bevel_options = cavc_parallel_offset_default_options(); + bevel_options.join_type = CAVC_OFFSET_JOIN_BEVEL; + + cavc_pline_list *raw_miter = nullptr; + cavc_parallel_offset(pline.get(), -0.5, &raw_miter, miter_options); + PlineListPtr miter_results(raw_miter); + + cavc_pline_list *raw_bevel = nullptr; + cavc_parallel_offset(pline.get(), -0.5, &raw_bevel, bevel_options); + PlineListPtr bevel_results(raw_bevel); + + ASSERT_EQ(cavc_pline_list_count(miter_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(bevel_results.get()), 1u); + + std::vector miter_vertexes = + readVertexes(cavc_pline_list_get(miter_results.get(), 0)); + std::vector bevel_vertexes = + readVertexes(cavc_pline_list_get(bevel_results.get(), 0)); + ASSERT_FALSE(miter_vertexes.empty()); + ASSERT_EQ(miter_vertexes.size(), bevel_vertexes.size()); + + std::vector> miter_vertex_sets = {miter_vertexes}; + std::vector> bevel_vertex_sets = {bevel_vertexes}; + EXPECT_THAT(miter_vertex_sets, t::Pointwise(VertexListsFuzzyEqual(true), bevel_vertex_sets)); +} + +TEST(CApiRegression, ParallelOffsetJoinArcHeavyStressMatrix) { + struct JoinMatrixCase { + const char *name; + std::vector vertexes; + uint32_t min_bevel_extra_positive; + uint32_t min_bevel_extra_negative; + }; + + std::vector cases; + cases.push_back(JoinMatrixCase{ + "high_curvature_arc_heavy", + { + {1.585, -0.404, 0.419}, + {1.856, -1.235, 0.125}, + {2.795, -2.210, 0.130}, + {4.185, -2.751, 0.773}, + {4.559, -1.233, 0.000}, + {5.090, -0.316, -0.312}, + }, + 2, + 2, + }); + cases.push_back(JoinMatrixCase{ + "near_tangent_turns", + { + {0.277, -0.934, 0.000}, + {0.616, -0.239, 0.125}, + {1.104, 0.666, 0.000}, + {2.029, 2.174, 0.000}, + {3.090, 0.843, 0.409}, + {3.904, 2.167, 0.000}, + {3.944, 3.010, -0.517}, + }, + 2, + 1, + }); + cases.push_back(JoinMatrixCase{ + "degenerate_repeated_vertex", + { + {1.092, 0.310, -0.847}, + {1.698, -0.231, 0.102}, + {1.698, -0.231, -0.400}, + {2.288, -0.255, 0.000}, + {3.848, 0.494, 0.515}, + {4.606, 0.328, 0.000}, + {4.888, 1.349, 0.374}, + }, + 2, + 2, + }); + + auto run_case = [](JoinMatrixCase const &test_case, cavc_real delta, uint32_t min_bevel_extra) { + PlinePtr pline(plineFromVertexes(test_case.vertexes, true)); + + cavc_parallel_offset_options miter_high_options = cavc_parallel_offset_default_options(); + miter_high_options.join_type = CAVC_OFFSET_JOIN_MITER; + miter_high_options.miter_limit = 8.0; + + cavc_parallel_offset_options miter_low_options = cavc_parallel_offset_default_options(); + miter_low_options.join_type = CAVC_OFFSET_JOIN_MITER; + miter_low_options.miter_limit = 1.0; + + cavc_parallel_offset_options bevel_options = cavc_parallel_offset_default_options(); + bevel_options.join_type = CAVC_OFFSET_JOIN_BEVEL; + + cavc_parallel_offset_options round_options = cavc_parallel_offset_default_options(); + + cavc_pline_list *raw_miter_high = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_miter_high, miter_high_options); + PlineListPtr miter_high_results(raw_miter_high); + + cavc_pline_list *raw_miter_low = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_miter_low, miter_low_options); + PlineListPtr miter_low_results(raw_miter_low); + + cavc_pline_list *raw_bevel = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_bevel, bevel_options); + PlineListPtr bevel_results(raw_bevel); + + cavc_pline_list *raw_round = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_round, round_options); + PlineListPtr round_results(raw_round); + + ASSERT_EQ(cavc_pline_list_count(miter_high_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(miter_low_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(bevel_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(round_results.get()), 1u); + + cavc_pline *miter_high_pline = cavc_pline_list_get(miter_high_results.get(), 0); + cavc_pline *miter_low_pline = cavc_pline_list_get(miter_low_results.get(), 0); + cavc_pline *bevel_pline = cavc_pline_list_get(bevel_results.get(), 0); + cavc_pline *round_pline = cavc_pline_list_get(round_results.get(), 0); + + std::vector miter_high_vertexes = readVertexes(miter_high_pline); + std::vector miter_low_vertexes = readVertexes(miter_low_pline); + std::vector bevel_vertexes = readVertexes(bevel_pline); + std::vector round_vertexes = readVertexes(round_pline); + + ASSERT_FALSE(miter_high_vertexes.empty()); + ASSERT_FALSE(miter_low_vertexes.empty()); + ASSERT_FALSE(bevel_vertexes.empty()); + ASSERT_FALSE(round_vertexes.empty()); + + std::vector> miter_low_wrap = {miter_low_vertexes}; + std::vector> bevel_wrap = {bevel_vertexes}; + EXPECT_THAT(miter_low_wrap, t::Pointwise(VertexListsFuzzyEqual(true), bevel_wrap)); + + const bool high_equals_bevel = closedVertexListsFuzzyEqual(miter_high_vertexes, bevel_vertexes); + const bool high_equals_round = closedVertexListsFuzzyEqual(miter_high_vertexes, round_vertexes); + EXPECT_TRUE(high_equals_round || !high_equals_bevel); + if (!high_equals_bevel && !high_equals_round) { + EXPECT_GE(bevel_vertexes.size(), miter_high_vertexes.size() + min_bevel_extra); + } + + EXPECT_TRUE(vertexListAllFinite(miter_high_vertexes)); + EXPECT_TRUE(vertexListAllFinite(miter_low_vertexes)); + EXPECT_TRUE(vertexListAllFinite(bevel_vertexes)); + EXPECT_TRUE(vertexListAllFinite(round_vertexes)); + EXPECT_GT(cavc_get_path_length(miter_high_pline), 0.0); + EXPECT_GT(cavc_get_path_length(miter_low_pline), 0.0); + EXPECT_GT(cavc_get_path_length(bevel_pline), 0.0); + EXPECT_GT(cavc_get_path_length(round_pline), 0.0); + }; + + for (auto const &test_case : cases) { + SCOPED_TRACE(test_case.name); + run_case(test_case, 0.2, test_case.min_bevel_extra_positive); + run_case(test_case, -0.2, test_case.min_bevel_extra_negative); + } +} + +TEST(CApiRegression, ParallelOffsetOffsetCase1JoinModesKeepClosedOutputs) { + const std::vector offset_case1 = makeOffsetCase1Vertexes(); + + auto run_case = [&](cavc_offset_join_type join_type, cavc_real delta) { + PlinePtr pline(plineFromVertexes(offset_case1, true)); + cavc_parallel_offset_options options = cavc_parallel_offset_default_options(); + options.join_type = join_type; + + cavc_pline_list *raw_results = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_results, options); + PlineListPtr results(raw_results); + + ASSERT_NE(results.get(), nullptr); + ASSERT_GT(cavc_pline_list_count(results.get()), 0u); + + const uint32_t result_count = cavc_pline_list_count(results.get()); + for (uint32_t i = 0; i < result_count; ++i) { + cavc_pline *offset = cavc_pline_list_get(results.get(), i); + ASSERT_NE(offset, nullptr); + + EXPECT_EQ(cavc_pline_is_closed(offset), 1) + << "join=" << join_type << ", delta=" << delta << ", index=" << i; + EXPECT_GE(cavc_pline_vertex_count(offset), 3u); + EXPECT_GT(cavc_get_path_length(offset), 0.0); + + const std::vector vertexes = readVertexes(offset); + EXPECT_TRUE(vertexListAllFinite(vertexes)); + } + }; + + run_case(CAVC_OFFSET_JOIN_ROUND, 1.0); + run_case(CAVC_OFFSET_JOIN_ROUND, -1.0); + run_case(CAVC_OFFSET_JOIN_MITER, 1.0); + run_case(CAVC_OFFSET_JOIN_MITER, -1.0); + run_case(CAVC_OFFSET_JOIN_BEVEL, 1.0); + run_case(CAVC_OFFSET_JOIN_BEVEL, -1.0); +} + +TEST(CApiRegression, ParallelOffsetOffsetCase1OpenMiterRoundEndCapNegativeRegression) { + const std::vector offset_case1 = makeOffsetCase1Vertexes(); + PlinePtr pline(plineFromVertexes(offset_case1, false)); + + cavc_parallel_offset_options options = cavc_parallel_offset_default_options(); + options.join_type = CAVC_OFFSET_JOIN_MITER; + options.end_cap_type = CAVC_OFFSET_END_CAP_ROUND; + options.miter_limit = 4.0; + + cavc_pline_list *raw_results = nullptr; + cavc_parallel_offset(pline.get(), -1.55, &raw_results, options); + PlineListPtr results(raw_results); + + ASSERT_NE(results.get(), nullptr); + ASSERT_EQ(cavc_pline_list_count(results.get()), 1u); + + cavc_pline *offset = cavc_pline_list_get(results.get(), 0); + ASSERT_NE(offset, nullptr); + EXPECT_EQ(cavc_pline_is_closed(offset), 0); + EXPECT_GE(cavc_pline_vertex_count(offset), 3u); + EXPECT_GT(cavc_get_path_length(offset), 0.0); + + const std::vector vertexes = readVertexes(offset); + ASSERT_FALSE(vertexes.empty()); + EXPECT_TRUE(vertexListAllFinite(vertexes)); + + cavc_real min_x = 0.0; + cavc_real min_y = 0.0; + cavc_real max_x = 0.0; + cavc_real max_y = 0.0; + cavc_get_extents(offset, &min_x, &min_y, &max_x, &max_y); + EXPECT_TRUE(std::isfinite(min_x)); + EXPECT_TRUE(std::isfinite(min_y)); + EXPECT_TRUE(std::isfinite(max_x)); + EXPECT_TRUE(std::isfinite(max_y)); + EXPECT_LT(min_x, max_x); + EXPECT_LT(min_y, max_y); +} + +TEST(CApiRegression, ParallelOffsetOffsetCase1ClosedJoinMatrixProducesSimpleLoops) { + const std::vector offset_case1 = makeOffsetCase1Vertexes(); + + const std::array deltas = {0.2, 0.6, 1.0, -0.2, -0.6, -1.0}; + const std::array join_types = { + CAVC_OFFSET_JOIN_ROUND, CAVC_OFFSET_JOIN_MITER, CAVC_OFFSET_JOIN_BEVEL}; + + for (cavc_offset_join_type join_type : join_types) { + for (cavc_real delta : deltas) { + SCOPED_TRACE(::testing::Message() << "join=" << join_type << ", delta=" << delta); + + PlinePtr pline(plineFromVertexes(offset_case1, true)); + cavc_parallel_offset_options options = cavc_parallel_offset_default_options(); + options.join_type = join_type; + + cavc_pline_list *raw_results = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_results, options); + PlineListPtr results(raw_results); + + ASSERT_NE(results.get(), nullptr); + ASSERT_GT(cavc_pline_list_count(results.get()), 0u); + + const uint32_t result_count = cavc_pline_list_count(results.get()); + for (uint32_t i = 0; i < result_count; ++i) { + cavc_pline *offset = cavc_pline_list_get(results.get(), i); + ASSERT_NE(offset, nullptr); + EXPECT_EQ(cavc_pline_is_closed(offset), 1); + EXPECT_GE(cavc_pline_vertex_count(offset), 3u); + EXPECT_GT(cavc_get_path_length(offset), 0.0); + EXPECT_FALSE(hasSelfIntersect(offset)); + EXPECT_TRUE(vertexListAllFinite(readVertexes(offset))); + } + } + } +} + +TEST(CApiRegression, ParallelOffsetOffsetCase1OpenJoinEndCapMatrixProducesSimplePolylines) { + const std::vector offset_case1 = makeOffsetCase1Vertexes(); + + const std::array deltas = {0.2, 0.6, -0.6, -1.55}; + const std::array join_types = { + CAVC_OFFSET_JOIN_ROUND, CAVC_OFFSET_JOIN_MITER, CAVC_OFFSET_JOIN_BEVEL}; + const std::array end_cap_types = { + CAVC_OFFSET_END_CAP_ROUND, CAVC_OFFSET_END_CAP_SQUARE, CAVC_OFFSET_END_CAP_BUTT}; + + for (cavc_offset_join_type join_type : join_types) { + for (cavc_offset_end_cap_type end_cap_type : end_cap_types) { + for (cavc_real delta : deltas) { + SCOPED_TRACE(::testing::Message() + << "join=" << join_type << ", endcap=" << end_cap_type << ", delta=" << delta); + + PlinePtr pline(plineFromVertexes(offset_case1, false)); + cavc_parallel_offset_options options = cavc_parallel_offset_default_options(); + options.join_type = join_type; + options.end_cap_type = end_cap_type; + + cavc_pline_list *raw_results = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_results, options); + PlineListPtr results(raw_results); + + ASSERT_NE(results.get(), nullptr); + ASSERT_GT(cavc_pline_list_count(results.get()), 0u); + + const uint32_t result_count = cavc_pline_list_count(results.get()); + for (uint32_t i = 0; i < result_count; ++i) { + cavc_pline *offset = cavc_pline_list_get(results.get(), i); + ASSERT_NE(offset, nullptr); + EXPECT_EQ(cavc_pline_is_closed(offset), 0); + EXPECT_GE(cavc_pline_vertex_count(offset), 2u); + EXPECT_GT(cavc_get_path_length(offset), 0.0); + EXPECT_FALSE(hasSelfIntersect(offset)); + EXPECT_TRUE(vertexListAllFinite(readVertexes(offset))); + } + } + } + } +} + +TEST(CApiRegression, ParallelOffsetSquareEndCapExtendsOpenLineEndpoints) { + std::vector line = {{0.0, 0.0, 0.0}, {10.0, 0.0, 0.0}}; + PlinePtr pline(plineFromVertexes(line, false)); + + cavc_parallel_offset_options options = cavc_parallel_offset_default_options(); + options.end_cap_type = CAVC_OFFSET_END_CAP_SQUARE; + + cavc_pline_list *raw_results = nullptr; + cavc_parallel_offset(pline.get(), -1.0, &raw_results, options); + PlineListPtr results(raw_results); + + ASSERT_NE(results.get(), nullptr); + ASSERT_EQ(cavc_pline_list_count(results.get()), 1u); + cavc_pline *offset = cavc_pline_list_get(results.get(), 0); + + EXPECT_EQ(cavc_pline_vertex_count(offset), 2u); + std::vector vertexes = readVertexes(offset); + ASSERT_EQ(vertexes.size(), 2u); + EXPECT_NEAR(vertexes[0].x, -1.0, 1e-9); + EXPECT_NEAR(vertexes[0].y, -1.0, 1e-9); + EXPECT_NEAR(vertexes[0].bulge, 0.0, 1e-12); + EXPECT_NEAR(vertexes[1].x, 11.0, 1e-9); + EXPECT_NEAR(vertexes[1].y, -1.0, 1e-9); + EXPECT_NEAR(vertexes[1].bulge, 0.0, 1e-12); + + EXPECT_NEAR(cavc_get_path_length(offset), 12.0, 1e-9); +} + +TEST(CApiRegression, ParallelOffsetSquareEndCapSupportsArcStartSegment) { + std::vector arc_start_open = {{0.0, 0.0, 1.0}, {2.0, 0.0, 0.0}, {4.0, 0.0, 0.0}}; + PlinePtr pline(plineFromVertexes(arc_start_open, false)); + + cavc_parallel_offset_options round_options = cavc_parallel_offset_default_options(); + cavc_parallel_offset_options square_options = cavc_parallel_offset_default_options(); + square_options.end_cap_type = CAVC_OFFSET_END_CAP_SQUARE; + + cavc_pline_list *raw_round = nullptr; + cavc_parallel_offset(pline.get(), -0.25, &raw_round, round_options); + PlineListPtr round_results(raw_round); + + cavc_pline_list *raw_square = nullptr; + cavc_parallel_offset(pline.get(), -0.25, &raw_square, square_options); + PlineListPtr square_results(raw_square); + + ASSERT_EQ(cavc_pline_list_count(round_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(square_results.get()), 1u); + + cavc_pline *round_offset = cavc_pline_list_get(round_results.get(), 0); + cavc_pline *square_offset = cavc_pline_list_get(square_results.get(), 0); + std::vector round_vertexes = readVertexes(round_offset); + std::vector square_vertexes = readVertexes(square_offset); + ASSERT_FALSE(round_vertexes.empty()); + ASSERT_FALSE(square_vertexes.empty()); + + auto endpoint_delta = [](cavc_vertex const &a, cavc_vertex const &b) { + return std::hypot(a.x - b.x, a.y - b.y); + }; + + EXPECT_NEAR(endpoint_delta(square_vertexes.front(), round_vertexes.front()), 0.25, 1e-9); + EXPECT_NEAR(endpoint_delta(square_vertexes.back(), round_vertexes.back()), 0.25, 1e-9); + EXPECT_NEAR(cavc_get_path_length(square_offset) - cavc_get_path_length(round_offset), 0.5, 1e-8); +} + +TEST(CApiRegression, ParallelOffsetSquareEndCapSupportsArcStartSegmentPositiveDelta) { + std::vector arc_start_open = {{0.0, 0.0, 1.0}, {2.0, 0.0, 0.0}, {4.0, 0.0, 0.0}}; + PlinePtr pline(plineFromVertexes(arc_start_open, false)); + + cavc_parallel_offset_options round_options = cavc_parallel_offset_default_options(); + cavc_parallel_offset_options square_options = cavc_parallel_offset_default_options(); + square_options.end_cap_type = CAVC_OFFSET_END_CAP_SQUARE; + + cavc_pline_list *raw_round = nullptr; + cavc_parallel_offset(pline.get(), 0.25, &raw_round, round_options); + PlineListPtr round_results(raw_round); + + cavc_pline_list *raw_square = nullptr; + cavc_parallel_offset(pline.get(), 0.25, &raw_square, square_options); + PlineListPtr square_results(raw_square); + + ASSERT_EQ(cavc_pline_list_count(round_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(square_results.get()), 1u); + + cavc_pline *round_offset = cavc_pline_list_get(round_results.get(), 0); + cavc_pline *square_offset = cavc_pline_list_get(square_results.get(), 0); + std::vector round_vertexes = readVertexes(round_offset); + std::vector square_vertexes = readVertexes(square_offset); + ASSERT_FALSE(round_vertexes.empty()); + ASSERT_FALSE(square_vertexes.empty()); + + auto endpoint_delta = [](cavc_vertex const &a, cavc_vertex const &b) { + return std::hypot(a.x - b.x, a.y - b.y); + }; + + EXPECT_NEAR(endpoint_delta(square_vertexes.front(), round_vertexes.front()), 0.25, 1e-9); + EXPECT_NEAR(endpoint_delta(square_vertexes.back(), round_vertexes.back()), 0.25, 1e-9); + EXPECT_NEAR(cavc_get_path_length(square_offset) - cavc_get_path_length(round_offset), 0.5, 1e-8); +} + +TEST(CApiRegression, ParallelOffsetSquareEndCapSupportsArcEndSegment) { + std::vector arc_end_open = {{0.0, 0.0, 0.0}, {2.0, 0.0, 1.0}, {4.0, 0.0, 0.0}}; + PlinePtr pline(plineFromVertexes(arc_end_open, false)); + + cavc_parallel_offset_options round_options = cavc_parallel_offset_default_options(); + cavc_parallel_offset_options square_options = cavc_parallel_offset_default_options(); + square_options.end_cap_type = CAVC_OFFSET_END_CAP_SQUARE; + + cavc_pline_list *raw_round = nullptr; + cavc_parallel_offset(pline.get(), -0.25, &raw_round, round_options); + PlineListPtr round_results(raw_round); + + cavc_pline_list *raw_square = nullptr; + cavc_parallel_offset(pline.get(), -0.25, &raw_square, square_options); + PlineListPtr square_results(raw_square); + + ASSERT_EQ(cavc_pline_list_count(round_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(square_results.get()), 1u); + + cavc_pline *round_offset = cavc_pline_list_get(round_results.get(), 0); + cavc_pline *square_offset = cavc_pline_list_get(square_results.get(), 0); + std::vector round_vertexes = readVertexes(round_offset); + std::vector square_vertexes = readVertexes(square_offset); + ASSERT_FALSE(round_vertexes.empty()); + ASSERT_FALSE(square_vertexes.empty()); + + auto endpoint_delta = [](cavc_vertex const &a, cavc_vertex const &b) { + return std::hypot(a.x - b.x, a.y - b.y); + }; + + EXPECT_NEAR(endpoint_delta(square_vertexes.front(), round_vertexes.front()), 0.25, 1e-9); + EXPECT_NEAR(endpoint_delta(square_vertexes.back(), round_vertexes.back()), 0.25, 1e-9); + EXPECT_NEAR(cavc_get_path_length(square_offset) - cavc_get_path_length(round_offset), 0.5, 1e-8); +} + +TEST(CApiRegression, ParallelOffsetSquareEndCapSupportsArcEndSegmentPositiveDelta) { + std::vector arc_end_open = {{0.0, 0.0, 0.0}, {2.0, 0.0, 1.0}, {4.0, 0.0, 0.0}}; + PlinePtr pline(plineFromVertexes(arc_end_open, false)); + + cavc_parallel_offset_options round_options = cavc_parallel_offset_default_options(); + cavc_parallel_offset_options square_options = cavc_parallel_offset_default_options(); + square_options.end_cap_type = CAVC_OFFSET_END_CAP_SQUARE; + + cavc_pline_list *raw_round = nullptr; + cavc_parallel_offset(pline.get(), 0.25, &raw_round, round_options); + PlineListPtr round_results(raw_round); + + cavc_pline_list *raw_square = nullptr; + cavc_parallel_offset(pline.get(), 0.25, &raw_square, square_options); + PlineListPtr square_results(raw_square); + + ASSERT_EQ(cavc_pline_list_count(round_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(square_results.get()), 1u); + + cavc_pline *round_offset = cavc_pline_list_get(round_results.get(), 0); + cavc_pline *square_offset = cavc_pline_list_get(square_results.get(), 0); + std::vector round_vertexes = readVertexes(round_offset); + std::vector square_vertexes = readVertexes(square_offset); + ASSERT_FALSE(round_vertexes.empty()); + ASSERT_FALSE(square_vertexes.empty()); + + auto endpoint_delta = [](cavc_vertex const &a, cavc_vertex const &b) { + return std::hypot(a.x - b.x, a.y - b.y); + }; + + EXPECT_NEAR(endpoint_delta(square_vertexes.front(), round_vertexes.front()), 0.25, 1e-9); + EXPECT_NEAR(endpoint_delta(square_vertexes.back(), round_vertexes.back()), 0.25, 1e-9); + EXPECT_NEAR(cavc_get_path_length(square_offset) - cavc_get_path_length(round_offset), 0.5, 1e-8); +} + +TEST(CApiRegression, ParallelOffsetButtEndCapMatchesRoundOnSimpleLine) { + std::vector line = {{0.0, 0.0, 0.0}, {10.0, 0.0, 0.0}}; + PlinePtr pline(plineFromVertexes(line, false)); + + cavc_parallel_offset_options round_options = cavc_parallel_offset_default_options(); + cavc_parallel_offset_options butt_options = cavc_parallel_offset_default_options(); + butt_options.end_cap_type = CAVC_OFFSET_END_CAP_BUTT; + + cavc_pline_list *raw_round = nullptr; + cavc_parallel_offset(pline.get(), -1.0, &raw_round, round_options); + PlineListPtr round_results(raw_round); + + cavc_pline_list *raw_butt = nullptr; + cavc_parallel_offset(pline.get(), -1.0, &raw_butt, butt_options); + PlineListPtr butt_results(raw_butt); + + ASSERT_EQ(cavc_pline_list_count(round_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(butt_results.get()), 1u); + + std::vector round_vertexes = + readVertexes(cavc_pline_list_get(round_results.get(), 0)); + std::vector butt_vertexes = readVertexes(cavc_pline_list_get(butt_results.get(), 0)); + ASSERT_EQ(round_vertexes.size(), butt_vertexes.size()); + EXPECT_THAT(round_vertexes, t::Pointwise(VertexEqual(), butt_vertexes)); +} + +TEST(CApiRegression, ParallelOffsetButtEndCapCanDifferFromRoundOnMixedLineArcPolyline) { + std::vector mixed_open = { + {0.0, 0.0, 0.0}, {1.0, 0.0, 0.0}, {2.0, 0.0, 0.5}, {2.0, 1.0, 0.0}}; + PlinePtr pline(plineFromVertexes(mixed_open, false)); + + cavc_parallel_offset_options round_options = cavc_parallel_offset_default_options(); + cavc_parallel_offset_options butt_options = cavc_parallel_offset_default_options(); + butt_options.end_cap_type = CAVC_OFFSET_END_CAP_BUTT; + + auto run_offset = [&](cavc_real delta) { + cavc_pline_list *raw_round = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_round, round_options); + PlineListPtr round_results(raw_round); + + cavc_pline_list *raw_butt = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_butt, butt_options); + PlineListPtr butt_results(raw_butt); + + ASSERT_EQ(cavc_pline_list_count(round_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(butt_results.get()), 1u); + + std::vector round_vertexes = + readVertexes(cavc_pline_list_get(round_results.get(), 0)); + std::vector butt_vertexes = + readVertexes(cavc_pline_list_get(butt_results.get(), 0)); + ASSERT_FALSE(round_vertexes.empty()); + ASSERT_FALSE(butt_vertexes.empty()); + + std::vector> round_wrap = {round_vertexes}; + std::vector> butt_wrap = {butt_vertexes}; + EXPECT_THAT(round_wrap, t::Not(t::Pointwise(VertexListsFuzzyEqual(false), butt_wrap))); + EXPECT_EQ(butt_vertexes.size(), round_vertexes.size() + 1); + }; + + run_offset(0.2); + run_offset(-0.2); +} + +TEST(CApiRegression, ParallelOffsetButtEndCapCanDifferFromRoundWhenArcStartsAtEndpoint) { + std::vector arc_start_open = { + {0.0, 0.0, 0.5}, {2.0, 0.0, 0.0}, {2.0, 1.0, 0.0}, {3.0, 1.0, 0.0}}; + PlinePtr pline(plineFromVertexes(arc_start_open, false)); + + cavc_parallel_offset_options round_options = cavc_parallel_offset_default_options(); + cavc_parallel_offset_options butt_options = cavc_parallel_offset_default_options(); + butt_options.end_cap_type = CAVC_OFFSET_END_CAP_BUTT; + + cavc_pline_list *raw_round = nullptr; + cavc_parallel_offset(pline.get(), 0.5, &raw_round, round_options); + PlineListPtr round_results(raw_round); + + cavc_pline_list *raw_butt = nullptr; + cavc_parallel_offset(pline.get(), 0.5, &raw_butt, butt_options); + PlineListPtr butt_results(raw_butt); + + ASSERT_EQ(cavc_pline_list_count(round_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(butt_results.get()), 1u); + + std::vector round_vertexes = + readVertexes(cavc_pline_list_get(round_results.get(), 0)); + std::vector butt_vertexes = readVertexes(cavc_pline_list_get(butt_results.get(), 0)); + ASSERT_FALSE(round_vertexes.empty()); + ASSERT_FALSE(butt_vertexes.empty()); + + std::vector> round_wrap = {round_vertexes}; + std::vector> butt_wrap = {butt_vertexes}; + EXPECT_THAT(round_wrap, t::Not(t::Pointwise(VertexListsFuzzyEqual(false), butt_wrap))); + EXPECT_EQ(butt_vertexes.size(), round_vertexes.size() + 1); +} + +TEST(CApiRegression, ParallelOffsetButtEndCapStressMatrix) { + struct ButtMatrixCase { + const char *name; + std::vector vertexes; + uint32_t min_extra_positive; + uint32_t min_extra_negative; + }; + + std::vector cases; + cases.push_back(ButtMatrixCase{ + "high_curvature", + { + {0.000, 0.000, -1.000}, + {1.726, -1.174, 0.000}, + {3.078, -0.618, 0.000}, + {3.734, -1.393, 0.050}, + {4.851, -0.804, 0.900}, + {6.329, 0.244, 0.500}, + {6.779, 1.285, 0.000}, + }, + 1, + 1, + }); + cases.push_back(ButtMatrixCase{ + "near_tangent", + { + {0.000, 0.000, -0.700}, + {0.443, 0.378, 0.080}, + {1.813, 0.591, -0.500}, + {2.839, 0.227, 0.000}, + }, + 1, + 1, + }); + cases.push_back(ButtMatrixCase{ + "multi_intersection_open", + { + {0.000, 0.000, 0.080}, + {0.714, -0.628, -0.700}, + {1.378, 0.844, -0.700}, + {3.126, -0.527, 0.000}, + {4.407, -0.858, 0.300}, + {5.060, -1.594, 0.000}, + {5.883, -0.695, -0.300}, + {6.388, -1.812, 0.000}, + }, + 2, + 2, + }); + + auto run_case = [](ButtMatrixCase const &test_case, cavc_real delta, uint32_t min_extra_count) { + PlinePtr pline(plineFromVertexes(test_case.vertexes, false)); + + cavc_parallel_offset_options round_options = cavc_parallel_offset_default_options(); + cavc_parallel_offset_options butt_options = cavc_parallel_offset_default_options(); + butt_options.end_cap_type = CAVC_OFFSET_END_CAP_BUTT; + + cavc_pline_list *raw_round = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_round, round_options); + PlineListPtr round_results(raw_round); + + cavc_pline_list *raw_butt = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_butt, butt_options); + PlineListPtr butt_results(raw_butt); + + ASSERT_EQ(cavc_pline_list_count(round_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(butt_results.get()), 1u); + + cavc_pline *round_offset = cavc_pline_list_get(round_results.get(), 0); + cavc_pline *butt_offset = cavc_pline_list_get(butt_results.get(), 0); + std::vector round_vertexes = readVertexes(round_offset); + std::vector butt_vertexes = readVertexes(butt_offset); + ASSERT_FALSE(round_vertexes.empty()); + ASSERT_FALSE(butt_vertexes.empty()); + + std::vector> round_wrap = {round_vertexes}; + std::vector> butt_wrap = {butt_vertexes}; + EXPECT_THAT(round_wrap, t::Not(t::Pointwise(VertexListsFuzzyEqual(false), butt_wrap))); + EXPECT_GE(butt_vertexes.size(), round_vertexes.size() + min_extra_count); + EXPECT_TRUE(vertexListAllFinite(round_vertexes)); + EXPECT_TRUE(vertexListAllFinite(butt_vertexes)); + EXPECT_GT(cavc_get_path_length(round_offset), 0.0); + EXPECT_GT(cavc_get_path_length(butt_offset), 0.0); + }; + + for (auto const &test_case : cases) { + SCOPED_TRACE(test_case.name); + run_case(test_case, 0.2, test_case.min_extra_positive); + run_case(test_case, -0.2, test_case.min_extra_negative); + } +} + +TEST(CApiRegression, ParallelOffsetEndCapDegenerateInputStressMatrix) { + struct EndCapMatrixCase { + const char *name; + std::vector vertexes; + uint32_t min_square_extra_positive; + uint32_t min_square_extra_negative; + }; + + std::vector cases; + cases.push_back(EndCapMatrixCase{ + "repeated_endpoints", + { + {1.274, 0.067, -0.237}, + {1.274, 0.067, -0.488}, + {1.913, 0.833, -0.735}, + {3.039, 0.660, 0.160}, + {4.423, 0.165, -0.129}, + {4.423, 0.165, -0.129}, + {5.062, 0.863, 0.000}, + {6.105, 0.891, 0.000}, + {6.105, 0.891, -0.327}, + }, + 1, + 1, + }); + cases.push_back(EndCapMatrixCase{ + "near_zero_radius_arc", + { + {0.000, 0.000, 0.966}, + {0.133, 0.000, -0.911}, + {0.272, 0.000, 0.000}, + {1.272, 0.189, 0.000}, + {2.272, 0.802, -0.201}, + {3.272, -0.049, 0.000}, + }, + 1, + 1, + }); + cases.push_back(EndCapMatrixCase{ + "high_density_self_intersection_open", + { + {0.000, 0.000, 0.513}, + {1.647, 1.281, 0.000}, + {2.447, -1.499, 0.000}, + {3.167, 1.313, 0.118}, + {3.739, -0.908, 0.000}, + {4.688, 1.162, -0.771}, + {6.126, 0.000, 0.000}, + }, + 1, + 1, + }); + + auto run_case = [](EndCapMatrixCase const &test_case, cavc_real delta, + uint32_t min_square_extra) { + PlinePtr pline(plineFromVertexes(test_case.vertexes, false)); + + cavc_parallel_offset_options round_options = cavc_parallel_offset_default_options(); + cavc_parallel_offset_options square_options = cavc_parallel_offset_default_options(); + square_options.end_cap_type = CAVC_OFFSET_END_CAP_SQUARE; + cavc_parallel_offset_options butt_options = cavc_parallel_offset_default_options(); + butt_options.end_cap_type = CAVC_OFFSET_END_CAP_BUTT; + + cavc_pline_list *raw_round = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_round, round_options); + PlineListPtr round_results(raw_round); + + cavc_pline_list *raw_square = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_square, square_options); + PlineListPtr square_results(raw_square); + + cavc_pline_list *raw_butt = nullptr; + cavc_parallel_offset(pline.get(), delta, &raw_butt, butt_options); + PlineListPtr butt_results(raw_butt); + + ASSERT_EQ(cavc_pline_list_count(round_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(square_results.get()), 1u); + ASSERT_EQ(cavc_pline_list_count(butt_results.get()), 1u); + + cavc_pline *round_pline = cavc_pline_list_get(round_results.get(), 0); + cavc_pline *square_pline = cavc_pline_list_get(square_results.get(), 0); + cavc_pline *butt_pline = cavc_pline_list_get(butt_results.get(), 0); + + std::vector round_vertexes = readVertexes(round_pline); + std::vector square_vertexes = readVertexes(square_pline); + std::vector butt_vertexes = readVertexes(butt_pline); + + ASSERT_FALSE(round_vertexes.empty()); + ASSERT_FALSE(square_vertexes.empty()); + ASSERT_FALSE(butt_vertexes.empty()); + + std::vector> round_wrap = {round_vertexes}; + std::vector> square_wrap = {square_vertexes}; + std::vector> butt_wrap = {butt_vertexes}; + EXPECT_THAT(square_wrap, t::Not(t::Pointwise(VertexListsFuzzyEqual(false), round_wrap))); + EXPECT_THAT(butt_wrap, t::Not(t::Pointwise(VertexListsFuzzyEqual(false), round_wrap))); + EXPECT_GE(square_vertexes.size(), round_vertexes.size() + min_square_extra); + + EXPECT_TRUE(vertexListAllFinite(round_vertexes)); + EXPECT_TRUE(vertexListAllFinite(square_vertexes)); + EXPECT_TRUE(vertexListAllFinite(butt_vertexes)); + EXPECT_GT(cavc_get_path_length(round_pline), 0.0); + EXPECT_GT(cavc_get_path_length(square_pline), 0.0); + EXPECT_GT(cavc_get_path_length(butt_pline), 0.0); + }; + + for (auto const &test_case : cases) { + SCOPED_TRACE(test_case.name); + run_case(test_case, 0.2, test_case.min_square_extra_positive); + run_case(test_case, -0.2, test_case.min_square_extra_negative); + } +} + +TEST(CApiRegression, SpatialIndexClosedPolylineQueryAndItemCount) { + std::vector square = { + {0.0, 0.0, 0.0}, {4.0, 0.0, 0.0}, {4.0, 2.0, 0.0}, {0.0, 2.0, 0.0}}; + PlinePtr pline(plineFromVertexes(square, true)); + SpatialIndexPtr index(cavc_spatial_index_create(pline.get()), cavc_spatial_index_delete); + + ASSERT_NE(index.get(), nullptr); + EXPECT_EQ(cavc_spatial_index_item_count(index.get()), 4u); + + std::vector right_hits = querySpatialIndexSorted(index.get(), 3.8, 0.5, 4.2, 1.5); + std::vector expected_right = {1u}; + EXPECT_THAT(right_hits, t::Pointwise(t::Eq(), expected_right)); + + std::vector all_hits = querySpatialIndexSorted(index.get(), -1.0, -1.0, 5.0, 3.0); + std::vector expected_all = {0u, 1u, 2u, 3u}; + EXPECT_THAT(all_hits, t::Pointwise(t::Eq(), expected_all)); +} + +TEST(CApiRegression, SpatialIndexOpenPolylineQueryAndItemCount) { + std::vector open_rect = { + {0.0, 0.0, 0.0}, {4.0, 0.0, 0.0}, {4.0, 2.0, 0.0}, {0.0, 2.0, 0.0}}; + PlinePtr pline(plineFromVertexes(open_rect, false)); + SpatialIndexPtr index(cavc_spatial_index_create(pline.get()), cavc_spatial_index_delete); + + ASSERT_NE(index.get(), nullptr); + EXPECT_EQ(cavc_spatial_index_item_count(index.get()), 3u); + + std::vector right_hits = querySpatialIndexSorted(index.get(), 3.8, 0.5, 4.2, 1.5); + std::vector expected_right = {1u}; + EXPECT_THAT(right_hits, t::Pointwise(t::Eq(), expected_right)); + + // Open polyline has no closing segment from last to first, so this query should miss. + std::vector left_hits = querySpatialIndexSorted(index.get(), -0.2, 0.5, 0.2, 1.5); + EXPECT_TRUE(left_hits.empty()); +} + +TEST(CApiRegression, EmptyPolylineExtentsReturnInfiniteBounds) { + PlinePtr pline(cavc_pline_new(nullptr, 0, 0)); + + cavc_real min_x = 0.0; + cavc_real min_y = 0.0; + cavc_real max_x = 0.0; + cavc_real max_y = 0.0; + cavc_get_extents(pline.get(), &min_x, &min_y, &max_x, &max_y); + + EXPECT_TRUE(std::isinf(min_x)); + EXPECT_TRUE(std::isinf(min_y)); + EXPECT_TRUE(std::isinf(max_x)); + EXPECT_TRUE(std::isinf(max_y)); + EXPECT_GT(min_x, 0.0); + EXPECT_GT(min_y, 0.0); + EXPECT_LT(max_x, 0.0); + EXPECT_LT(max_y, 0.0); + EXPECT_DOUBLE_EQ(cavc_get_area(pline.get()), 0.0); + EXPECT_DOUBLE_EQ(cavc_get_path_length(pline.get()), 0.0); +} + +TEST(CApiRegression, CombinePlinesDoesNotMutateInputs) { + std::vector pline_a_vertexes = { + {0.0, 0.0, 0.0}, {10.0, 0.0, 0.0}, {10.0, 5.0, 0.0}, {0.0, 5.0, 0.0}}; + std::vector pline_b_vertexes = { + {5.0, 1.0, 0.0}, {12.0, 1.0, 0.0}, {12.0, 7.0, 0.0}, {5.0, 7.0, 0.0}}; + + PlinePtr pline_a(plineFromVertexes(pline_a_vertexes, true)); + PlinePtr pline_b(plineFromVertexes(pline_b_vertexes, true)); + + std::vector before_a = readVertexes(pline_a.get()); + std::vector before_b = readVertexes(pline_b.get()); + + cavc_pline_list *raw_remaining = nullptr; + cavc_pline_list *raw_subtracted = nullptr; + cavc_combine_plines(pline_a.get(), pline_b.get(), 0, &raw_remaining, &raw_subtracted); + PlineListPtr remaining(raw_remaining); + PlineListPtr subtracted(raw_subtracted); + + ASSERT_NE(remaining.get(), nullptr); + ASSERT_NE(subtracted.get(), nullptr); + EXPECT_GE(cavc_pline_list_count(remaining.get()), 1u); + + std::vector after_a = readVertexes(pline_a.get()); + std::vector after_b = readVertexes(pline_b.get()); + EXPECT_THAT(after_a, t::Pointwise(VertexEqual(), before_a)); + EXPECT_THAT(after_b, t::Pointwise(VertexEqual(), before_b)); +} + +TEST(CApiRegression, InvertDirectionPreservesPathAndNegatesArea) { + std::vector original_vertexes = { + {0.0, 0.0, 0.0}, {4.0, 0.0, 0.414213562373095}, {4.0, 4.0, 0.0}, {0.0, 4.0, 0.0}}; + PlinePtr pline(plineFromVertexes(original_vertexes, true)); + + cavc_real original_area = cavc_get_area(pline.get()); + cavc_real original_path = cavc_get_path_length(pline.get()); + + std::vector expected_reversed = original_vertexes; + reverseDirection(expected_reversed); + + cavc_pline_invert_direction(pline.get()); + std::vector actual_reversed = readVertexes(pline.get()); + EXPECT_THAT(actual_reversed, t::Pointwise(VertexFuzzyEqual(), expected_reversed)); + EXPECT_NEAR(cavc_get_area(pline.get()), -original_area, TEST_EPSILON()); + EXPECT_NEAR(cavc_get_path_length(pline.get()), original_path, TEST_EPSILON()); + + cavc_pline_invert_direction(pline.get()); + std::vector roundtrip = readVertexes(pline.get()); + EXPECT_THAT(roundtrip, t::Pointwise(VertexFuzzyEqual(), original_vertexes)); +} + +TEST(CApiRegression, PruneSingularitiesRemovesDuplicatePositions) { + std::vector with_singularities = {{0.0, 0.0, 0.0}, {1.0, 0.0, 0.2}, {1.0, 0.0, 0.3}, + {1.0, 1.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 0.0}}; + PlinePtr input(plineFromVertexes(with_singularities, true)); + + cavc_pline *raw_output = nullptr; + cavc_pline_prune_singularities(input.get(), 1e-5, &raw_output); + PlinePtr output(raw_output); + + ASSERT_NE(output.get(), nullptr); + EXPECT_EQ(cavc_pline_vertex_count(input.get()), with_singularities.size()); + EXPECT_EQ(cavc_pline_vertex_count(output.get()), 4u); + EXPECT_EQ(cavc_pline_is_closed(output.get()), 1); + + std::vector expected = { + {0.0, 0.0, 0.0}, {1.0, 0.0, 0.3}, {1.0, 1.0, 0.0}, {0.0, 1.0, 0.0}}; + std::vector actual = readVertexes(output.get()); + EXPECT_THAT(actual, t::Pointwise(VertexEqual(), expected)); +} + +TEST(CApiRegression, ConvertArcsToLinesProducesLineSegments) { + const cavc_real radius = 2.0; + std::vector circle = {{0.0, 0.0, 1.0}, {2.0 * radius, 0.0, 1.0}}; + PlinePtr input(plineFromVertexes(circle, true)); + + cavc_pline *raw_output = nullptr; + cavc_pline_convert_arcs_to_lines(input.get(), 0.01, &raw_output); + PlinePtr output(raw_output); + + ASSERT_NE(output.get(), nullptr); + EXPECT_EQ(cavc_pline_is_closed(output.get()), 1); + EXPECT_GT(cavc_pline_vertex_count(output.get()), cavc_pline_vertex_count(input.get())); + + std::vector output_vertexes = readVertexes(output.get()); + for (const cavc_vertex &v : output_vertexes) { + EXPECT_NEAR(v.bulge, 0.0, TEST_EPSILON()); + } + + const cavc_real expected_area = PI() * radius * radius; + EXPECT_NEAR(std::abs(cavc_get_area(output.get())), expected_area, 0.05 * expected_area); + + std::vector input_after = readVertexes(input.get()); + EXPECT_THAT(input_after, t::Pointwise(VertexEqual(), circle)); +} + +TEST(CApiRegression, NormalizePrunesSingularitiesAndKeepsWinding) { + std::vector with_singularities = {{0.0, 0.0, 0.0}, {0.0, 2.0, 0.0}, {2.0, 2.0, 0.2}, + {2.0, 2.0, 0.3}, {2.0, 0.0, 0.0}, {0.0, 0.0, 0.0}}; + PlinePtr input(plineFromVertexes(with_singularities, true)); + + const cavc_real area_before = cavc_get_area(input.get()); + + cavc_pline *raw_output = nullptr; + cavc_pline_normalize(input.get(), 1e-5, CAVC_WINDING_KEEP, &raw_output); + PlinePtr output(raw_output); + + ASSERT_NE(output.get(), nullptr); + EXPECT_EQ(cavc_pline_vertex_count(input.get()), with_singularities.size()); + EXPECT_EQ(cavc_pline_vertex_count(output.get()), 4u); + EXPECT_LT(area_before, 0.0); + EXPECT_LT(cavc_get_area(output.get()), 0.0); +} + +TEST(CApiRegression, NormalizeCanForceCounterClockwiseWinding) { + std::vector clockwise = { + {0.0, 0.0, 0.0}, {0.0, 2.0, 0.0}, {2.0, 2.0, 0.0}, {2.0, 0.0, 0.0}}; + PlinePtr input(plineFromVertexes(clockwise, true)); + + ASSERT_LT(cavc_get_area(input.get()), 0.0); + cavc_pline *raw_output = nullptr; + cavc_pline_normalize(input.get(), 1e-5, CAVC_WINDING_COUNTER_CLOCKWISE, &raw_output); + PlinePtr output(raw_output); + + ASSERT_NE(output.get(), nullptr); + EXPECT_GT(cavc_get_area(output.get()), 0.0); + EXPECT_EQ(cavc_pline_vertex_count(output.get()), 4u); +} + +TEST(CApiRegression, NormalizeCanForceClockwiseWinding) { + std::vector counter_clockwise = { + {0.0, 0.0, 0.0}, {2.0, 0.0, 0.0}, {2.0, 2.0, 0.0}, {0.0, 2.0, 0.0}}; + PlinePtr input(plineFromVertexes(counter_clockwise, true)); + + ASSERT_GT(cavc_get_area(input.get()), 0.0); + cavc_pline *raw_output = nullptr; + cavc_pline_normalize(input.get(), 1e-5, CAVC_WINDING_CLOCKWISE, &raw_output); + PlinePtr output(raw_output); + + ASSERT_NE(output.get(), nullptr); + EXPECT_LT(cavc_get_area(output.get()), 0.0); + EXPECT_EQ(cavc_pline_vertex_count(output.get()), 4u); +} + +TEST(CApiRegression, PointContainmentClassifiesClosedPolyline) { + std::vector square = { + {0.0, 0.0, 0.0}, {2.0, 0.0, 0.0}, {2.0, 2.0, 0.0}, {0.0, 2.0, 0.0}}; + PlinePtr pline(plineFromVertexes(square, true)); + + EXPECT_EQ(cavc_get_point_containment(pline.get(), cavc_point{1.0, 1.0}, 1e-6), CAVC_POINT_INSIDE); + EXPECT_EQ(cavc_get_point_containment(pline.get(), cavc_point{3.0, 1.0}, 1e-6), + CAVC_POINT_OUTSIDE); + EXPECT_EQ(cavc_get_point_containment(pline.get(), cavc_point{0.0, 1.0}, 1e-6), + CAVC_POINT_ON_BOUNDARY); + EXPECT_EQ(cavc_get_point_containment(pline.get(), cavc_point{0.0, 0.0}, 1e-6), + CAVC_POINT_ON_BOUNDARY); +} + +TEST(CApiRegression, PointContainmentForOpenPolylineHasNoInside) { + std::vector seg = {{0.0, 0.0, 0.0}, {2.0, 0.0, 0.0}}; + PlinePtr pline(plineFromVertexes(seg, false)); + + EXPECT_EQ(cavc_get_point_containment(pline.get(), cavc_point{1.0, 0.0}, 1e-6), + CAVC_POINT_ON_BOUNDARY); + EXPECT_EQ(cavc_get_point_containment(pline.get(), cavc_point{1.0, 0.1}, 1e-6), + CAVC_POINT_OUTSIDE); + EXPECT_EQ(cavc_get_point_containment(pline.get(), cavc_point{1.0, 0.1}, 0.2), + CAVC_POINT_ON_BOUNDARY); +} + +TEST(CApiRegression, OffsetLoopTopologyBuildProducesStableOrderAndParentChild) { + auto island_vertexes = makeAxisAlignedRectLoopVertexes(12.0, 4.0, 16.0, 8.0, false); + auto outer_vertexes = makeAxisAlignedRectLoopVertexes(0.0, 0.0, 20.0, 20.0, false); + auto hole_b_vertexes = makeAxisAlignedRectLoopVertexes(10.0, 2.0, 18.0, 10.0, true); + auto hole_a_vertexes = makeAxisAlignedRectLoopVertexes(2.0, 2.0, 8.0, 8.0, true); + auto hole_c_vertexes = makeAxisAlignedRectLoopVertexes(13.0, 5.0, 15.0, 7.0, true); + + PlinePtr island(plineFromVertexes(island_vertexes, true)); + PlinePtr outer(plineFromVertexes(outer_vertexes, true)); + PlinePtr hole_b(plineFromVertexes(hole_b_vertexes, true)); + PlinePtr hole_a(plineFromVertexes(hole_a_vertexes, true)); + PlinePtr hole_c(plineFromVertexes(hole_c_vertexes, true)); + + cavc_pline const *ccw_loops[] = {island.get(), outer.get()}; + cavc_pline const *cw_loops[] = {hole_b.get(), hole_a.get(), hole_c.get()}; + + TopologyPtr topology( + cavc_offset_loop_topology_build(ccw_loops, 2, cw_loops, 3, static_cast(1e-5))); + ASSERT_NE(topology.get(), nullptr); + ASSERT_EQ(cavc_offset_loop_topology_count(topology.get()), 5u); + + cavc_offset_loop_topology_node node0 = cavc_offset_loop_topology_get(topology.get(), 0); + cavc_offset_loop_topology_node node1 = cavc_offset_loop_topology_get(topology.get(), 1); + cavc_offset_loop_topology_node node2 = cavc_offset_loop_topology_get(topology.get(), 2); + cavc_offset_loop_topology_node node3 = cavc_offset_loop_topology_get(topology.get(), 3); + cavc_offset_loop_topology_node node4 = cavc_offset_loop_topology_get(topology.get(), 4); + + EXPECT_EQ(node0.role, CAVC_OFFSET_LOOP_ROLE_OUTER); + EXPECT_EQ(node0.source_index, 1u); + EXPECT_EQ(node1.role, CAVC_OFFSET_LOOP_ROLE_HOLE); + EXPECT_EQ(node1.source_index, 0u); + EXPECT_EQ(node2.role, CAVC_OFFSET_LOOP_ROLE_HOLE); + EXPECT_EQ(node2.source_index, 1u); + EXPECT_EQ(node3.role, CAVC_OFFSET_LOOP_ROLE_OUTER); + EXPECT_EQ(node3.source_index, 0u); + EXPECT_EQ(node4.role, CAVC_OFFSET_LOOP_ROLE_HOLE); + EXPECT_EQ(node4.source_index, 2u); + + uint32_t outer_index = findTopologyNodeIndex(topology.get(), CAVC_OFFSET_LOOP_ROLE_OUTER, 1u); + uint32_t hole_b_index = findTopologyNodeIndex(topology.get(), CAVC_OFFSET_LOOP_ROLE_HOLE, 0u); + uint32_t hole_a_index = findTopologyNodeIndex(topology.get(), CAVC_OFFSET_LOOP_ROLE_HOLE, 1u); + uint32_t island_index = findTopologyNodeIndex(topology.get(), CAVC_OFFSET_LOOP_ROLE_OUTER, 0u); + uint32_t hole_c_index = findTopologyNodeIndex(topology.get(), CAVC_OFFSET_LOOP_ROLE_HOLE, 2u); + + ASSERT_NE(outer_index, CAVC_OFFSET_LOOP_NO_PARENT); + ASSERT_NE(hole_b_index, CAVC_OFFSET_LOOP_NO_PARENT); + ASSERT_NE(hole_a_index, CAVC_OFFSET_LOOP_NO_PARENT); + ASSERT_NE(island_index, CAVC_OFFSET_LOOP_NO_PARENT); + ASSERT_NE(hole_c_index, CAVC_OFFSET_LOOP_NO_PARENT); + + EXPECT_EQ(cavc_offset_loop_topology_get(topology.get(), outer_index).parent_index, + CAVC_OFFSET_LOOP_NO_PARENT); + EXPECT_EQ(cavc_offset_loop_topology_get(topology.get(), hole_b_index).parent_index, outer_index); + EXPECT_EQ(cavc_offset_loop_topology_get(topology.get(), hole_a_index).parent_index, outer_index); + EXPECT_EQ(cavc_offset_loop_topology_get(topology.get(), island_index).parent_index, hole_b_index); + EXPECT_EQ(cavc_offset_loop_topology_get(topology.get(), hole_c_index).parent_index, island_index); +} + +TEST(CApiRegression, OffsetLoopTopologySupportsOuterOnlyInput) { + auto small_vertexes = makeAxisAlignedRectLoopVertexes(0.0, 0.0, 2.0, 2.0, false); + auto large_vertexes = makeAxisAlignedRectLoopVertexes(10.0, 0.0, 20.0, 8.0, false); + + PlinePtr small(plineFromVertexes(small_vertexes, true)); + PlinePtr large(plineFromVertexes(large_vertexes, true)); + + cavc_pline const *ccw_loops[] = {small.get(), large.get()}; + + TopologyPtr topology( + cavc_offset_loop_topology_build(ccw_loops, 2, nullptr, 0, static_cast(1e-5))); + ASSERT_NE(topology.get(), nullptr); + ASSERT_EQ(cavc_offset_loop_topology_count(topology.get()), 2u); + + cavc_offset_loop_topology_node node0 = cavc_offset_loop_topology_get(topology.get(), 0); + cavc_offset_loop_topology_node node1 = cavc_offset_loop_topology_get(topology.get(), 1); + + EXPECT_EQ(node0.role, CAVC_OFFSET_LOOP_ROLE_OUTER); + EXPECT_EQ(node0.source_index, 1u); + EXPECT_EQ(node0.parent_index, CAVC_OFFSET_LOOP_NO_PARENT); + EXPECT_EQ(node1.role, CAVC_OFFSET_LOOP_ROLE_OUTER); + EXPECT_EQ(node1.source_index, 0u); + EXPECT_EQ(node1.parent_index, CAVC_OFFSET_LOOP_NO_PARENT); +} + +TEST(CApiRegression, TolerancesSetGetResetRoundTrip) { + cavc_tolerances defaults{}; + cavc_get_tolerances(&defaults); + EXPECT_GT(defaults.real_threshold, 0.0); + EXPECT_GT(defaults.real_precision, 0.0); + EXPECT_GT(defaults.slice_join_threshold, 0.0); + EXPECT_GT(defaults.offset_threshold, 0.0); + + cavc_tolerances custom{2e-7, 3e-6, 4e-5, 5e-5}; + cavc_set_tolerances(&custom); + + cavc_tolerances after_set{}; + cavc_get_tolerances(&after_set); + EXPECT_EQ(after_set.real_threshold, custom.real_threshold); + EXPECT_EQ(after_set.real_precision, custom.real_precision); + EXPECT_EQ(after_set.slice_join_threshold, custom.slice_join_threshold); + EXPECT_EQ(after_set.offset_threshold, custom.offset_threshold); + + cavc_reset_tolerances(); + cavc_tolerances after_reset{}; + cavc_get_tolerances(&after_reset); + EXPECT_EQ(after_reset.real_threshold, defaults.real_threshold); + EXPECT_EQ(after_reset.real_precision, defaults.real_precision); + EXPECT_EQ(after_reset.slice_join_threshold, defaults.slice_join_threshold); + EXPECT_EQ(after_reset.offset_threshold, defaults.offset_threshold); +} + +int main(int argc, char **argv) { + t::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/tests/TEST_cavc_combine_plines.cpp b/tests/tests/TEST_cavc_combine_plines.cpp index a161a85..d3b2983 100644 --- a/tests/tests/TEST_cavc_combine_plines.cpp +++ b/tests/tests/TEST_cavc_combine_plines.cpp @@ -16,34 +16,33 @@ struct CombinePlinesTestCase { std::vector expectedRemaining; std::vector expectedSubtracted; - CombinePlinesTestCase(std::string name, int combineMode, - std::vector const &plineVertexesA, - std::vector const &plineVertexesB, - std::vector expectedRemaining, - std::vector expectedSubtracted) - : name(std::move(name)), combineMode(combineMode), - plineA( - cavc_pline_new(&plineVertexesA[0], static_cast(plineVertexesA.size()), true)), - plineB( - cavc_pline_new(&plineVertexesA[0], static_cast(plineVertexesB.size()), true)), - expectedRemaining(std::move(expectedRemaining)), - expectedSubtracted(std::move(expectedSubtracted)) {} - - CombinePlinesTestCase(std::string name, int combineMode, cavc_pline *plineA, cavc_pline *plineB, - std::vector expectedRemaining, - std::vector expectedSubtracted) - : name(std::move(name)), combineMode(combineMode), plineA(plineA), plineB(plineB), - - expectedRemaining(std::move(expectedRemaining)), - expectedSubtracted(std::move(expectedSubtracted)) {}; + CombinePlinesTestCase(std::string p_name, int p_combineMode, + std::vector const &p_plineVertexesA, + std::vector const &p_plineVertexesB, + std::vector p_expectedRemaining, + std::vector p_expectedSubtracted) + : name(std::move(p_name)), combineMode(p_combineMode), + plineA(cavc_pline_new(&p_plineVertexesA[0], static_cast(p_plineVertexesA.size()), + true)), + plineB(cavc_pline_new(&p_plineVertexesB[0], static_cast(p_plineVertexesB.size()), + true)), + expectedRemaining(std::move(p_expectedRemaining)), + expectedSubtracted(std::move(p_expectedSubtracted)) {} + + CombinePlinesTestCase(std::string p_name, int p_combineMode, cavc_pline *p_plineA, + cavc_pline *p_plineB, std::vector p_expectedRemaining, + std::vector p_expectedSubtracted) + : name(std::move(p_name)), combineMode(p_combineMode), plineA(p_plineA), plineB(p_plineB), + expectedRemaining(std::move(p_expectedRemaining)), + expectedSubtracted(std::move(p_expectedSubtracted)) {}; }; - -std::ostream &operator<<(std::ostream &os, CombinePlinesTestCase const &c) { +namespace { +[[maybe_unused]] std::ostream &operator<<(std::ostream &os, CombinePlinesTestCase const &c) { os << "{ " << c.name << ", combineMode: " << c.combineMode << " }"; return os; } -static std::vector createSimpleCases() { +std::vector createSimpleCases() { std::vector cases; { @@ -97,7 +96,7 @@ static std::vector createSimpleCases() { return cases; } -static std::vector createCoincidentCases() { +std::vector createCoincidentCases() { std::vector cases; { @@ -203,6 +202,7 @@ static std::vector createCoincidentCases() { return cases; } +} // namespace static std::vector simpleCases = createSimpleCases(); static std::vector coincidentCases = createCoincidentCases(); diff --git a/tests/tests/TEST_cavc_offset_islands.cpp b/tests/tests/TEST_cavc_offset_islands.cpp new file mode 100644 index 0000000..d4864be --- /dev/null +++ b/tests/tests/TEST_cavc_offset_islands.cpp @@ -0,0 +1,116 @@ +#include +#include + +#include + +#include "cavc/polylineoffsetislands.hpp" + +namespace { +using cavc::createApproxSpatialIndex; +using cavc::invertDirection; +using cavc::kNoParentOffsetLoop; +using cavc::OffsetLoop; +using cavc::OffsetLoopRole; +using cavc::OffsetLoopSet; +using cavc::OffsetLoopTopologyNode; +using cavc::ParallelOffsetIslands; +using cavc::Polyline; + +OffsetLoop makeAxisAlignedRectLoop(double min_x, double min_y, double max_x, double max_y, + bool clockwise, std::size_t parent_index) { + Polyline loop; + loop.addVertex(min_x, min_y, 0.0); + loop.addVertex(max_x, min_y, 0.0); + loop.addVertex(max_x, max_y, 0.0); + loop.addVertex(min_x, max_y, 0.0); + loop.isClosed() = true; + if (clockwise) { + invertDirection(loop); + } + + auto spatial_index = createApproxSpatialIndex(loop); + return {parent_index, std::move(loop), std::move(spatial_index)}; +} + +std::size_t findNodeIndex(std::vector> const &topology, + OffsetLoopRole role, std::size_t source_index) { + for (std::size_t i = 0; i < topology.size(); ++i) { + if (topology[i].role == role && topology[i].sourceIndex == source_index) { + return i; + } + } + return kNoParentOffsetLoop; +} +} // namespace + +TEST(OffsetIslands, BuildTopologyProducesStableOrderAndParentChild) { + OffsetLoopSet loop_set; + // Unsorted input on purpose. + loop_set.ccwLoops.push_back(makeAxisAlignedRectLoop(12.0, 4.0, 16.0, 8.0, false, 0)); // island + loop_set.ccwLoops.push_back(makeAxisAlignedRectLoop(0.0, 0.0, 20.0, 20.0, false, 1)); // outer + loop_set.cwLoops.push_back(makeAxisAlignedRectLoop(10.0, 2.0, 18.0, 10.0, true, 0)); // hole B + loop_set.cwLoops.push_back(makeAxisAlignedRectLoop(2.0, 2.0, 8.0, 8.0, true, 1)); // hole A + loop_set.cwLoops.push_back(makeAxisAlignedRectLoop(13.0, 5.0, 15.0, 7.0, true, 2)); // hole C + + std::vector> topology = cavc::buildOffsetLoopTopology(loop_set); + ASSERT_EQ(topology.size(), 5u); + + // Stable order is area-desc then bbox key. + EXPECT_EQ(topology[0].role, OffsetLoopRole::Outer); + EXPECT_EQ(topology[0].sourceIndex, 1u); + EXPECT_EQ(topology[1].role, OffsetLoopRole::Hole); + EXPECT_EQ(topology[1].sourceIndex, 0u); + EXPECT_EQ(topology[2].role, OffsetLoopRole::Hole); + EXPECT_EQ(topology[2].sourceIndex, 1u); + EXPECT_EQ(topology[3].role, OffsetLoopRole::Outer); + EXPECT_EQ(topology[3].sourceIndex, 0u); + EXPECT_EQ(topology[4].role, OffsetLoopRole::Hole); + EXPECT_EQ(topology[4].sourceIndex, 2u); + + std::size_t outer_index = findNodeIndex(topology, OffsetLoopRole::Outer, 1); + std::size_t hole_b_index = findNodeIndex(topology, OffsetLoopRole::Hole, 0); + std::size_t hole_a_index = findNodeIndex(topology, OffsetLoopRole::Hole, 1); + std::size_t island_index = findNodeIndex(topology, OffsetLoopRole::Outer, 0); + std::size_t hole_c_index = findNodeIndex(topology, OffsetLoopRole::Hole, 2); + + ASSERT_NE(outer_index, kNoParentOffsetLoop); + ASSERT_NE(hole_b_index, kNoParentOffsetLoop); + ASSERT_NE(hole_a_index, kNoParentOffsetLoop); + ASSERT_NE(island_index, kNoParentOffsetLoop); + ASSERT_NE(hole_c_index, kNoParentOffsetLoop); + + EXPECT_EQ(topology[outer_index].parentIndex, kNoParentOffsetLoop); + EXPECT_EQ(topology[hole_b_index].parentIndex, outer_index); + EXPECT_EQ(topology[hole_a_index].parentIndex, outer_index); + EXPECT_EQ(topology[island_index].parentIndex, hole_b_index); + EXPECT_EQ(topology[hole_c_index].parentIndex, island_index); +} + +TEST(OffsetIslands, ComputeSortsOutputLoopsByStableGeometryOrder) { + OffsetLoopSet input; + // Small then large to verify sorting is applied in compute result. + input.ccwLoops.push_back(makeAxisAlignedRectLoop(0.0, 0.0, 2.0, 2.0, false, 0)); + input.ccwLoops.push_back(makeAxisAlignedRectLoop(10.0, 0.0, 20.0, 8.0, false, 1)); + + ParallelOffsetIslands algorithm; + OffsetLoopSet result = algorithm.compute(input, 0.5); + + ASSERT_EQ(result.ccwLoops.size(), 2u); + ASSERT_TRUE(result.cwLoops.empty()); + + double area0 = std::abs(cavc::getArea(result.ccwLoops[0].polyline)); + double area1 = std::abs(cavc::getArea(result.ccwLoops[1].polyline)); + EXPECT_GT(area0, area1); + + std::vector> topology = cavc::buildOffsetLoopTopology(result); + ASSERT_EQ(topology.size(), 2u); + EXPECT_EQ(topology[0].role, OffsetLoopRole::Outer); + EXPECT_EQ(topology[1].role, OffsetLoopRole::Outer); + EXPECT_EQ(topology[0].parentIndex, kNoParentOffsetLoop); + EXPECT_EQ(topology[1].parentIndex, kNoParentOffsetLoop); +} + +int main(int argc, char **argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/tests/TEST_cavc_parallel_offset.cpp b/tests/tests/TEST_cavc_parallel_offset.cpp index 1c34236..a5ae6b1 100644 --- a/tests/tests/TEST_cavc_parallel_offset.cpp +++ b/tests/tests/TEST_cavc_parallel_offset.cpp @@ -14,15 +14,16 @@ struct ParallelOffsetTestCase { cavc_pline *pline; std::vector expectedResult; - ParallelOffsetTestCase(std::string name, cavc_real delta, - std::vector const &plineVertexes, bool isClosed, - std::vector expectedResult) - : name(std::move(name)), offsetDelta(delta), - pline(plineFromVertexes(plineVertexes, isClosed)), - expectedResult(std::move(expectedResult)) {} + ParallelOffsetTestCase(std::string p_name, cavc_real p_delta, + std::vector const &p_plineVertexes, bool isClosed, + std::vector p_expectedResult) + : name(std::move(p_name)), offsetDelta(p_delta), + pline(plineFromVertexes(p_plineVertexes, isClosed)), + expectedResult(std::move(p_expectedResult)) {} }; +namespace { -std::ostream &operator<<(std::ostream &os, ParallelOffsetTestCase const &c) { +[[maybe_unused]] std::ostream &operator<<(std::ostream &os, ParallelOffsetTestCase const &c) { os << "{ " << c.name << ", offsetDelta: " << c.offsetDelta << " }"; return os; } @@ -76,7 +77,6 @@ std::vector createSpecificCases() { return cases; } - static std::vector specificCases = createSpecificCases(); std::vector createSimpleCases() { @@ -171,6 +171,7 @@ std::vector createSimpleCases() { return cases; } +} // namespace static std::vector simpleCases = createSimpleCases(); @@ -184,7 +185,8 @@ INSTANTIATE_TEST_SUITE_P(simple_cases, cavc_parallel_offsetTests, t::ValuesIn(si TEST_P(cavc_parallel_offsetTests, parallel_offset_test) { ParallelOffsetTestCase const &testCase = GetParam(); cavc_pline_list *results; - cavc_parallel_offset(testCase.pline, testCase.offsetDelta, &results, 0); + cavc_parallel_offset(testCase.pline, testCase.offsetDelta, &results, + defaultParallelOffsetOptions()); ASSERT_EQ(cavc_pline_list_count(results), testCase.expectedResult.size()); std::vector resultsProperties; @@ -214,7 +216,7 @@ TEST_P(cavc_parallel_offsetTests, reversed_parallel_offset_test) { } cavc_pline_list *results = nullptr; - cavc_parallel_offset(revPline, delta, &results, 0); + cavc_parallel_offset(revPline, delta, &results, defaultParallelOffsetOptions()); ASSERT_EQ(cavc_pline_list_count(results), expectedResult.size()); std::vector resultsProperties; @@ -237,14 +239,32 @@ TEST(cavc_parallel_offsetTests, parallel_offset_does_not_modify_input_test) { cavc_pline_vertex_data(testCase.pline, &vertexesBefore[0]); cavc_pline_list *results = nullptr; - cavc_parallel_offset(testCase.pline, testCase.offsetDelta, &results, 0); + cavc_parallel_offset(testCase.pline, testCase.offsetDelta, &results, + defaultParallelOffsetOptions()); std::vector vertexesAfter(cavc_pline_vertex_count(testCase.pline)); cavc_pline_vertex_data(testCase.pline, &vertexesAfter[0]); - ASSERT_THAT(vertexesAfter, t::Pointwise(VertexEqual(), vertexesAfter)); + ASSERT_THAT(vertexesAfter, t::Pointwise(VertexEqual(), vertexesBefore)); + + cavc_pline_list_delete(results); +} + +TEST(cavc_parallel_offsetTests, parallel_offset_handles_repeat_position_input_test) { + std::vector vertexes = {{0, 0, 0}, {20, 0, 0}, {20, 0, 0}, {20, 10, 0}, {0, 10, 0}}; + cavc_pline *pline = plineFromVertexes(vertexes, true); + + cavc_pline_list *results = nullptr; + cavc_parallel_offset(pline, -2, &results, defaultParallelOffsetOptions()); + + ASSERT_EQ(cavc_pline_list_count(results), 1u); + cavc_pline *resultPline = cavc_pline_list_get(results, 0); + PolylineProperties actual(resultPline); + PolylineProperties expected(8, 332.56637061436, 72.566370614359, -2, -2, 22, 12); + ASSERT_EQ(actual, expected); cavc_pline_list_delete(results); + cavc_pline_delete(pline); } int main(int argc, char **argv) { diff --git a/tests/tests/TEST_cavc_pline_function.cpp b/tests/tests/TEST_cavc_pline_function.cpp index d1b24ee..72fbc46 100644 --- a/tests/tests/TEST_cavc_pline_function.cpp +++ b/tests/tests/TEST_cavc_pline_function.cpp @@ -7,9 +7,10 @@ namespace t = testing; struct cavc_plineFunctionsTestCase { - cavc_plineFunctionsTestCase(std::string name, std::vector vertexes, bool isClosed) - : name(std::move(name)), pline(plineFromVertexes(vertexes, isClosed)), - plineVertexes(std::move(vertexes)) {}; + cavc_plineFunctionsTestCase(std::string p_name, std::vector p_vertexes, + bool isClosed) + : name(std::move(p_name)), pline(plineFromVertexes(p_vertexes, isClosed)), + plineVertexes(std::move(p_vertexes)) {}; // simple name for the test case std::string name; @@ -25,7 +26,7 @@ struct cavc_plineFunctionsTestCase { // expected path length cavc_real pathLength = std::numeric_limits::quiet_NaN(); - bool skipPathLengthTest() const { return std::isnan(signedArea); } + bool skipPathLengthTest() const { return std::isnan(pathLength); } // expected extents cavc_real minX = std::numeric_limits::quiet_NaN(); @@ -85,7 +86,22 @@ struct cavc_plineFunctionsTestCase { bool isClosed() const { return cavc_pline_is_closed(pline); } }; -std::ostream &operator<<(std::ostream &os, cavc_plineFunctionsTestCase const &c) { +struct CombineWithSelfTestCase { + CombineWithSelfTestCase(std::string p_name, std::vector p_vertexes, bool is_closed) + : name(std::move(p_name)), pline(plineFromVertexes(p_vertexes, is_closed)), + plineVertexes(std::move(p_vertexes)) {} + + std::string name; + cavc_pline *pline = nullptr; + std::vector plineVertexes; +}; +namespace { +[[maybe_unused]] std::ostream &operator<<(std::ostream &os, cavc_plineFunctionsTestCase const &c) { + os << c.name; + return os; +} + +[[maybe_unused]] std::ostream &operator<<(std::ostream &os, CombineWithSelfTestCase const &c) { os << c.name; return os; } @@ -167,8 +183,8 @@ void addCircleCases(std::vector &cases, cavc_real c std::vector onCirclAt45deg; onCirclAt45deg.reserve(4); for (std::size_t i = 0; i < 4; ++i) { - cavc_real xCos = std::cos(PI() / 4 + i * PI() / 2); - cavc_real ySin = std::sin(PI() / 4 + i * PI() / 2); + cavc_real xCos = std::cos(PI() / 4 + static_cast(i) * PI() / 2); + cavc_real ySin = std::sin(PI() / 4 + static_cast(i) * PI() / 2); cavc_real x = circleCenter.x + insideDist * xCos; cavc_real y = circleCenter.y + insideDist * ySin; @@ -535,6 +551,19 @@ std::vector createHalfCircleCases() { return result; } +std::vector +createCombineWithSelfCases(std::vector const &cases) { + std::vector result; + for (auto const &test_case : cases) { + if (!test_case.isClosed()) { + continue; + } + result.emplace_back(test_case.name, test_case.plineVertexes, true); + } + return result; +} +} // namespace + class cavc_plineFunctionTests : public t::TestWithParam { protected: void SetUp() override; @@ -560,6 +589,19 @@ INSTANTIATE_TEST_SUITE_P(cavc_pline_circles, cavc_plineFunctionTests, INSTANTIATE_TEST_SUITE_P(cavc_pline_half_circles, cavc_plineFunctionTests, t::ValuesIn(cavc_plineFunctionTests::halfCircleCases)); +static std::vector combineWithSelfCircleCases = + createCombineWithSelfCases(cavc_plineFunctionTests::circleCases); +static std::vector combineWithSelfHalfCircleCases = + createCombineWithSelfCases(cavc_plineFunctionTests::halfCircleCases); + +class cavc_combineWithSelfTests : public t::TestWithParam {}; + +INSTANTIATE_TEST_SUITE_P(cavc_combine_with_self_circles, cavc_combineWithSelfTests, + t::ValuesIn(combineWithSelfCircleCases)); + +INSTANTIATE_TEST_SUITE_P(cavc_combine_with_self_half_circles, cavc_combineWithSelfTests, + t::ValuesIn(combineWithSelfHalfCircleCases)); + TEST_P(cavc_plineFunctionTests, cavc_get_path_length) { cavc_plineFunctionsTestCase const &testCase = GetParam(); if (testCase.skipPathLengthTest()) { @@ -650,7 +692,8 @@ TEST_P(cavc_plineFunctionTests, cavc_parallel_offset) { if (!testCase.skipOffsetTest()) { std::vector results(testCase.offsetTestDeltas.size()); for (std::size_t i = 0; i < testCase.offsetTestDeltas.size(); ++i) { - cavc_parallel_offset(testCase.pline, testCase.offsetTestDeltas[i], &results[i], 0); + cavc_parallel_offset(testCase.pline, testCase.offsetTestDeltas[i], &results[i], + defaultParallelOffsetOptions()); } // test there is only one resulting offset pline @@ -683,7 +726,8 @@ TEST_P(cavc_plineFunctionTests, cavc_parallel_offset) { if (!testCase.skipCollapsedOffsetTest()) { std::vector results(testCase.collapsedOffsetDeltas.size()); for (std::size_t i = 0; i < testCase.collapsedOffsetDeltas.size(); ++i) { - cavc_parallel_offset(testCase.pline, testCase.collapsedOffsetDeltas[i], &results[i], 0); + cavc_parallel_offset(testCase.pline, testCase.collapsedOffsetDeltas[i], &results[i], + defaultParallelOffsetOptions()); } ASSERT_THAT(results, t::Each(t::ResultOf(cavc_pline_list_count, t::Eq(0)))); for (auto result : results) { @@ -692,11 +736,8 @@ TEST_P(cavc_plineFunctionTests, cavc_parallel_offset) { } } -TEST_P(cavc_plineFunctionTests, combine_with_self_invariants) { - cavc_plineFunctionsTestCase const &testCase = GetParam(); - if (!testCase.isClosed()) { - GTEST_SKIP(); - } +TEST_P(cavc_combineWithSelfTests, combine_with_self_invariants) { + CombineWithSelfTestCase const &testCase = GetParam(); cavc_pline_list *remaining = nullptr; cavc_pline_list *subtracted = nullptr; diff --git a/tests/tests/TEST_sample.cpp b/tests/tests/TEST_sample.cpp index c0a4f8e..314b870 100644 --- a/tests/tests/TEST_sample.cpp +++ b/tests/tests/TEST_sample.cpp @@ -5,7 +5,6 @@ #include "c_api_include/cavaliercontours.h" #include "c_api_test_helpers.hpp" -#include "cavc/polyline.hpp" // Use basic gtest rather than gmock to make debug easier TEST(basic, basic_extent) { diff --git a/tests/tests/TEST_staticspatialindex.cpp b/tests/tests/TEST_staticspatialindex.cpp index 69a8fc3..d89b477 100644 --- a/tests/tests/TEST_staticspatialindex.cpp +++ b/tests/tests/TEST_staticspatialindex.cpp @@ -9,315 +9,315 @@ namespace t = testing; -std::vector createTestData() -{ - return std::vector{ - 8, 62, 11, 66, 57, 17, 57, 19, 76, 26, 79, 29, 36, 56, 38, 56, 92, 77, 96, 80, 87, 70, 90, - 74, 43, 41, 47, 43, 0, 58, 2, 62, 76, 86, 80, 89, 27, 13, 27, 15, 71, 63, 75, 67, 25, 2, - 27, 2, 87, 6, 88, 6, 22, 90, 23, 93, 22, 89, 22, 93, 57, 11, 61, 13, 61, 55, 63, 56, 17, - 85, 21, 87, 33, 43, 37, 43, 6, 1, 7, 3, 80, 87, 80, 87, 23, 50, 26, 52, 58, 89, 58, 89, - 12, 30, 15, 34, 32, 58, 36, 61, 41, 84, 44, 87, 44, 18, 44, 19, 13, 63, 15, 67, 52, 70, 54, - 74, 57, 59, 58, 59, 17, 90, 20, 92, 48, 53, 52, 56, 92, 68, 92, 72, 26, 52, 30, 52, 56, 23, - 57, 26, 88, 48, 88, 48, 66, 13, 67, 15, 7, 82, 8, 86, 46, 68, 50, 68, 37, 33, 38, 36, 6, - 15, 8, 18, 85, 36, 89, 38, 82, 45, 84, 48, 12, 2, 16, 3, 26, 15, 26, 16, 55, 23, 59, 26, - 76, 37, 79, 39, 86, 74, 90, 77, 16, 75, 18, 78, 44, 18, 45, 21, 52, 67, 54, 71, 59, 78, 62, - 78, 24, 5, 24, 8, 64, 80, 64, 83, 66, 55, 70, 55, 0, 17, 2, 19, 15, 71, 18, 74, 87, 57, - 87, 59, 6, 34, 7, 37, 34, 30, 37, 32, 51, 19, 53, 19, 72, 51, 73, 55, 29, 45, 30, 45, 94, - 94, 96, 95, 7, 22, 11, 24, 86, 45, 87, 48, 33, 62, 34, 65, 18, 10, 21, 14, 64, 66, 67, 67, - 64, 25, 65, 28, 27, 4, 31, 6, 84, 4, 85, 5, 48, 80, 50, 81, 1, 61, 3, 61, 71, 89, 74, - 92, 40, 42, 43, 43, 27, 64, 28, 66, 46, 26, 50, 26, 53, 83, 57, 87, 14, 75, 15, 79, 31, 45, - 34, 45, 89, 84, 92, 88, 84, 51, 85, 53, 67, 87, 67, 89, 39, 26, 43, 27, 47, 61, 47, 63, 23, - 49, 25, 53, 12, 3, 14, 5, 16, 50, 19, 53, 63, 80, 64, 84, 22, 63, 22, 64, 26, 66, 29, 66, - 2, 15, 3, 15, 74, 77, 77, 79, 64, 11, 68, 11, 38, 4, 39, 8, 83, 73, 87, 77, 85, 52, 89, - 56, 74, 60, 76, 63, 62, 66, 65, 67}; +std::vector createTestData() { + return std::vector{ + 8, 62, 11, 66, 57, 17, 57, 19, 76, 26, 79, 29, 36, 56, 38, 56, 92, 77, 96, 80, 87, 70, 90, + 74, 43, 41, 47, 43, 0, 58, 2, 62, 76, 86, 80, 89, 27, 13, 27, 15, 71, 63, 75, 67, 25, 2, + 27, 2, 87, 6, 88, 6, 22, 90, 23, 93, 22, 89, 22, 93, 57, 11, 61, 13, 61, 55, 63, 56, 17, + 85, 21, 87, 33, 43, 37, 43, 6, 1, 7, 3, 80, 87, 80, 87, 23, 50, 26, 52, 58, 89, 58, 89, + 12, 30, 15, 34, 32, 58, 36, 61, 41, 84, 44, 87, 44, 18, 44, 19, 13, 63, 15, 67, 52, 70, 54, + 74, 57, 59, 58, 59, 17, 90, 20, 92, 48, 53, 52, 56, 92, 68, 92, 72, 26, 52, 30, 52, 56, 23, + 57, 26, 88, 48, 88, 48, 66, 13, 67, 15, 7, 82, 8, 86, 46, 68, 50, 68, 37, 33, 38, 36, 6, + 15, 8, 18, 85, 36, 89, 38, 82, 45, 84, 48, 12, 2, 16, 3, 26, 15, 26, 16, 55, 23, 59, 26, + 76, 37, 79, 39, 86, 74, 90, 77, 16, 75, 18, 78, 44, 18, 45, 21, 52, 67, 54, 71, 59, 78, 62, + 78, 24, 5, 24, 8, 64, 80, 64, 83, 66, 55, 70, 55, 0, 17, 2, 19, 15, 71, 18, 74, 87, 57, + 87, 59, 6, 34, 7, 37, 34, 30, 37, 32, 51, 19, 53, 19, 72, 51, 73, 55, 29, 45, 30, 45, 94, + 94, 96, 95, 7, 22, 11, 24, 86, 45, 87, 48, 33, 62, 34, 65, 18, 10, 21, 14, 64, 66, 67, 67, + 64, 25, 65, 28, 27, 4, 31, 6, 84, 4, 85, 5, 48, 80, 50, 81, 1, 61, 3, 61, 71, 89, 74, + 92, 40, 42, 43, 43, 27, 64, 28, 66, 46, 26, 50, 26, 53, 83, 57, 87, 14, 75, 15, 79, 31, 45, + 34, 45, 89, 84, 92, 88, 84, 51, 85, 53, 67, 87, 67, 89, 39, 26, 43, 27, 47, 61, 47, 63, 23, + 49, 25, 53, 12, 3, 14, 5, 16, 50, 19, 53, 63, 80, 64, 84, 22, 63, 22, 64, 26, 66, 29, 66, + 2, 15, 3, 15, 74, 77, 77, 79, 64, 11, 68, 11, 38, 4, 39, 8, 83, 73, 87, 77, 85, 52, 89, + 56, 74, 60, 76, 63, 62, 66, 65, 67}; } static std::vector testData = createTestData(); -cavc::StaticSpatialIndex createIndex() -{ - cavc::StaticSpatialIndex index(testData.size() / 4); - for (std::size_t i = 0; i < testData.size(); i += 4) - { - index.add(testData[i], testData[i + 1], testData[i + 2], testData[i + 3]); - } - index.finish(); - return index; +cavc::StaticSpatialIndex createIndex() { + cavc::StaticSpatialIndex index(testData.size() / 4); + for (std::size_t i = 0; i < testData.size(); i += 4) { + index.add(testData[i], testData[i + 1], testData[i + 2], testData[i + 3]); + } + index.finish(); + return index; }; -static cavc::StaticSpatialIndex createSmallIndex() -{ - std::size_t numItems = 14; - cavc::StaticSpatialIndex index(numItems); - for (std::size_t i = 0; i < 4 * numItems; i += 4) - { - index.add(testData[i], testData[i + 1], testData[i + 2], testData[i + 3]); - } - index.finish(); - return index; +static cavc::StaticSpatialIndex createSmallIndex() { + std::size_t numItems = 14; + cavc::StaticSpatialIndex index(numItems); + for (std::size_t i = 0; i < 4 * numItems; i += 4) { + index.add(testData[i], testData[i + 1], testData[i + 2], testData[i + 3]); + } + index.finish(); + return index; } -struct Box -{ - double minX; - double minY; - double maxX; - double maxY; - - std::string toInitList() const - { - std::stringstream ss; - ss << "{" << minX << "," << minY << "," << maxX << "," << maxY << "}"; - return ss.str(); - } +struct Box { + double minX; + double minY; + double maxX; + double maxY; + + std::string toInitList() const { + std::stringstream ss; + ss << "{" << minX << "," << minY << "," << maxX << "," << maxY << "}"; + return ss.str(); + } }; -std::ostream &operator<<(std::ostream &os, Box const &box) -{ - os << "{" << box.minX << ", " << box.minY << ", " << box.maxX << ", " << box.maxY << "}"; - return os; +std::ostream &operator<<(std::ostream &os, Box const &box) { + os << "{" << box.minX << ", " << box.minY << ", " << box.maxX << ", " << box.maxY << "}"; + return os; } -bool operator==(Box const &left, Box const &right) -{ - return fuzzyEqual(left.minX, right.minX) && fuzzyEqual(left.minY, right.minY) && - fuzzyEqual(left.maxX, right.maxX) && fuzzyEqual(left.maxY, right.maxY); +bool operator==(Box const &left, Box const &right) { + return fuzzyEqual(left.minX, right.minX) && fuzzyEqual(left.minY, right.minY) && + fuzzyEqual(left.maxX, right.maxX) && fuzzyEqual(left.maxY, right.maxY); } -TEST(StaticSpatialIndexTests, index) -{ - auto index = createIndex(); - ASSERT_EQ(index.minX(), 0); - ASSERT_EQ(index.minY(), 1); - ASSERT_EQ(index.maxX(), 96); - ASSERT_EQ(index.maxY(), 95); - - std::vector levelBounds; - std::size_t currLevel = std::numeric_limits::max(); - std::vector> levelBoxes; - auto visitor = [&](std::size_t level, double minX, double minY, double maxX, double maxY) - { - if (currLevel != level) - { - currLevel = level; - levelBounds.push_back(1); - levelBoxes.emplace_back(); - } - else - { - levelBounds.back() += 1; - } - - levelBoxes.back().push_back({minX, minY, maxX, maxY}); - return true; - }; - - index.visitBoundingBoxes(visitor); - - ASSERT_THAT(levelBounds, t::SizeIs(3)); - - std::vector expectedLevelBounds = {1, 7, 100}; - ASSERT_THAT(levelBounds, t::ContainerEq(expectedLevelBounds)); - - ASSERT_THAT(levelBoxes[0], t::SizeIs(1)); - Box expectedRootNodeBox{0, 1, 96, 95}; - ASSERT_EQ(levelBoxes[0][0], expectedRootNodeBox); - - std::vector expectedFirstTierBoxes = {{0, 1, 45, 24}, {6, 26, 50, 67}, {0, 58, 50, 93}, - {23, 50, 70, 89}, {59, 60, 96, 95}, {51, 13, 89, 59}, - {57, 4, 88, 13}}; - ASSERT_THAT(levelBoxes[1], t::Pointwise(t::Eq(), expectedFirstTierBoxes)); - - // reverse the value boxes (they are visited in reverse order due to tree stack traversal) - std::reverse(levelBoxes[2].begin(), levelBoxes[2].end()); - - // create actual node size blocks of boxes to be compared with expected - std::vector> valueNodeBoxes; - std::size_t nodeSize = 16; - std::size_t valueNodeCount = (levelBoxes[2].size() - 1) / nodeSize + 1; - valueNodeBoxes.reserve(valueNodeCount); - - for (std::size_t k = 0; k < valueNodeCount; ++k) - { - std::size_t start = k * nodeSize; - std::size_t end = start + nodeSize; - - if (end > levelBoxes[2].size()) - { - end = levelBoxes[2].size(); - } - - valueNodeBoxes.emplace_back(); - valueNodeBoxes.back().reserve(end - start); - for (std::size_t i = start; i < end; ++i) - { - valueNodeBoxes.back().push_back(levelBoxes[2][i]); - } +TEST(StaticSpatialIndexTests, index) { + auto index = createIndex(); + ASSERT_EQ(index.minX(), 0); + ASSERT_EQ(index.minY(), 1); + ASSERT_EQ(index.maxX(), 96); + ASSERT_EQ(index.maxY(), 95); + + std::vector levelBounds; + std::size_t currLevel = std::numeric_limits::max(); + std::vector> levelBoxes; + auto visitor = [&](std::size_t level, double minX, double minY, double maxX, double maxY) { + if (currLevel != level) { + currLevel = level; + levelBounds.push_back(1); + levelBoxes.emplace_back(); + } else { + levelBounds.back() += 1; + } + + levelBoxes.back().push_back({minX, minY, maxX, maxY}); + return true; + }; + + index.visitBoundingBoxes(visitor); + + ASSERT_THAT(levelBounds, t::SizeIs(3)); + + std::vector expectedLevelBounds = {1, 7, 100}; + ASSERT_THAT(levelBounds, t::ContainerEq(expectedLevelBounds)); + + ASSERT_THAT(levelBoxes[0], t::SizeIs(1)); + Box expectedRootNodeBox{0, 1, 96, 95}; + ASSERT_EQ(levelBoxes[0][0], expectedRootNodeBox); + + std::vector expectedFirstTierBoxes = {{0, 1, 45, 24}, {6, 26, 50, 67}, {0, 58, 50, 93}, + {23, 50, 70, 89}, {59, 60, 96, 95}, {51, 13, 89, 59}, + {57, 4, 88, 13}}; + ASSERT_THAT(levelBoxes[1], t::Pointwise(t::Eq(), expectedFirstTierBoxes)); + + // reverse the value boxes (they are visited in reverse order due to tree stack traversal) + std::reverse(levelBoxes[2].begin(), levelBoxes[2].end()); + + // create actual node size blocks of boxes to be compared with expected + std::vector> valueNodeBoxes; + std::size_t nodeSize = 16; + std::size_t valueNodeCount = (levelBoxes[2].size() - 1) / nodeSize + 1; + valueNodeBoxes.reserve(valueNodeCount); + + for (std::size_t k = 0; k < valueNodeCount; ++k) { + std::size_t start = k * nodeSize; + std::size_t end = start + nodeSize; + + if (end > levelBoxes[2].size()) { + end = levelBoxes[2].size(); } - // note on all these compares are unordered (value boxes within the same node are allowed to be - // unordered and may be unordered for performance optimizations) - std::vector expectedFirstValueNodeBoxes = { - {6, 1, 7, 3}, {2, 15, 3, 15}, {0, 17, 2, 19}, {7, 22, 11, 24}, - {6, 15, 8, 18}, {18, 10, 21, 14}, {12, 3, 14, 5}, {12, 2, 16, 3}, - {24, 5, 24, 8}, {25, 2, 27, 2}, {27, 4, 31, 6}, {38, 4, 39, 8}, - {44, 18, 44, 19}, {44, 18, 45, 21}, {26, 15, 26, 16}, {27, 13, 27, 15}}; - ASSERT_THAT(valueNodeBoxes[0], t::UnorderedPointwise(t::Eq(), expectedFirstValueNodeBoxes)); - - std::vector expectedSecondValueNodeBoxes = { - {34, 30, 37, 32}, {39, 26, 43, 27}, {46, 26, 50, 26}, {37, 33, 38, 36}, - {43, 41, 47, 43}, {40, 42, 43, 43}, {31, 45, 34, 45}, {33, 43, 37, 43}, - {29, 45, 30, 45}, {12, 30, 15, 34}, {6, 34, 7, 37}, {16, 50, 19, 53}, - {23, 49, 25, 53}, {13, 63, 15, 67}, {22, 63, 22, 64}, {8, 62, 11, 66}}; - ASSERT_THAT(valueNodeBoxes[1], t::UnorderedPointwise(t::Eq(), expectedSecondValueNodeBoxes)); - - std::vector expectedThirdValueNodeBoxes = { - {1, 61, 3, 61}, {0, 58, 2, 62}, {7, 82, 8, 86}, {17, 90, 20, 92}, - {22, 90, 23, 93}, {22, 89, 22, 93}, {17, 85, 21, 87}, {16, 75, 18, 78}, - {14, 75, 15, 79}, {15, 71, 18, 74}, {41, 84, 44, 87}, {46, 68, 50, 68}, - {47, 61, 47, 63}, {26, 66, 29, 66}, {27, 64, 28, 66}, {33, 62, 34, 65}}; - ASSERT_THAT(valueNodeBoxes[2], t::UnorderedPointwise(t::Eq(), expectedThirdValueNodeBoxes)); - - std::vector expectedFourthValueNodeBoxes = { - {32, 58, 36, 61}, {26, 52, 30, 52}, {23, 50, 26, 52}, {36, 56, 38, 56}, - {48, 53, 52, 56}, {57, 59, 58, 59}, {66, 55, 70, 55}, {61, 55, 63, 56}, - {64, 66, 67, 67}, {62, 66, 65, 67}, {52, 67, 54, 71}, {52, 70, 54, 74}, - {48, 80, 50, 81}, {58, 89, 58, 89}, {53, 83, 57, 87}, {67, 87, 67, 89}}; - ASSERT_THAT(valueNodeBoxes[3], t::UnorderedPointwise(t::Eq(), expectedFourthValueNodeBoxes)); - - std::vector expectedFifthValueNodeBoxes = { - {64, 80, 64, 83}, {63, 80, 64, 84}, {59, 78, 62, 78}, {74, 77, 77, 79}, - {76, 86, 80, 89}, {71, 89, 74, 92}, {80, 87, 80, 87}, {94, 94, 96, 95}, - {89, 84, 92, 88}, {92, 77, 96, 80}, {86, 74, 90, 77}, {83, 73, 87, 77}, - {87, 70, 90, 74}, {92, 68, 92, 72}, {71, 63, 75, 67}, {74, 60, 76, 63}}; - ASSERT_THAT(valueNodeBoxes[4], t::UnorderedPointwise(t::Eq(), expectedFifthValueNodeBoxes)); - - std::vector expectedSixthValueNodeBoxes = { - {72, 51, 73, 55}, {84, 51, 85, 53}, {85, 52, 89, 56}, {87, 57, 87, 59}, - {88, 48, 88, 48}, {86, 45, 87, 48}, {85, 36, 89, 38}, {76, 26, 79, 29}, - {76, 37, 79, 39}, {82, 45, 84, 48}, {64, 25, 65, 28}, {66, 13, 67, 15}, - {55, 23, 59, 26}, {56, 23, 57, 26}, {51, 19, 53, 19}, {57, 17, 57, 19}}; - ASSERT_THAT(valueNodeBoxes[5], t::UnorderedPointwise(t::Eq(), expectedSixthValueNodeBoxes)); - - std::vector expectedSeventhValueNodeBoxes = { - {57, 11, 61, 13}, {64, 11, 68, 11}, {87, 6, 88, 6}, {84, 4, 85, 5}}; - ASSERT_THAT(valueNodeBoxes[6], t::UnorderedPointwise(t::Eq(), expectedSeventhValueNodeBoxes)); + valueNodeBoxes.emplace_back(); + valueNodeBoxes.back().reserve(end - start); + for (std::size_t i = start; i < end; ++i) { + valueNodeBoxes.back().push_back(levelBoxes[2][i]); + } + } + + // note on all these compares are unordered (value boxes within the same node are allowed to be + // unordered and may be unordered for performance optimizations) + std::vector expectedFirstValueNodeBoxes = { + {6, 1, 7, 3}, {2, 15, 3, 15}, {0, 17, 2, 19}, {7, 22, 11, 24}, + {6, 15, 8, 18}, {18, 10, 21, 14}, {12, 3, 14, 5}, {12, 2, 16, 3}, + {24, 5, 24, 8}, {25, 2, 27, 2}, {27, 4, 31, 6}, {38, 4, 39, 8}, + {44, 18, 44, 19}, {44, 18, 45, 21}, {26, 15, 26, 16}, {27, 13, 27, 15}}; + ASSERT_THAT(valueNodeBoxes[0], t::UnorderedPointwise(t::Eq(), expectedFirstValueNodeBoxes)); + + std::vector expectedSecondValueNodeBoxes = { + {34, 30, 37, 32}, {39, 26, 43, 27}, {46, 26, 50, 26}, {37, 33, 38, 36}, + {43, 41, 47, 43}, {40, 42, 43, 43}, {31, 45, 34, 45}, {33, 43, 37, 43}, + {29, 45, 30, 45}, {12, 30, 15, 34}, {6, 34, 7, 37}, {16, 50, 19, 53}, + {23, 49, 25, 53}, {13, 63, 15, 67}, {22, 63, 22, 64}, {8, 62, 11, 66}}; + ASSERT_THAT(valueNodeBoxes[1], t::UnorderedPointwise(t::Eq(), expectedSecondValueNodeBoxes)); + + std::vector expectedThirdValueNodeBoxes = { + {1, 61, 3, 61}, {0, 58, 2, 62}, {7, 82, 8, 86}, {17, 90, 20, 92}, + {22, 90, 23, 93}, {22, 89, 22, 93}, {17, 85, 21, 87}, {16, 75, 18, 78}, + {14, 75, 15, 79}, {15, 71, 18, 74}, {41, 84, 44, 87}, {46, 68, 50, 68}, + {47, 61, 47, 63}, {26, 66, 29, 66}, {27, 64, 28, 66}, {33, 62, 34, 65}}; + ASSERT_THAT(valueNodeBoxes[2], t::UnorderedPointwise(t::Eq(), expectedThirdValueNodeBoxes)); + + std::vector expectedFourthValueNodeBoxes = { + {32, 58, 36, 61}, {26, 52, 30, 52}, {23, 50, 26, 52}, {36, 56, 38, 56}, + {48, 53, 52, 56}, {57, 59, 58, 59}, {66, 55, 70, 55}, {61, 55, 63, 56}, + {64, 66, 67, 67}, {62, 66, 65, 67}, {52, 67, 54, 71}, {52, 70, 54, 74}, + {48, 80, 50, 81}, {58, 89, 58, 89}, {53, 83, 57, 87}, {67, 87, 67, 89}}; + ASSERT_THAT(valueNodeBoxes[3], t::UnorderedPointwise(t::Eq(), expectedFourthValueNodeBoxes)); + + std::vector expectedFifthValueNodeBoxes = { + {64, 80, 64, 83}, {63, 80, 64, 84}, {59, 78, 62, 78}, {74, 77, 77, 79}, + {76, 86, 80, 89}, {71, 89, 74, 92}, {80, 87, 80, 87}, {94, 94, 96, 95}, + {89, 84, 92, 88}, {92, 77, 96, 80}, {86, 74, 90, 77}, {83, 73, 87, 77}, + {87, 70, 90, 74}, {92, 68, 92, 72}, {71, 63, 75, 67}, {74, 60, 76, 63}}; + ASSERT_THAT(valueNodeBoxes[4], t::UnorderedPointwise(t::Eq(), expectedFifthValueNodeBoxes)); + + std::vector expectedSixthValueNodeBoxes = { + {72, 51, 73, 55}, {84, 51, 85, 53}, {85, 52, 89, 56}, {87, 57, 87, 59}, + {88, 48, 88, 48}, {86, 45, 87, 48}, {85, 36, 89, 38}, {76, 26, 79, 29}, + {76, 37, 79, 39}, {82, 45, 84, 48}, {64, 25, 65, 28}, {66, 13, 67, 15}, + {55, 23, 59, 26}, {56, 23, 57, 26}, {51, 19, 53, 19}, {57, 17, 57, 19}}; + ASSERT_THAT(valueNodeBoxes[5], t::UnorderedPointwise(t::Eq(), expectedSixthValueNodeBoxes)); + + std::vector expectedSeventhValueNodeBoxes = { + {57, 11, 61, 13}, {64, 11, 68, 11}, {87, 6, 88, 6}, {84, 4, 85, 5}}; + ASSERT_THAT(valueNodeBoxes[6], t::UnorderedPointwise(t::Eq(), expectedSeventhValueNodeBoxes)); } -TEST(StaticSpatialIndexTests, skip_sorting_small_index) -{ - auto index = createSmallIndex(); - - ASSERT_EQ(index.minX(), 0); - ASSERT_EQ(index.minY(), 2); - ASSERT_EQ(index.maxX(), 96); - ASSERT_EQ(index.maxY(), 93); - - std::vector levelBounds; - std::size_t currLevel = std::numeric_limits::max(); - std::vector> levelBoxes; - auto visitor = [&](std::size_t level, double minX, double minY, double maxX, double maxY) - { - if (currLevel != level) - { - currLevel = level; - levelBounds.push_back(1); - levelBoxes.emplace_back(); - } - else - { - levelBounds.back() += 1; - } - - levelBoxes.back().push_back({minX, minY, maxX, maxY}); - return true; - }; - - index.visitBoundingBoxes(visitor); - - ASSERT_THAT(levelBounds, t::SizeIs(2)); - - std::vector expectedLevelBounds = {1, 14}; - ASSERT_THAT(levelBounds, t::ContainerEq(expectedLevelBounds)); - - ASSERT_THAT(levelBoxes[0], t::SizeIs(1)); - Box expectedRootNodeBox{0, 2, 96, 93}; - ASSERT_EQ(levelBoxes[0][0], expectedRootNodeBox); - - std::vector expectedValueBoxes = { - {8, 62, 11, 66}, {57, 17, 57, 19}, {76, 26, 79, 29}, {36, 56, 38, 56}, {92, 77, 96, 80}, - {87, 70, 90, 74}, {43, 41, 47, 43}, {0, 58, 2, 62}, {76, 86, 80, 89}, {27, 13, 27, 15}, - {71, 63, 75, 67}, {25, 2, 27, 2}, {87, 6, 88, 6}, {22, 90, 23, 93}}; - - ASSERT_THAT(levelBoxes[1], t::ContainerEq(expectedValueBoxes)); +TEST(StaticSpatialIndexTests, skip_sorting_small_index) { + auto index = createSmallIndex(); + + ASSERT_EQ(index.minX(), 0); + ASSERT_EQ(index.minY(), 2); + ASSERT_EQ(index.maxX(), 96); + ASSERT_EQ(index.maxY(), 93); + + std::vector levelBounds; + std::size_t currLevel = std::numeric_limits::max(); + std::vector> levelBoxes; + auto visitor = [&](std::size_t level, double minX, double minY, double maxX, double maxY) { + if (currLevel != level) { + currLevel = level; + levelBounds.push_back(1); + levelBoxes.emplace_back(); + } else { + levelBounds.back() += 1; + } + + levelBoxes.back().push_back({minX, minY, maxX, maxY}); + return true; + }; + + index.visitBoundingBoxes(visitor); + + ASSERT_THAT(levelBounds, t::SizeIs(2)); + + std::vector expectedLevelBounds = {1, 14}; + ASSERT_THAT(levelBounds, t::ContainerEq(expectedLevelBounds)); + + ASSERT_THAT(levelBoxes[0], t::SizeIs(1)); + Box expectedRootNodeBox{0, 2, 96, 93}; + ASSERT_EQ(levelBoxes[0][0], expectedRootNodeBox); + + std::vector expectedValueBoxes = { + {8, 62, 11, 66}, {57, 17, 57, 19}, {76, 26, 79, 29}, {36, 56, 38, 56}, {92, 77, 96, 80}, + {87, 70, 90, 74}, {43, 41, 47, 43}, {0, 58, 2, 62}, {76, 86, 80, 89}, {27, 13, 27, 15}, + {71, 63, 75, 67}, {25, 2, 27, 2}, {87, 6, 88, 6}, {22, 90, 23, 93}}; + + ASSERT_THAT(levelBoxes[1], t::ContainerEq(expectedValueBoxes)); } -TEST(StaticSpatialIndexTests, query) -{ - auto index = createIndex(); - std::vector queryResults; - index.query(40, 40, 60, 60, queryResults); - std::vector expectedIndexes = {6, 29, 31, 75}; - ASSERT_THAT(queryResults, t::UnorderedPointwise(t::Eq(), expectedIndexes)); +TEST(StaticSpatialIndexTests, query) { + auto index = createIndex(); + std::vector queryResults; + index.query(40, 40, 60, 60, queryResults); + std::vector expectedIndexes = {6, 29, 31, 75}; + ASSERT_THAT(queryResults, t::UnorderedPointwise(t::Eq(), expectedIndexes)); } -TEST(StaticSpatialIndexTests, visitQuery) -{ - auto index = createIndex(); +TEST(StaticSpatialIndexTests, visitQuery) { + auto index = createIndex(); - std::vector queryResults; - auto visitor = [&](std::size_t index) - { - queryResults.push_back(index); - return true; - }; - - index.visitQuery(40, 40, 60, 60, visitor); - std::vector expectedIndexes = {6, 29, 31, 75}; - ASSERT_THAT(queryResults, t::UnorderedPointwise(t::Eq(), expectedIndexes)); + std::vector queryResults; + auto visitor = [&](std::size_t index) { + queryResults.push_back(index); + return true; + }; + + index.visitQuery(40, 40, 60, 60, visitor); + std::vector expectedIndexes = {6, 29, 31, 75}; + ASSERT_THAT(queryResults, t::UnorderedPointwise(t::Eq(), expectedIndexes)); } -TEST(StaticSpatialIndexTests, visitQuery_stops_early) -{ - auto index = createIndex(); +TEST(StaticSpatialIndexTests, visitQuery_stops_early) { + auto index = createIndex(); - std::vector queryResults; - auto visitor = [&](std::size_t index) - { - queryResults.push_back(index); - return queryResults.size() != 2; - }; - - index.visitQuery(40, 40, 60, 60, visitor); - std::vector expectedIndexes = {6, 29, 31, 75}; - ASSERT_THAT(queryResults, t::SizeIs(2)); - ASSERT_THAT(expectedIndexes, t::IsSupersetOf(queryResults)); + std::vector queryResults; + auto visitor = [&](std::size_t index) { + queryResults.push_back(index); + return queryResults.size() != 2; + }; + + index.visitQuery(40, 40, 60, 60, visitor); + std::vector expectedIndexes = {6, 29, 31, 75}; + ASSERT_THAT(queryResults, t::SizeIs(2)); + ASSERT_THAT(expectedIndexes, t::IsSupersetOf(queryResults)); } -TEST(StaticSpatialIndexTests, visitItemBoxes) -{ - auto index = createIndex(); +TEST(StaticSpatialIndexTests, visitItemBoxes) { + auto index = createIndex(); - std::vector indexes; - auto visitor = [&](std::size_t index, double, double, double, double) - { - indexes.push_back(index); - return true; - }; + std::vector indexes; + auto visitor = [&](std::size_t index, double, double, double, double) { + indexes.push_back(index); + return true; + }; - index.visitItemBoxes(visitor); + index.visitItemBoxes(visitor); - std::vector expectedIndexes; - expectedIndexes.reserve(100); - for (std::size_t i = 0; i < 100; ++i) - { - expectedIndexes.push_back(i); + std::vector expectedIndexes; + expectedIndexes.reserve(100); + for (std::size_t i = 0; i < 100; ++i) { + expectedIndexes.push_back(i); + } + + ASSERT_THAT(indexes, t::WhenSorted(t::ContainerEq(expectedIndexes))); +} + +TEST(StaticSpatialIndexTests, query_with_degenerate_global_extents) { + { + cavc::StaticSpatialIndex index(17); + for (std::size_t i = 0; i < 17; ++i) { + double y = static_cast(i); + // all boxes have the same x extents (global width is zero) + index.add(1.0, y, 1.0, y + 0.1); + } + index.finish(); + + std::vector queryResults; + index.query(0.0, -1.0, 2.0, 18.0, queryResults); + ASSERT_EQ(queryResults.size(), 17u); + } + + { + cavc::StaticSpatialIndex index(17); + for (std::size_t i = 0; i < 17; ++i) { + double x = static_cast(i); + // all boxes have the same y extents (global height is zero) + index.add(x, 2.0, x + 0.1, 2.0); } + index.finish(); - ASSERT_THAT(indexes, t::WhenSorted(t::ContainerEq(expectedIndexes))); + std::vector queryResults; + index.query(-1.0, 1.0, 18.0, 3.0, queryResults); + ASSERT_EQ(queryResults.size(), 17u); + } } -int main(int argc, char **argv) -{ - t::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); + +int main(int argc, char **argv) { + t::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/tests/tests/c_api_test_helpers.hpp b/tests/tests/c_api_test_helpers.hpp index 13e4d51..afff59c 100644 --- a/tests/tests/c_api_test_helpers.hpp +++ b/tests/tests/c_api_test_helpers.hpp @@ -99,12 +99,13 @@ inline std::ostream &operator<<(std::ostream &os, cavc_point const &p) { } // helper to just create a cavc_pline from vertexes -cavc_pline *plineFromVertexes(std::vector const &vertexes, bool isClosed) { - return cavc_pline_new(&vertexes[0], static_cast(vertexes.size()), isClosed ? 1 : 0); +inline cavc_pline *plineFromVertexes(std::vector const &vertexes, bool isClosed) { + cavc_vertex const *vertex_data = vertexes.empty() ? nullptr : &vertexes[0]; + return cavc_pline_new(vertex_data, static_cast(vertexes.size()), isClosed ? 1 : 0); } // reverses the direction of the polyline defined by vertexes -void reverseDirection(std::vector &vertexes) { +inline void reverseDirection(std::vector &vertexes) { if (vertexes.size() < 2) { return; } @@ -119,7 +120,7 @@ void reverseDirection(std::vector &vertexes) { } // create a reversed polyline from the given pline (caller must delete the created pline) -cavc_pline *createRevseredPline(cavc_pline *pline) { +inline cavc_pline *createRevseredPline(cavc_pline *pline) { uint32_t count = cavc_pline_vertex_count(pline); std::vector vertexes(count); cavc_pline_vertex_data(pline, &vertexes[0]); @@ -127,4 +128,8 @@ cavc_pline *createRevseredPline(cavc_pline *pline) { return cavc_pline_new(&vertexes[0], count, cavc_pline_is_closed(pline)); } +inline cavc_parallel_offset_options defaultParallelOffsetOptions() { + return cavc_parallel_offset_default_options(); +} + #endif // CAVC_API_TEST_HELPERS_HPP diff --git a/tests/tests/testhelpers.hpp b/tests/tests/testhelpers.hpp index b29a950..3687996 100644 --- a/tests/tests/testhelpers.hpp +++ b/tests/tests/testhelpers.hpp @@ -26,10 +26,10 @@ struct PolylineProperties { cavc_real maxX; cavc_real maxY; - PolylineProperties(std::size_t vertexCount, cavc_real area, cavc_real pathLength, cavc_real minX, - cavc_real minY, cavc_real maxX, cavc_real maxY) - : vertexCount(vertexCount), area(area), pathLength(pathLength), minX(minX), minY(minY), - maxX(maxX), maxY(maxY) {} + PolylineProperties(std::size_t p_vertexCount, cavc_real p_area, cavc_real p_pathLength, + cavc_real p_minX, cavc_real p_minY, cavc_real p_maxX, cavc_real p_maxY) + : vertexCount(p_vertexCount), area(p_area), pathLength(p_pathLength), minX(p_minX), + minY(p_minY), maxX(p_maxX), maxY(p_maxY) {} PolylineProperties(cavc_pline *pline) { vertexCount = cavc_pline_vertex_count(pline); @@ -65,7 +65,7 @@ inline std::ostream &operator<<(std::ostream &os, PolylineProperties const &p) { } // Custom function to print differences -void PrintDiff(const PolylineProperties &expected, const PolylineProperties &actual) { +inline void PrintDiff(const PolylineProperties &expected, const PolylineProperties &actual) { if (expected.vertexCount != actual.vertexCount) { std::cout << "vertexCount: expected " << expected.vertexCount << ", actual " << actual.vertexCount << "\n";