diff --git a/comparisons.scad b/comparisons.scad index bd2a25c..9e33734 100644 --- a/comparisons.scad +++ b/comparisons.scad @@ -12,7 +12,8 @@ // Usage: // test = approx(a, b, [eps]) // Description: -// Compares two numbers or vectors, and returns true if they are closer than `eps` to each other. +// Compares two numbers, vectors, or matrices. Returns true if they are closer than `eps` to each other. +// Results are undefined if `a` and `b` are of different types, or if vectors or matrices contain non-numbers. // Arguments: // a = First value. // b = Second value. @@ -21,12 +22,22 @@ // test1 = approx(-0.3333333333,-1/3); // Returns: true // test2 = approx(0.3333333333,1/3); // Returns: true // test3 = approx(0.3333,1/3); // Returns: false -// test4 = approx(0.3333,1/3,eps=1e-3); // Returns: true +// test4 = approx(0.3333,1/3,eps=1e-3); // Returns: true // test5 = approx(PI,3.1415926536); // Returns: true +// test6 = approx([0,0,sin(45)],[0,0,sqrt(2)/2]); // Returns: true function approx(a,b,eps=EPSILON) = - (a==b && is_bool(a) == is_bool(b)) || - (is_num(a) && is_num(b) && abs(a-b) <= eps) || - (is_list(a) && is_list(b) && len(a) == len(b) && [] == [for (i=idx(a)) if (!approx(a[i],b[i],eps=eps)) 1]); + a == b? is_bool(a) == is_bool(b) : + is_num(a) && is_num(b)? abs(a-b) <= eps : + is_list(a) && is_list(b) && len(a) == len(b)? ( + [] == [ + for (i=idx(a)) + let(aa=a[i], bb=b[i]) + if( + is_num(aa) && is_num(bb)? abs(aa-bb) > eps : + !approx(aa,bb,eps=eps) + ) 1 + ] + ) : false; // Function: all_zero() @@ -290,12 +301,12 @@ function compare_lists(a, b) = // idx = find_approx(val, list, [start=], [eps=]); // indices = find_approx(val, list, all=true, [start=], [eps=]); // Description: -// Finds the first item in `list` that matches `val`, returning the index. +// Finds the first item in `list` that matches `val`, returning the index. Returns `undef` if there is no match. // Arguments: // val = The value to search for. If given a function literal of signature `function (x)`, uses that function to check list items. Returns true for a match. // list = The list to search through. // --- -// start = The index to start searching from. +// start = The index to start searching from. Default: 0 // all = If true, returns a list of all matching item indices. // eps = The maximum allowed floating point rounding error for numeric comparisons. function find_approx(val, list, start=0, all=false, eps=EPSILON) = @@ -778,3 +789,4 @@ function list_smallest(list, k) = let( bigger = [for(li=list) if(li>v) li ] ) concat(smaller, equal, list_smallest(bigger, k-len(smaller) -len(equal))); +// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/geometry.scad b/geometry.scad index 4b134db..107cf03 100644 --- a/geometry.scad +++ b/geometry.scad @@ -38,14 +38,10 @@ function _is_point_on_line(point, line, bounded=false, eps=EPSILON) = t = v0*v1/(v1*v1), bounded = force_list(bounded,2) ) - abs(cross(v0,v1))=-eps) && (!bounded[1] || t<1+eps) ; -function xis_point_on_line(point, line, bounded=false, eps=EPSILON) = - assert( is_finite(eps) && (eps>=0), "The tolerance should be a non-negative value." ) - point_line_distance(point, line, bounded)=3, "Polygon `poly` should be a list of at least three 2d or 3d points") - assert(is_undef(ind) - || (is_vector(ind) && min(ind)>=0 && max(ind)=0 && max(ind) 2*eps, - "The polygon vertices are collinear.") - [ind] + let( degen = norm(scalar_vec3(cross(poly[ind[1]]-poly[ind[0]], poly[ind[2]]-poly[ind[0]]))) < 2*eps ) + assert( ! error || ! degen, "The polygon vertices are collinear.") + degen ? undef : [ind] : len(poly[ind[0]]) == 3 - ? // represents the polygon projection on its plane as a 2d polygon + ? // find a representation of the polygon as a 2d polygon by projecting it on its own plane let( ind = deduplicate_indexed(poly, ind, eps) ) len(ind)<3 ? [] : let( pts = select(poly,ind), - nrm = polygon_normal(pts) + nrm = -polygon_normal(pts) ) - assert( nrm!=undef, - "The polygon has self-intersections or its vertices are collinear or non coplanar.") + assert( ! error || (nrm != undef), + "The polygon has self-intersections or zero area or its vertices are collinear or non coplanar.") + nrm == undef ? undef : let( imax = max_index([for(p=pts) norm(p-pts[0]) ]), v1 = unit( pts[imax] - pts[0] ), v2 = cross(v1,nrm), - prpts = pts*transpose([v1,v2]) + prpts = pts*transpose([v1,v2]) // the 2d projection of pts on the polygon plane ) - [for(tri=_triangulate(prpts, count(len(ind)), eps)) select(ind,tri) ] - : let( cw = is_polygon_clockwise(select(poly, ind)) ) - cw - ? [for(tri=_triangulate( poly, reverse(ind), eps )) reverse(tri) ] - : _triangulate( poly, ind, eps ); + let( tris = _triangulate(prpts, count(len(ind)), error, eps) ) + tris == undef ? undef : + [for(tri=tris) select(ind,tri) ] + : is_polygon_clockwise(select(poly, ind)) + ? _triangulate( poly, ind, error, eps ) + : let( tris = _triangulate( poly, reverse(ind), error, eps ) ) + tris == undef ? undef : + [for(tri=tris) reverse(tri) ]; -function _triangulate(poly, ind, eps=EPSILON, tris=[]) = +// poly is supposed to be a 2d cw polygon +// implements a modified version of ear cut method for non-twisted polygons +// the polygons accepted by this function are those decomposable in simple +// CW polygons. +function _triangulate(poly, ind, error, eps=EPSILON, tris=[]) = len(ind)==3 - ? _is_degenerate(select(poly,ind),eps) - ? tris // last 3 pts perform a degenerate triangle, ignore it + ? _degenerate_tri(select(poly,ind),eps) + ? tris // if last 3 pts perform a degenerate triangle, ignore it : concat(tris,[ind]) // otherwise, include it : let( ear = _get_ear(poly,ind,eps) ) - assert( ear!=undef, - "The polygon has self-intersections or its vertices are collinear or non coplanar.") - is_list(ear) // degenerate ear - ? _triangulate(poly, select(ind,ear[0]+2, ear[0]), eps, tris) // discard it + assert( ! error || (ear != undef), + "The polygon has twists or all its vertices are collinear or non coplanar.") + ear == undef ? undef : + is_list(ear) // is it a degenerate ear ? + ? len(ind) <= 4 ? tris : + _triangulate(poly, select(ind,ear[0]+3, ear[0]), error, eps, tris) // discard it : let( ear_tri = select(ind,ear,ear+2), - indr = select(ind,ear+2, ear) // remaining point indices + indr = select(ind,ear+2, ear) // indices of the remaining path ) - _triangulate(poly, indr, eps, concat(tris,[ear_tri])); + _triangulate(poly, indr, error, eps, concat(tris,[ear_tri])); // a returned ear will be: -// 1. a CCW (non-degenerate) triangle, made of subsequent vertices, without other -// points inside except possibly at its vertices +// 1. a CW non-reflex triangle, made of subsequent poly vertices, without any other +// poly points inside except possibly at its own vertices // 2. or a degenerate triangle where two vertices are coincident // the returned ear is specified by the index of `ind` of its first vertex -function _get_ear(poly, ind, eps, _i=0) = - _i>=len(ind) ? undef : // poly has no ears +function _get_ear(poly, ind, eps, _i=0) = + let( lind = len(ind) ) + lind==3 ? 0 : let( // the _i-th ear candidate p0 = poly[ind[_i]], - p1 = poly[ind[(_i+1)%len(ind)]], - p2 = poly[ind[(_i+2)%len(ind)]] + p1 = poly[ind[(_i+1)%lind]], + p2 = poly[ind[(_i+2)%lind]] ) - // degenerate triangles are returned codified - _is_degenerate([p0,p1,p2],eps) ? [_i] : - // if it is not a convex vertex, check the next one - _is_cw2(p0,p1,p2,eps) ? _get_ear(poly,ind,eps, _i=_i+1) : - let( // vertex p1 is convex - // check if the triangle contains any other point - // except possibly its own vertices - to_tst = select(ind,_i+3, _i-1), - q = [(p0-p2).y, (p2-p0).x], // orthogonal to ray [p0,p2] pointing right - r = [(p2-p1).y, (p1-p2).x], // orthogonal to ray [p2,p1] pointing right - s = [(p1-p0).y, (p0-p1).x], // orthogonal to ray [p1,p0] pointing right - inside = [for(p=select(poly,to_tst)) // for vertices other than p0, p1 and p2 - if( (p-p0)*q<=0 && (p-p2)*r<=0 && (p-p1)*s<=0 // p is on the triangle - && norm(p-p0)>eps // but not on any vertex of it - && norm(p-p1)>eps - && norm(p-p2)>eps ) - p ] + // if vertex p1 is a convex candidate to be an ear, + // check if the triangle [p0,p1,p2] contains any other point + // except possibly p0 and p2 + // exclude the ear candidate central vertex p1 from the verts to check + _tri_class([p0,p1,p2],eps) > 0 + && _none_inside(select(ind,_i+2, _i),poly,p0,p1,p2,eps) ? _i : // found an ear + // otherwise check the next ear candidate + _i=len(idxs) ? true : + let( + vert = poly[idxs[i]], + prev_vert = poly[select(idxs,i-1)], + next_vert = poly[select(idxs,i+1)] ) - inside==[] ? _i : // found an ear - // check the next ear candidate - _get_ear(poly, ind, eps, _i=_i+1); - - -// true for some specific kinds of degeneracy -function _is_degenerate(tri,eps) = - norm(tri[0]-tri[1])0 && + _tri_class([p1,p2,vert],eps)>0 && + _tri_class([p2,p0,vert],eps)>=0 ) + // or it is equal to p1 and some of its adjacent edges cross the open segment (p0,p2) + || ( norm(vert-p1) < eps + && _is_at_left(p0,[prev_vert,p1],eps) && _is_at_left(p2,[p1,prev_vert],eps) + && _is_at_left(p2,[p1,next_vert],eps) && _is_at_left(p0,[next_vert,p1],eps) + ) + ) + ? false + : _none_inside(idxs,poly,p0,p1,p2,eps,i=i+1); // Function: is_polygon_clockwise() diff --git a/lists.scad b/lists.scad index 9da1972..0720545 100644 --- a/lists.scad +++ b/lists.scad @@ -230,14 +230,15 @@ function select(list, start, end) = : end==undef ? is_num(start) ? list[ (start%l+l)%l ] - : assert( is_list(start) || is_range(start), "Invalid start parameter") + : assert( start==[] || is_vector(start) || is_range(start), "Invalid start parameter") [for (i=start) list[ (i%l+l)%l ] ] : assert(is_finite(start), "When `end` is given, `start` parameter should be a number.") assert(is_finite(end), "Invalid end parameter.") let( s = (start%l+l)%l, e = (end%l+l)%l ) (s <= e) - ? [for (i = [s:1:e]) list[i]] - : concat([for (i = [s:1:l-1]) list[i]], [for (i = [0:1:e]) list[i]]) ; + ? [ for (i = [s:1:e]) list[i] ] + : [ for (i = [s:1:l-1]) list[i], + for (i = [0:1:e]) list[i] ] ; // Function: slice() diff --git a/paths.scad b/paths.scad index 44e09d7..3b8cfd7 100644 --- a/paths.scad +++ b/paths.scad @@ -278,9 +278,8 @@ function _path_self_intersections(path, closed=true, eps=EPSILON) = // signs at its two vertices can have an intersection with segment // [a1,a2]. The variable signals is zero when abs(vals[j]-ref) is less than // eps and the sign of vals[j]-ref otherwise. - signals = [for(j=[i+2:1:plen-(i==0 && closed? 2: 1)]) vals[j]-ref > eps ? 1 - : vals[j]-ref < -eps ? -1 - : 0] + signals = [for(j=[i+2:1:plen-(i==0 && closed? 2: 1)]) + abs(vals[j]-ref) < eps ? 0 : sign(vals[j]-ref) ] ) if(max(signals)>=0 && min(signals)<=0 ) // some remaining edge intersects line [a1,a2] for(j=[i+2:1:plen-(i==0 && closed? 3: 2)]) diff --git a/regions.scad b/regions.scad index 03491f6..f4c2990 100644 --- a/regions.scad +++ b/regions.scad @@ -429,9 +429,9 @@ function _region_region_intersections(region1, region2, closed1=true,closed2=tru for(p2=idx(region2)) let( poly = closed2?close_path(region2[p2]):region2[p2], - signs = [for(v=poly*seg_normal) v-ref> eps ? 1 : v-ref<-eps ? -1 : 0] + signs = [for(v=poly*seg_normal) abs(v-ref) < eps ? 0 : sign(v-ref) ] ) - if(max(signs)>=0 && min(signs)<=0) // some edge edge intersects line [a1,a2] + if(max(signs)>=0 && min(signs)<=0) // some edge intersects line [a1,a2] for(j=[0:1:len(poly)-2]) if(signs[j]!=signs[j+1]) let( // exclude non-crossing and collinear segments diff --git a/tests/test_all.scad b/tests/test_all.scad new file mode 100644 index 0000000..e51434a --- /dev/null +++ b/tests/test_all.scad @@ -0,0 +1,32 @@ +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include +include diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index a751f9c..4099372 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -85,14 +85,14 @@ module test_polygon_triangulate() { poly1 = [ [-10,0,-10], [10,0,10], [0,10,0], [-10,0,-10], [-4,4,-4], [4,4,4], [0,2,0], [-4,4,-4] ]; poly2 = [ [0,0], [5,5], [-5,5], [0,0], [-5,-5], [5,-5] ]; poly3 = [ [0,0], [10,0], [10,10], [10,13], [10,10], [0,10], [0,0], [3,3], [7,3], [7,7], [7,3], [3,3] ]; - tris0 = sort(polygon_triangulate(poly0)); + tris0 = (polygon_triangulate(poly0)); assert(approx(tris0, [[0, 1, 2]])); tris1 = (polygon_triangulate(poly1)); - assert(approx(tris1,( [[2, 3, 4], [6, 7, 0], [2, 4, 5], [6, 0, 1], [1, 2, 5], [5, 6, 1]]))); + assert(approx(tris1,( [[2, 3, 4], [6, 7, 0], [2, 4, 5], [6, 0, 1], [1, 2, 5], [5, 6, 1]]))); tris2 = (polygon_triangulate(poly2)); - assert(approx(tris2,([[0, 1, 2], [3, 4, 5]]))); + assert(approx(tris2,( [[3, 4, 5], [1, 2, 3]]))); tris3 = (polygon_triangulate(poly3)); - assert(approx(tris3,( [[5, 6, 7], [7, 8, 9], [10, 11, 0], [5, 7, 9], [10, 0, 1], [4, 5, 9], [9, 10, 1], [1, 4, 9]]))); + assert(approx(tris3,( [[5, 6, 7], [11, 0, 1], [5, 7, 8], [10, 11, 1], [5, 8, 9], [10, 1, 2], [4, 5, 9], [9, 10, 2]]))); } module test__normalize_plane(){ diff --git a/vectors.scad b/vectors.scad index 7343941..687f0d4 100644 --- a/vectors.scad +++ b/vectors.scad @@ -491,12 +491,11 @@ function _bt_tree(points, ind, leafsize=25) = bounds = pointlist_bounds(select(points,ind)), coord = max_index(bounds[1]-bounds[0]), projc = [for(i=ind) points[i][coord] ], - pmc = mean(projc), - pivot = min_index([for(p=projc) abs(p-pmc)]), + meanpr = mean(projc), + pivot = min_index([for(p=projc) abs(p-meanpr)]), radius = max([for(i=ind) norm(points[ind[pivot]]-points[i]) ]), - median = median(projc), - Lind = [for(i=idx(ind)) if(projc[i]<=median && i!=pivot) ind[i] ], - Rind = [for(i=idx(ind)) if(projc[i] >median && i!=pivot) ind[i] ] + Lind = [for(i=idx(ind)) if(projc[i]<=meanpr && i!=pivot) ind[i] ], + Rind = [for(i=idx(ind)) if(projc[i] >meanpr && i!=pivot) ind[i] ] ) [ ind[pivot], radius, _bt_tree(points, Lind, leafsize), _bt_tree(points, Rind, leafsize) ]; diff --git a/vnf.scad b/vnf.scad index 61d329d..0e7684f 100644 --- a/vnf.scad +++ b/vnf.scad @@ -318,14 +318,13 @@ function vnf_merge(vnfs, cleanup=false, eps=EPSILON) = cleanup? _vnf_cleanup(verts,faces,eps) : [verts,faces]; - function _vnf_cleanup(verts,faces,eps) = let( dedup = vector_search(verts,eps,verts), // collect vertex duplicates map = [for(i=idx(verts)) min(dedup[i]) ], // remap duplic vertices offset = cumsum([for(i=idx(verts)) map[i]==i ? 0 : 1 ]), // remaping face vertex offsets map2 = list(idx(verts))-offset, // map old vertex indices to new indices - nverts = [for(i=idx(verts)) if(map[i]==i) verts[i] ], // eliminates all unreferenced vertices + nverts = [for(i=idx(verts)) if(map[i]==i) verts[i] ], // this doesn't eliminate unreferenced vertices nfaces = [ for(face=faces) let( @@ -385,35 +384,124 @@ function _join_paths_at_vertices(path1,path2,v1,v2) = ]; -// Given a region that is connected and has its outer border in region[0], -// produces a polygon with the same points that has overlapping connected paths -// to join internal holes to the outer border. Output is a single path. -function _cleave_connected_region(region) = - len(region)==0? [] : - len(region)<=1? clockwise_polygon(region[0]) : - let( - dists = [ - for (i=[1:1:len(region)-1]) - _path_path_closest_vertices(region[0],region[i]) - ], - idxi = min_index(column(dists,0)), - newoline = _join_paths_at_vertices( - region[0], region[idxi+1], - dists[idxi][1], dists[idxi][2] - ) - ) len(region)==2? clockwise_polygon(newoline) : - let( - orgn = [ - newoline, - for (i=idx(region)) - if (i>0 && i!=idxi+1) - region[i] - ] - ) - assert(len(orgn) pt.y+eps) + && (edge[1].y <= pt.y) + && _is_at_left(pt, [edge[1], edge[0]], eps) ) + [ i, + // the point of edge with ordinate pt.y + abs(pt.y-edge[1].y) pt.y then pt!=vert0 + norm(pt-isect) < eps ? undef : // if pt touches the middle of an outer edge -> error + let( + // the edge [vert0, vert1] necessarily satisfies vert0.y > vert1.y + // indices of candidates to an outer bridge point + cand = + (vert0.x > pt.x) + ? [ proj[0], + // select reflex vertices inside of the triangle [pt, vert0, isect] + for(i=idx(outer)) + if( _tri_class(select(outer,i-1,i+1),eps) <= 0 + && _pt_in_tri(outer[i], [pt, vert0, isect], eps)>=0 ) + i + ] + : [ (proj[0]+1)%l, + // select reflex vertices inside of the triangle [pt, isect, vert1] + for(i=idx(outer)) + if( _tri_class(select(outer,i-1,i+1),eps) <= 0 + && _pt_in_tri(outer[i], [pt, isect, vert1], eps)>=0 ) + i + ], + // choose the candidate outer[i] such that the line [pt, outer[i]] has minimum slope + // among those with minimum slope choose the nearest to pt + slopes = [for(i=cand) 1-abs(outer[i].x-pt.x)/norm(outer[i]-pt) ], + min_slp = min(slopes), + cand2 = [for(i=idx(cand)) if(slopes[i]<=min_slp+eps) cand[i] ], + nearest = min_index([for(i=cand2) norm(pt-outer[i]) ]) + ) + cand2[nearest]; + // Function: vnf_from_region() // Usage: @@ -436,13 +524,15 @@ function _cleave_connected_region(region) = function vnf_from_region(region, transform, reverse=false) = let ( regions = region_parts(force_region(region)), - vnfs = [ - for (rgn = regions) let( - cleaved = path3d(_cleave_connected_region(rgn)), - face = is_undef(transform)? cleaved : apply(transform,cleaved), - faceidxs = reverse? [for (i=[len(face)-1:-1:0]) i] : [for (i=[0:1:len(face)-1]) i] - ) [face, [faceidxs]] - ], + vnfs = + [ for (rgn = regions) + let( cleaved = path3d(_cleave_connected_region(rgn)) ) + assert( cleaved, "The region is invalid") + let( + face = is_undef(transform)? cleaved : apply(transform,cleaved), + faceidxs = reverse? [for (i=[len(face)-1:-1:0]) i] : [for (i=[0:1:len(face)-1]) i] + ) [face, [faceidxs]] + ], outvnf = vnf_merge(vnfs) ) vnf_triangulate(outvnf); @@ -550,9 +640,13 @@ function _link_indicator(l,imin,imax) = function vnf_triangulate(vnf) = let( verts = vnf[0], - faces = [for (face=vnf[1]) each len(face)==3 ? [face] : - polygon_triangulate(verts, face)] - ) [verts, faces]; + faces = [for (face=vnf[1]) + each (len(face)==3 ? [face] : + let( tris = polygon_triangulate(verts, face) ) + assert( tris!=undef, "Some `vnf` face cannot be triangulated.") + tris ) ] + ) + [verts, faces];