Added subdivide_paths, reindex_polygon and align_polygon.

This commit is contained in:
Adrian Mariano 2019-11-19 17:42:11 -05:00
parent 9f92fe8775
commit 4ea33f9bf3
2 changed files with 173 additions and 0 deletions

View file

@ -943,6 +943,81 @@ function polygon_shift_to_closest_point(path, pt) =
) select(path,segnum,segnum+len(path)-1);
// Function: reindex_polygon(reference, poly)
//
// Description:
// Rotates the point order and possibly reverses the point order of a polygon path to optimize its pairwise its
// point association with a reference polygon. The two polygons must have the same number of vertices.
// The optimization is done by computing the distance, norm(reference[i]-poly[i]), between corresponding pairs of
// vertices of the two polygons and choosing the polygon point order that makes the total sum over all pairs as
// small as possible. Returns the reindexed polygon. Note that the geometry of the polygon is not changed by
// this operation, just the labeling of its vertices. If the input polygon is oriented opposite
// the reference then its point order is flipped.
//
// Arguments:
// reference = reference polygon path
// poly = input polygon to reindex
//
// Example(2D): The red dots show the 0th entry in the two input path lists. Note that the red dots are not near each other. The blue dot shows the 0th entry in the output polygon
// pent = subdivide_path([for(i=[0:4])[sin(72*i),cos(72*i)]],30);
// circ = circle($fn=30,r=2.2);
// reindexed = reindex_polygon(circ,pent);
// place_copies(concat(circ,pent)) circle(r=.1,$fn=32);
// color("red") place_copies([pent[0],circ[0]]) circle(r=.1,$fn=32);
// color("blue") translate(reindexed[0])circle(r=.1,$fn=32);
// Example(2D): The indexing that minimizes the total distance will not necessarily associate the nearest point of `poly` with the reference, as in this example where again the blue dot indicates the 0th entry in the reindexed result.
// pent = move([3.5,-1],p=subdivide_path([for(i=[0:4])[sin(72*i),cos(72*i)]],30));
// circ = circle($fn=30,r=2.2);
// reindexed = reindex_polygon(circ,pent);
// place_copies(concat(circ,pent)) circle(r=.1,$fn=32);
// color("red") place_copies([pent[0],circ[0]]) circle(r=.1,$fn=32);
// color("blue") translate(reindexed[0])circle(r=.1,$fn=32);
function reindex_polygon(reference, poly, return_error=false) =
assert(is_path(reference) && is_path(poly))
assert(len(reference)==len(poly), "Polygons must be the same length in reindex_polygon")
let(
N = len(reference),
fixpoly = polygon_is_clockwise(reference) ? clockwise_polygon(poly) : ccw_polygon(poly),
dist = [for (p1=reference) [for (p2=fixpoly) norm(p1-p2)]], // Matrix of all pairwise distances
// Compute the sum of all distance pairs for a each shift
sums = [for(shift=[0:N-1])
sum([for(i=[0:N-1]) dist[i][(i+shift)%N]])],
optimal_poly = polygon_shift(fixpoly,min_index(sums))
)
return_error ? [optimal_poly, min(sums)] : optimal_poly;
// Function: align_polygon(reference, poly, angles, [cp])
//
// Description:
// Tries the list or range of angles to find a rotation of the specified polygon that best aligns
// with the reference polygon. For each angle, the polygon is reindexed, which is a costly operation
// so if run time is a problem, use a smaller sampling of angles. Returns the rotated and reindexed
// polygon.
//
// Arguments:
// reference = reference polygon
// poly = polygon to rotate into alignment with the reference
// angles = list or range of angles to test
// cp = centerpoint for rotations
//
// Example(2D): The original hexagon in yellow is not well aligned with the pentagon. Turning it so the faces line up gives an optimal alignment, shown in red.
// $fn=32;
// pentagon = subdivide_path(pentagon(side=2),60);
// hexagon = subdivide_path(hexagon(side=2.7),60);
// color("red")place_copies(scale(1.4,p=align_polygon(pentagon,hexagon,[0:10:359],cp=[1,1])))circle(r=.1);
// place_copies(concat(pentagon,hexagon))circle(r=.1);
function align_polygon(reference, poly, angles, cp) =
assert(is_path(reference) && is_path(poly))
assert(len(reference)==len(poly), "Polygons must be the same length to be aligned in align_polygon")
assert(is_num(angles[0]), "The `angle` parameter to align_polygon must be a range or vector")
let( // alignments is a vector of entries of the form: [polygon, error]
alignments = [for(angle=angles) reindex_polygon(reference, zrot(angle,p=poly,cp=cp),return_error=true)],
best = min_index(subindex(alignments,1))
)
alignments[best][0];
// Function: first_noncollinear()
// Usage:
// first_noncollinear(i1, i2, points);

View file

@ -687,5 +687,103 @@ function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) =
) nextdir
];
// Input `data` is a list that sums to an integer.
// Returns rounded version of input data so that every
// entry is rounded to an integer and the sum is the same as
// that of the input. Works by rounding an entry in the list
// and passing the rounding error forward to the next entry.
// This will generally distribute the error in a uniform manner.
function _sum_preserving_round(data, index=0) =
index == len(data)-1 ? list_set(data, len(data)-1, round(data[len(data)-1])) :
let(
newval = round(data[index]),
error = newval - data[index]
)
_sum_preserving_round(list_set(data, [index,index+1], [newval, data[index+1]-error]), index+1);
// Function: subdivide_path(path, N, method)
//
// Description:
// Takes a path as input (closed or open) and subdivides the path to produce a more
// finely sampled path. The new points can be distributed proportional to length
// (`method="length"`) or they can be divided up evenly among all the path segments
// (`method="segment"`). If the extra points don't fit evenly on the path then the
// algorithm attempts to distribute them uniformly. The `exact` option requires that
// the final length is exactly as requested. If you set it to `false` then the
// algorithm will favor uniformity and the output path may have a different number of
// points due to rounding error.
//
// With the `"segment"` method you can also specify a vector of lengths. This vector,
// `N` specfies the desired point count on each segment: with vector input, `subdivide_path`
// attempts to place `N[i]-1` points on segment `i`. The reason for the -1 is to avoid
// double counting the endpoints, which are shared by pairs of segments, so that for
// a closed polygon the total number of points will be sum(N). Note that with an open
// path there is an extra point at the end, so the number of points will be sum(N)+1.
//
// Arguments:
// path = path to subdivide
// N = scalar total number of points desired or with `method="segment"` can be a vector requesting `N[i]-1` points on segment i.
// closed = set to false if the path is open. Default: True
// exact = if true return exactly the requested number of points, possibly sacrificing uniformity. If false, return uniform point sample that may not match the number of points requested. Default: True
//
// Example(2D):
// mypath = subdivide_path(square([2,2],center=true), 12);
// place_copies(mypath)circle(r=.1,$fn=32);
// Example(2D):
// mypath = subdivide_path(square([8,2],center=true), 12);
// place_copies(mypath)circle(r=.1,$fn=32);
// Example(2D):
// mypath = subdivide_path(square([8,2],center=true), 12, method="segment");
// place_copies(mypath)circle(r=.1,$fn=32);
// Example(2D):
// mypath = subdivide_path(square([2,2],center=true), 17, closed=false);
// place_copies(mypath)circle(r=.1,$fn=32);
// Example(2D): Specifying different numbers of points on each segment
// mypath = subdivide_path(hexagon(side=2), [2,3,4,5,6,7], method="segment");
// place_copies(mypath)circle(r=.1,$fn=32);
// Example(2D): Requested point total is 14 but 15 points output due to extra end point
// mypath = subdivide_path(pentagon(side=2), [3,4,3,4], method="segment", closed=false);
// place_copies(mypath)circle(r=.1,$fn=32);
// Example(2D): Since 17 is not divisible by 5, a completely uniform distribution is not possible.
// mypath = subdivide_path(pentagon(side=2), 17);
// place_copies(mypath)circle(r=.1,$fn=32);
// Example(2D): With `exact=false` a uniform distribution, but only 15 points
// mypath = subdivide_path(pentagon(side=2), 17, exact=false);
// place_copies(mypath)circle(r=.1,$fn=32);
// Example(2D): With `exact=false` you can also get extra points, here 20 instead of requested 18
// mypath = subdivide_path(pentagon(side=2), 18, exact=false);
// place_copies(mypath)circle(r=.1,$fn=32);
// Example(FlatSpin): Three-dimensional paths also work
// mypath = subdivide_path([[0,0,0],[2,0,1],[2,3,2]], 12);
// place_copies(mypath)sphere(r=.1,$fn=32);
function subdivide_path(path, N, closed=true, exact=true, method="length") =
assert(is_path(path))
assert(method=="length" || method=="segment")
assert((is_num(N) && N>0) || is_vector(N),"Parameter N to subdivide_path must be postive number or vector")
let(
count = len(path) - (closed?0:1),
add_guess =
method=="segment" ?
(is_list(N) ? assert(len(N)==count,"Vector parameter N to subdivide_path has the wrong length")
add_scalar(N,-1)
: replist((N-len(path)) / count, count))
: // method=="length"
assert(is_num(N),"Parameter N to subdivide path must be a number when method=\"length\"")
let(
path_lens = concat([for (i = [0:1:len(path)-2]) norm(path[i+1]-path[i])],
closed?[norm(path[len(path)-1]-path[0])]:[]),
add_density = (N - len(path)) / sum(path_lens)
)
path_lens * add_density,
add = exact ? _sum_preserving_round(add_guess) : [for (val=add_guess) round(val)]
)
concat(
[for (i=[0:1:count])
each [for(j=[0:1:add[i]]) lerp(path[i],select(path,i+1), j/(add[i]+1))]],
closed ? [] : [select(path,-1)]
);
// vim: noexpandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap