split_path_at_region_crossings does not return zero length segments

faster code for path and region intersections
This commit is contained in:
Adrian Mariano 2021-09-27 18:33:44 -04:00
parent b7c5b789b7
commit dbee0abec6
2 changed files with 105 additions and 44 deletions

View file

@ -211,33 +211,34 @@ function _path_self_intersections(path, closed=true, eps=EPSILON) =
let( let(
path = cleanup_path(path, eps=eps), path = cleanup_path(path, eps=eps),
plen = len(path) plen = len(path)
) [ )
for (i = [0:1:plen-(closed?2:3)], j=[i+2:1:plen-(closed?1:2)]) let( [
a1 = path[i], for (i = [0:1:plen-(closed?2:3)])
a2 = path[(i+1)%plen], let(
b1 = path[j], a1 = path[i],
b2 = path[(j+1)%plen], a2 = path[(i+1)%plen],
isect = maxax = max(a1.x,a2.x),
(max(a1.x, a2.x) < min(b1.x, b2.x))? undef : minax = min(a1.x,a2.x),
(min(a1.x, a2.x) > max(b1.x, b2.x))? undef : maxay = max(a1.y,a2.y),
(max(a1.y, a2.y) < min(b1.y, b2.y))? undef : minay = min(a1.y,a2.y)
(min(a1.y, a2.y) > max(b1.y, b2.y))? undef : )
for(j=[i+2:1:plen-(closed?1:2)])
let( let(
c = a1-a2, b1 = path[j],
d = b1-b2, b2 = path[(j+1)%plen],
denom = (c.x*d.y)-(c.y*d.x) isect =
) abs(denom)<eps? undef : maxax < b1.x && maxax < b2.x ||
let( minax > b1.x && minax > b2.x ||
e = a1-b1, maxay < b1.y && maxay < b2.y ||
t = ((e.x*d.y)-(e.y*d.x)) / denom, minay > b1.y && minay > b2.y
u = ((e.x*c.y)-(e.y*c.x)) / denom ? undef
) [a1+t*(a2-a1), t, u] : _general_line_intersection([a1,a2],[b1,b2])
) if ( )
(!closed || i!=0 || j!=plen-1) && if ((!closed || i!=0 || j!=plen-1)
isect != undef && && isect != undef
isect[1]>=-eps && isect[1]<=1+eps && && isect[1]>=-eps && isect[1]<=1+eps
isect[2]>=-eps && isect[2]<=1+eps && isect[2]>=-eps && isect[2]<=1+eps)
) [isect[0], i, isect[1], j, isect[2]] [isect[0], i, isect[1], j, isect[2]]
]; ];
@ -440,6 +441,14 @@ function resample_path(path, N, spacing, closed=false) =
// closed = set to true to treat path as a polygon. Default: false // closed = set to true to treat path as a polygon. Default: false
// eps = Epsilon error value used for determine if points coincide. Default: `EPSILON` (1e-9) // eps = Epsilon error value used for determine if points coincide. Default: `EPSILON` (1e-9)
function is_path_simple(path, closed=false, eps=EPSILON) = function is_path_simple(path, closed=false, eps=EPSILON) =
[for(i=[0:1:len(path)-(closed?2:3)])
let(v1=path[i+1]-path[i],
v2=select(path,i+2)-path[i+1],
normv1 = norm(v1),
normv2 = norm(v2)
)
if (/*approx(normv1,0) || approx(normv2,0) ||*/ approx(v1*v2/normv1/normv2,-1)) 1] == []
&&
_path_self_intersections(path,closed=closed,eps=eps) == []; _path_self_intersections(path,closed=closed,eps=eps) == [];
@ -1043,10 +1052,10 @@ function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) =
// polygon(path); // polygon(path);
// right(27)rainbow(polygon_parts(path)) polygon($item); // right(27)rainbow(polygon_parts(path)) polygon($item);
// move([16,-14])rainbow(polygon_parts(path,nonzero=true)) polygon($item); // move([16,-14])rainbow(polygon_parts(path,nonzero=true)) polygon($item);
function polygon_parts(path, nonzero=false, closed=true, eps=EPSILON) = function polygon_parts(path, nonzero=false, eps=EPSILON) =
let( let(
path = cleanup_path(path, eps=eps), path = cleanup_path(path, eps=eps),
tagged = _tag_self_crossing_subpaths(path, nonzero=nonzero, closed=closed, eps=eps), tagged = _tag_self_crossing_subpaths(path, nonzero=nonzero, closed=true, eps=eps),
kept = [for (sub = tagged) if(sub[0] == "O") sub[1]], kept = [for (sub = tagged) if(sub[0] == "O") sub[1]],
outregion = _assemble_path_fragments(kept, eps=eps) outregion = _assemble_path_fragments(kept, eps=eps)
) outregion; ) outregion;

View file

@ -109,7 +109,7 @@ module region(r)
// Function: point_in_region() // Function: point_in_region()
// Usage: // Usage:
// point_in_region(point, region); // check = point_in_region(point, region);
// Description: // Description:
// Tests if a point is inside, outside, or on the border of a region. // Tests if a point is inside, outside, or on the border of a region.
// Returns -1 if the point is outside the region. // Returns -1 if the point is outside the region.
@ -126,6 +126,37 @@ function point_in_region(point, region, eps=EPSILON, _i=0, _cnt=0) =
// Function: is_region_simple()
// Usage:
// bool = is_region_simple(region, [eps]);
// Description:
// Returns true if the region is entirely non-self-intersecting, meaning that it is
// formed from a list of simple polygons that do not intersect each other.
// Arguments:
// region = region to check
// eps = tolerance for geometric omparisons. Default: `EPSILON` = 1e-9
function is_region_simple(region, eps=EPSILON) =
[for(p=region) if (!is_path_simple(p)) 1] == []
&&
[for(i=[0:1:len(region)-2]) if (_path_region_intersections(region[i],[for(j=[i+1:1:len(region)-1]) region[j]]) != []) 1] ==[];
function approx_sign(x) = approx(x,0) ? 0 : sign(x);
function do_segments_intersect(s1,s2) =
let(
a1=cross(s1[1]-s1[0], s2[0]-s1[1]),
a2=cross(s1[1]-s1[0], s2[1]-s1[1]),
a3=cross(s2[1]-s2[0], s1[0]-s2[1]),
a4=cross(s2[1]-s2[0], s1[1]-s2[1])
)
approx_sign(a1)!=approx_sign(a2) && approx_sign(a3)!=approx_sign(a4);
// Note: parallel intersecting lines seem to have all the a's equal approx to zero
// Function: polygons_equal() // Function: polygons_equal()
// Usage: // Usage:
// b = polygons_equal(poly1, poly2, [eps]) // b = polygons_equal(poly1, poly2, [eps])
@ -196,28 +227,49 @@ function __regions_equal(region1, region2, i) =
__regions_equal(region1, region2, i+1); __regions_equal(region1, region2, i+1);
/// Internal Function: _region_path_crossings() /// Internal Function: _path_region_intersections()
/// Usage: /// Usage:
/// _region_path_crossings(path, region); /// _path_region_intersections(path, region);
/// Description: /// Description:
/// Returns a sorted list of [SEGMENT, U] that describe where a given path is crossed by a second path. /// Returns a sorted list of [SEGMENT, U] that describe where a given path intersects the region
// in a single point. (Note that intersections of collinear segments, where the intersection is another segment, are
// ignored.)
/// Arguments: /// Arguments:
/// path = The path to find crossings on. /// path = The path to find crossings on.
/// region = Region to test for crossings of. /// region = Region to test for crossings of.
/// closed = If true, treat path as a closed polygon. Default: true /// closed = If true, treat path as a closed polygon. Default: true
/// eps = Acceptable variance. Default: `EPSILON` (1e-9) /// eps = Acceptable variance. Default: `EPSILON` (1e-9)
function _region_path_crossings(path, region, closed=true, eps=EPSILON) = function _path_region_intersections(path, region, closed=true, eps=EPSILON) =
let( let(
segs = pair(closed? close_path(path) : cleanup_path(path)) segs = pair(closed? close_path(path) : cleanup_path(path))
) )
sort([for (si = idx(segs), p = close_region(region), s2 = pair(p)) sort(
let ( [for(si = idx(segs))
isect = _general_line_intersection(segs[si], s2, eps=eps) let(
) a1 = segs[si][0],
if (!is_undef(isect[0]) && isect[1] >= 0-eps && isect[1] < 1+eps a2 = segs[si][1],
&& isect[2] >= 0-eps && isect[2] < 1+eps ) maxax = max(a1.x,a2.x),
[si, isect[1]] minax = min(a1.x,a2.x),
]); maxay = max(a1.y,a2.y),
minay = min(a1.y,a2.y)
)
for(p=close_region(region), s2=pair(p))
let(
b1 = s2[0],
b2 = s2[1],
isect =
maxax < b1.x && maxax < b2.x ||
minax > b1.x && minax > b2.x ||
maxay < b1.y && maxay < b2.y ||
minay > b1.y && minay > b2.y
? undef
: _general_line_intersection([a1,a2],[b1,b2],eps)
)
if (isect && isect[1]>=-eps && isect[1]<=1+eps
&& isect[2]>=-eps && isect[2]<=1+eps)
[si,isect[1]]
]
);
// Function: split_path_at_region_crossings() // Function: split_path_at_region_crossings()
@ -241,7 +293,7 @@ function split_path_at_region_crossings(path, region, closed=true, eps=EPSILON)
let( let(
path = deduplicate(path, eps=eps), path = deduplicate(path, eps=eps),
region = [for (path=region) deduplicate(path, eps=eps)], region = [for (path=region) deduplicate(path, eps=eps)],
xings = _region_path_crossings(path, region, closed=closed, eps=eps), xings = _path_region_intersections(path, region, closed=closed, eps=eps),
crossings = deduplicate( crossings = deduplicate(
concat([[0,0]], xings, [[len(path)-1,1]]), concat([[0,0]], xings, [[len(path)-1,1]]),
eps=eps eps=eps
@ -254,7 +306,7 @@ function split_path_at_region_crossings(path, region, closed=true, eps=EPSILON)
) )
] ]
) )
subpaths; [for(s=subpaths) if (len(s)>1) s];
// Function: split_nested_region() // Function: split_nested_region()
@ -986,7 +1038,7 @@ function intersection(regions=[],b=undef,c=undef,eps=EPSILON) =
// circle(d=40); // circle(d=40);
// } // }
function exclusive_or(regions=[],b=undef,c=undef,eps=EPSILON) = function exclusive_or(regions=[],b=undef,c=undef,eps=EPSILON) =
b!=undef? exclusive_or(concat([regions],[b],c==undef?[]:[c]),eps=eps) : b!=undef? exclusive_or([regions, b, if(is_def(c)) c],eps=eps) :
len(regions)<=1? regions[0] : len(regions)<=1? regions[0] :
exclusive_or( exclusive_or(
let(regions=[for (r=regions) is_path(r)? [r] : r]) let(regions=[for (r=regions) is_path(r)? [r] : r])