Merge branch 'master' into revarbat_dev

This commit is contained in:
Revar Desmera 2021-03-06 02:37:00 -08:00 committed by GitHub
commit e7e523bcf7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 177 additions and 105 deletions

View file

@ -244,7 +244,7 @@ function _list_pattern(list) =
// Example:
// same_shape([3,[4,5]],[7,[3,4]]); // Returns true
// same_shape([3,4,5], [7,[3,4]]); // Returns false
function same_shape(a,b) = _list_pattern(a) == b*0;
function same_shape(a,b) = is_def(b) && _list_pattern(a) == b*0;
// Function: is_bool_list()

View file

@ -74,6 +74,15 @@ module hull_points(points, fast=false) {
}
function _backtracking(i,points,h,t,m) =
m<t || _is_cw(points[i], points[h[m-1]], points[h[m-2]]) ? m :
_backtracking(i,points,h,t,m-1) ;
// clockwise check (2d)
function _is_cw(a,b,c) = cross(a-c,b-c)<-EPSILON*norm(a-c)*norm(b-c);
// Function: hull2d_path()
// Usage:
// hull2d_path(points)
@ -85,68 +94,52 @@ module hull_points(points, fast=false) {
// path = hull2d_path(pts);
// move_copies(pts) color("red") sphere(1);
// polygon(points=pts, paths=[path]);
// Code based on this method:
// https://www.hackerearth.com/practice/math/geometry/line-sweep-technique/tutorial/
//
function hull2d_path(points) =
assert(is_path(points,2),"Invalid input to hull2d_path")
len(points) < 2 ? []
: len(points) == 2 ? [0,1]
: let(tri=noncollinear_triple(points, error=false))
tri == [] ? _hull_collinear(points)
: let(
remaining = [ for (i = [0:1:len(points)-1]) if (i != tri[0] && i!=tri[1] && i!=tri[2]) i ],
ccw = triangle_area(points[tri[0]], points[tri[1]], points[tri[2]]) > 0,
polygon = ccw ? [tri[0],tri[1],tri[2]] : [tri[0],tri[2],tri[1]]
) _hull2d_iterative(points, polygon, remaining);
// Adds the remaining points one by one to the convex hull
function _hull2d_iterative(points, polygon, remaining, _i=0) =
(_i >= len(remaining))? polygon : let (
// pick a point
i = remaining[_i],
// find the segments that are in conflict with the point (point not inside)
conflicts = _find_conflicting_segments(points, polygon, points[i])
// no conflicts, skip point and move on
) (len(conflicts) == 0)? _hull2d_iterative(points, polygon, remaining, _i+1) : let(
// find the first conflicting segment and the first not conflicting
// conflict will be sorted, if not wrapping around, do it the easy way
polygon = _remove_conflicts_and_insert_point(polygon, conflicts, i)
) _hull2d_iterative(points, polygon, remaining, _i+1);
len(points) < 2 ? [] :
let( n = len(points),
ip = sortidx(points) )
// lower hull points
let( lh =
[ for( i = 2,
k = 2,
h = [ip[0],ip[1]]; // current list of hull point indices
i <= n;
k = i<n ? _backtracking(ip[i],points,h,2,k)+1 : k,
h = i<n ? [for(j=[0:1:k-2]) h[j], ip[i]] : [],
i = i+1
) if( i==n ) h ][0] )
// concat lower hull points with upper hull ones
[ for( i = n-2,
k = len(lh),
t = k+1,
h = lh; // current list of hull point indices
i >= -1;
k = i>=0 ? _backtracking(ip[i],points,h,t,k)+1 : k,
h = [for(j=[0:1:k-2]) h[j], if(i>0) ip[i]],
i = i-1
) if( i==-1 ) h ][0] ;
function _hull_collinear(points) =
let(
a = points[0],
n = points[1] - a,
i = max_index([for(pt=points) norm(pt-a)]),
n = points[i] - a
)
norm(n)==0 ? [0]
:
let(
points1d = [ for(p = points) (p-a)*n ],
min_i = min_index(points1d),
max_i = max_index(points1d)
) [min_i, max_i];
function _find_conflicting_segments(points, polygon, point) = [
for (i = [0:1:len(polygon)-1]) let(
j = (i+1) % len(polygon),
p1 = points[polygon[i]],
p2 = points[polygon[j]],
area = triangle_area(p1, p2, point)
) if (area < 0) i
];
// remove the conflicting segments from the polygon
function _remove_conflicts_and_insert_point(polygon, conflicts, point) =
(conflicts[0] == 0)? let(
nonconflicting = [ for(i = [0:1:len(polygon)-1]) if (!in_list(i, conflicts)) i ],
new_indices = concat(nonconflicting, (nonconflicting[len(nonconflicting)-1]+1) % len(polygon)),
polygon = concat([ for (i = new_indices) polygon[i] ], point)
) polygon : let(
before_conflicts = [ for(i = [0:1:min(conflicts)]) polygon[i] ],
after_conflicts = (max(conflicts) >= (len(polygon)-1))? [] : [ for(i = [max(conflicts)+1:1:len(polygon)-1]) polygon[i] ],
polygon = concat(before_conflicts, point, after_conflicts)
) polygon;
// Function: hull3d_faces()
// Usage:

View file

@ -1270,6 +1270,12 @@ function path_cut_segs(path,cutlens,closed=false) =
// vector to the path. It tries to find a normal vector that is coplanar to the path near the cut
// point. If this fails it will return a normal vector parallel to the xy plane. The output with
// direction vectors will be `[point, next_index, tangent, normal]`.
// .
// If you give the very last point of the path as a cut point then the returned index will be
// one larger than the last index (so it will not be a valid index). If you use the closed
// option then the returned index will be equal to the path length for cuts along the closing
// path segment, and if you give a point equal to the path length you will get an
// index of len(path)+1 for the index.
//
// Arguments:
// path = path to cut
@ -1286,8 +1292,10 @@ function path_cut_segs(path,cutlens,closed=false) =
function path_cut_points(path, dists, closed=false, direction=false) =
let(long_enough = len(path) >= (closed ? 3 : 2))
assert(long_enough,len(path)<2 ? "Two points needed to define a path" : "Closed path must include three points")
!is_list(dists)? path_cut_points(path, [dists],closed, direction)[0]
: let(cuts = _path_cut_points(path,dists,closed))
is_num(dists) ? path_cut_points(path, [dists],closed, direction)[0] :
assert(is_vector(dists))
assert(list_increasing(dists), "Cut distances must be an increasing list")
let(cuts = _path_cut_points(path,dists,closed))
!direction
? cuts
: let(
@ -1300,20 +1308,23 @@ function path_cut_points(path, dists, closed=false, direction=false) =
function _path_cut_points(path, dists, closed=false, pind=0, dtotal=0, dind=0, result=[]) =
dind == len(dists) ? result :
let(
lastpt = len(result)>0? select(result,-1)[0] : [],
dpartial = len(result)==0? 0 : norm(lastpt-path[pind]),
nextpoint = dpartial > dists[dind]-dtotal?
[lerp(lastpt,path[pind], (dists[dind]-dtotal)/dpartial),pind] :
_path_cut_single(path, dists[dind]-dtotal-dpartial, closed, pind)
) is_undef(nextpoint)?
concat(result, repeat(undef,len(dists)-dind)) :
_path_cut_points(path, dists, closed, nextpoint[1], dists[dind],dind+1, concat(result, [nextpoint]));
lastpt = len(result)==0? [] : select(result,-1)[0], // location of last cut point
dpartial = len(result)==0? 0 : norm(lastpt-select(path,pind)), // remaining length in segment
nextpoint = dists[dind] <= dpartial+dtotal // Do we have enough length left on the current segment?
? [lerp(lastpt,select(path,pind),(dists[dind]-dtotal)/dpartial),pind]
: _path_cut_single(path, dists[dind]-dtotal-dpartial, closed, pind)
)
_path_cut_points(path, dists, closed, nextpoint[1], dists[dind],dind+1, concat(result, [nextpoint]));
// Search for a single cut point in the path
function _path_cut_single(path, dist, closed=false, ind=0, eps=1e-7) =
ind>=len(path)? undef :
ind==len(path)-1 && !closed? (dist<eps? [path[ind],ind+1] : undef) :
let(d = norm(path[ind]-select(path,ind+1))) d > dist ?
// If we get to the very end of the path (ind is last point or wraparound for closed case) then
// check if we are within epsilon of the final path point. If not we're out of path, so we fail
ind==len(path)-(closed?0:1) ?
assert(dist<eps,"Path is too short for specified cut distance")
[select(path,ind),ind+1]
:let(d = norm(path[ind]-select(path,ind+1))) d > dist ?
[lerp(path[ind],select(path,ind+1),dist/d), ind+1] :
_path_cut_single(path, dist-d,closed, ind+1, eps);
@ -1347,18 +1358,61 @@ function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) =
zeros = path[0]*0,
nextind = cuts[ind][1],
nextpath = unit(select(path, nextind+1)-select(path, nextind),zeros),
thispath = unit(select(path, nextind) - path[nextind-1],zeros),
lastpath = unit(path[nextind-1] - select(path, nextind-2),zeros),
thispath = unit(select(path, nextind) - select(path,nextind-1),zeros),
lastpath = unit(select(path,nextind-1) - select(path, nextind-2),zeros),
nextdir =
nextind==len(path) && !closed? lastpath :
(nextind<=len(path)-2 || closed) && approx(cuts[ind][0], path[nextind],eps)?
unit(nextpath+thispath) :
(nextind>1 || closed) && approx(cuts[ind][0],path[nextind-1],eps)?
unit(thispath+lastpath) :
thispath
(nextind<=len(path)-2 || closed) && approx(cuts[ind][0], path[nextind],eps)
? unit(nextpath+thispath)
: (nextind>1 || closed) && approx(cuts[ind][0],select(path,nextind-1),eps)
? unit(thispath+lastpath)
: thispath
) nextdir
];
// Function: path_cut_segs()
// Usage:
// path_list = path_cut_segs(path, cutdist, <closed>);
// Description:
// Given a list of distances in `cutdist`, cut the path into
// subpaths at those lengths, returning a list of paths.
// If the input path is closed then the final path will include the
// original starting point. The list of cut distances must be
// in ascending order. If you repeat a distance you will get an
// empty list in that position in the output.
// Arguments:
// path = path to cut
// cutdist = distance or list of distances where path is cut
// closed = set to true for a closed path. Default: false
function path_cut_segs(path,cutdist,closed) =
is_num(cutdist) ? path_cut_segs(path,[cutdist],closed) :
assert(is_vector(cutdist))
assert(select(cutdist,-1)<path_length(path,closed=closed),"Cut distances must be smaller than the path length")
assert(cutdist[0]>0, "Cut distances must be strictly positive")
let(
cutlist = path_cut(path,cutdist,closed=closed),
cuts = len(cutlist)
)
[
[ each slice(path,0,cutlist[0][1]),
if (!approx(cutlist[0][0], path[cutlist[0][1]-1])) cutlist[0][0]
],
for(i=[0:1:cuts-2])
cutlist[i][0]==cutlist[i+1][0] ? []
:
[ if (!approx(cutlist[i][0], select(path,cutlist[i][1]))) cutlist[i][0],
each slice(path,cutlist[i][1], cutlist[i+1][1]),
if (!approx(cutlist[i+1][0], select(path,cutlist[i+1][1]-1))) cutlist[i+1][0],
],
[
if (!approx(cutlist[cuts-1][0], select(path,cutlist[cuts-1][1]))) cutlist[cuts-1][0],
each select(path,cutlist[cuts-1][1],closed ? 0 : -1)
]
];
// 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

View file

@ -60,7 +60,8 @@ include <structs.scad>
// or you can specify a list that has length len(path)-2, omitting the two dummy values.
// .
// If your input path includes collinear points you must use a cut or radius value of zero for those "corners". You can
// choose a nonzero joint parameter, which will cause extra points to be inserted.
// choose a nonzero joint parameter when the collinear points form a 180 degree angle. This will cause extra points to be inserted.
// If the collinear points form a spike (0 degree angle) then round_corners will fail.
// .
// Examples:
// * `method="circle", radius=2`:
@ -75,7 +76,8 @@ include <structs.scad>
// ignored. Note that $fn is interpreted as the number of points on the roundover curve, which is
// not equivalent to its meaning for rounding circles because roundovers are usually small fractions
// of a circular arc. When doing continuous curvature rounding be sure to use lots of segments or the effect
// will be hidden by the discretization.
// will be hidden by the discretization. Note that if you use $fn then $fn with "smooth" then $fn points are added at each corner, even
// if the "corner" is flat, with collinear points, so this guarantees a specific output length.
//
// Figure(2D,Med):
// h = 18;
@ -260,10 +262,16 @@ function round_corners(path, method="circle", radius, cut, joint, k, closed=true
dk = [
for(i=[0:1:len(path)-1])
let(
angle = vector_angle(select(path,i-1,i+1))/2
pathbit = select(path,i-1,i+1),
angle = approx(pathbit[0],pathbit[1]) || approx(pathbit[1],pathbit[2]) ? undef
: vector_angle(select(path,i-1,i+1))/2,
f=echo(angle=angle)
)
(!closed && (i==0 || i==len(path)-1)) ? [0] : // Force zeros at ends for non-closed
parm[i]==0 ? [0] : // If no rounding requested then don't try to compute parameters
assert(is_def(angle), str("Repeated point in path at index ",i," with nonzero rounding"))
assert(!approx(angle,0), closed && i==0 ? "Closing the path causes it to turn back on itself at the end" :
str("Path turns back on itself at index ",i," with nonzero rounding"))
(method=="chamfer" && measure=="joint")? [parm[i]] :
(method=="chamfer" && measure=="cut") ? [parm[i]/cos(angle)] :
(method=="smooth" && measure=="joint") ? [parm[i],k[i]] :
@ -277,10 +285,11 @@ function round_corners(path, method="circle", radius, cut, joint, k, closed=true
lengths = [for(i=[0:1:len(path)]) norm(select(path,i)-select(path,i-1))],
scalefactors = [
for(i=[0:1:len(path)-1])
min(
if (closed || (i!=0 && i!=len(path)-1))
min(
lengths[i]/(select(dk,i-1)[0]+dk[i][0]),
lengths[i+1]/(dk[i][0]+select(dk,i+1)[0])
)
)
],
dummy = verbose ? echo("Roundover scale factors:",scalefactors) : 0
)
@ -639,12 +648,12 @@ function _path_join(paths,joint,k=0.5,i=0,result=[],relocate=true,closed=false)
d_next = is_vector(joint[i]) ? joint[i][1] : joint[i]
)
assert(d_first>=0 && d_next>=0, str("Joint value negative when adding path ",i+1))
assert(d_first<path_length(revresult),str("Path ",i," is too short for specified cut distance ",d_first))
assert(d_next<path_length(nextpath), str("Path ",i+1," is too short for specified cut distance ",d_next))
let(
firstcut = path_cut_points(revresult, d_first, direction=true),
nextcut = path_cut_points(nextpath, d_next, direction=true)
)
assert(is_def(firstcut),str("Path ",i," is too short for specified cut distance ",d_first))
assert(is_def(nextcut),str("Path ",i+1," is too short for specified cut distance ",d_next))
assert(!loop || nextcut[1] < len(revresult)-1-firstcut[1], "Path is too short to close the loop")
let(
first_dir=firstcut[2],

View file

@ -47,31 +47,29 @@
// profiles that you specify. It is generally best if the triangles forming your polyhedron
// are approximately equilateral. The `slices` parameter specifies the number of slices to insert
// between each pair of profiles, either a scalar to insert the same number everywhere, or a vector
// to insert a different number between each pair. To resample the profiles you can use set
// `refine=N` which will place `N` points on each edge of your profile. This has the effect of
// multiplying the number of points by N, so a profile with 8 points will have 8*N points after
// refinement. Note that when dealing with continuous curves it is always better to adjust the
// to insert a different number between each pair.
// .
// Resampling may occur, depending on the `method` parameter, to make profiles compatible.
// To force (possibly additional) resampling of the profiles to increase the point density you can set `refine=N`, which
// will multiply the number of points on your profile by `N`. You can choose between two resampling
// schemes using the `sampling` option, which you can set to `"length"` or `"segment"`.
// The length resampling method resamples proportional to length.
// The segment method divides each segment of a profile into the same number of points.
// This means that if you refine a profile with the "segment" method you will get N points
// on each edge, but if you refine a profile with the "length" method you will get new points
// distributed around the profile based on length, so small segments will get fewer new points than longer ones.
// A uniform division may be impossible, in which case the code computes an approximation, which may result
// in arbitrary distribution of extra points. See `subdivide_path` for more details.
// Note that when dealing with continuous curves it is always better to adjust the
// sampling in your code to generate the desired sampling rather than using the `refine` argument.
// .
// Two methods are available for resampling, `"length"` and `"segment"`. Specify them using
// the `sampling` argument. The length resampling method resamples proportional to length.
// The segment method divides each segment of a profile into the same number of points.
// A uniform division may be impossible, in which case the code computes an approximation.
// See `subdivide_path` for more details.
//
// You can choose from four methods for specifying alignment for incommensurate profiles.
// The available methods are `"distance"`, `"tangent"`, `"direct"` and `"reindex"`.
// It is useful to distinguish between continuous curves like a circle and discrete profiles
// like a hexagon or star, because the algorithms' suitability depend on this distinction.
// .
// The "direct" and "reindex" methods work by resampling the profiles if necessary. As noted above,
// for continuous input curves, it is better to generate your curves directly at the desired sample size,
// but for mapping between a discrete profile like a hexagon and a circle, the hexagon must be resampled
// to match the circle. You can do this in two different ways using the `sampling` parameter. The default
// of `sampling="length"` approximates a uniform length sampling of the profile. The other option
// is `sampling="segment"` which attempts to place the same number of new points on each segment.
// If the segments are of varying length, this will produce a different result. Note that "direct" is
// the default method. If you simply supply a list of compatible profiles it will link them up
// The default method for aligning profiles is `method="direct"`.
// If you simply supply a list of compatible profiles it will link them up
// exactly as you have provided them. You may find that profiles you want to connect define the
// right shapes but the point lists don't start from points that you want aligned in your skinned
// polyhedron. You can correct this yourself using `reindex_polygon`, or you can use the "reindex"
@ -79,12 +77,25 @@
// in the polyhedron---in will produce the least twisted possible result. This algorithm has quadratic
// run time so it can be slow with very large profiles.
// .
// When the profiles are incommensurate, the "direct" and "reindex" resampling them to match. As noted above,
// for continuous input curves, it is better to generate your curves directly at the desired sample size,
// but for mapping between a discrete profile like a hexagon and a circle, the hexagon must be resampled
// to match the circle. When you use "direct" or "reindex" the default `sampling` value is
// of `sampling="length"` to approximate a uniform length sampling of the profile. This will generally
// produce the natural result for connecting two continuously sampled profiles or a continuous
// profile and a polygonal one. However depending on your particular case,
// `sampling="segment"` may produce a more pleasing result. These two approaches differ only when
// the segments of your input profiles have unequal length.
// .
// The "distance" and "tangent" methods work by duplicating vertices to create
// triangular faces. The "distance" method finds the global minimum distance method for connecting two
// profiles. This algorithm generally produces a good result when both profiles are discrete ones with
// a small number of vertices. It is computationally intensive (O(N^3)) and may be
// slow on large inputs. The resulting surfaces generally have curved faces, so be
// sure to select a sufficiently large value for `slices` and `refine`.
// sure to select a sufficiently large value for `slices` and `refine`. Note that for
// this method, `sampling` must be set to `"segment"`, and hence this is the default setting.
// Using sampling by length would ignore the repeated vertices and ruin the alignment.
// .
// The `"tangent"` method generally produces good results when
// connecting a discrete polygon to a convex, finely sampled curve. It works by finding
// a plane that passed through each edge of the polygon that is tangent to
@ -92,9 +103,8 @@
// all of the tangent points from each other. It connects all of the points of the curve to the corners of the discrete
// polygon using triangular faces. Using `refine` with this method will have little effect on the model, so
// you should do it only for agreement with other profiles, and these models are linear, so extra slices also
// have no effect. For best efficiency set `refine=1` and `slices=0`. When you use refinement with either
// of these methods, it is always the "segment" based resampling described above. This is necessary because
// sampling by length will ignore the repeated vertices and break the alignment.
// have no effect. For best efficiency set `refine=1` and `slices=0`. As with the "distance" method, refinement
// must be done using the "segment" sampling scheme to preserve alignment across duplicated points.
// .
// It is possible to specify `method` and `refine` as arrays, but it is important to observe
// matching rules when you do this. If a pair of profiles is connected using "tangent" or "distance"

View file

@ -245,6 +245,12 @@ test_is_consistent();
module test_same_shape() {
assert(same_shape([3,[4,5]],[7,[3,4]]));
assert(!same_shape([3,4,5], [7,[3,4]]));
assert(!same_shape([3,4,5],undef));
assert(!same_shape([5,3],3));
assert(!same_shape(undef,[3,4]));
assert(same_shape(4,5));
assert(!same_shape(5,undef));
}
test_same_shape();

View file

@ -6,9 +6,9 @@ module test_hull() {
assert_equal(hull([[3,4,1],[5,5,3]]), [0,1]);
test_collinear_2d = let(u = unit([5,3])) [ for(i = [9,2,3,4,5,7,12,15,13]) i * u ];
assert_equal(hull(test_collinear_2d), [7,1]);
assert_equal(sort(hull(test_collinear_2d)), [1,7]);
test_collinear_3d = let(u = unit([5,3,2])) [ for(i = [9,2,3,4,5,7,12,15,13]) i * u ];
assert_equal(hull(test_collinear_3d), [7,1]);
assert_equal(sort(hull(test_collinear_3d)), [1,7]);
/* // produces some extra points along edges
test_square_2d = [for(x=[1:5], y=[2:6]) [x,y]];
@ -105,9 +105,9 @@ module test_hull2d_path() {
assert_equal(hull([[3,4,1],[5,5,3]]), [0,1]);
test_collinear_2d = let(u = unit([5,3])) [ for(i = [9,2,3,4,5,7,12,15,13]) i * u ];
assert_equal(hull(test_collinear_2d), [7,1]);
assert_equal(sort(hull(test_collinear_2d)), [1,7]);
test_collinear_3d = let(u = unit([5,3,2])) [ for(i = [9,2,3,4,5,7,12,15,13]) i * u ];
assert_equal(hull(test_collinear_3d), [7,1]);
assert_equal(sort(hull(test_collinear_3d)), [1,7]);
rand10_2d = [[1.55356, -1.98965], [4.23157, -0.947788], [-4.06193, -1.55463],
[1.23889, -3.73133], [-1.02637, -4.0155], [4.26806, -4.61909],

View file

@ -6,7 +6,7 @@
//////////////////////////////////////////////////////////////////////
BOSL_VERSION = [2,0,579];
BOSL_VERSION = [2,0,580];
// Section: BOSL Library Version Functions