2019-04-20 00:02:17 +00:00
//////////////////////////////////////////////////////////////////////
// LibFile: geometry.scad
// Geometry helpers.
// To use, add the following lines to the beginning of your file:
// ```
// use <BOSL2/std.scad>
// ```
//////////////////////////////////////////////////////////////////////
2019-06-27 01:56:33 +00:00
// CommonCode:
2019-07-13 22:57:24 +00:00
// include <BOSL2/rounding.scad>
2019-06-27 01:56:33 +00:00
2019-04-20 00:02:17 +00:00
// Section: Lines and Triangles
2019-05-02 02:28:02 +00:00
// Function: point_on_segment2d()
2019-04-20 00:02:17 +00:00
// Usage:
2019-05-02 02:28:02 +00:00
// point_on_segment2d(point, edge);
2019-04-20 00:02:17 +00:00
// Description:
// Determine if the point is on the line segment between two points.
2019-06-26 00:57:03 +00:00
// Returns true if yes, and false if not.
2019-04-20 00:02:17 +00:00
// Arguments:
2019-05-01 06:45:05 +00:00
// point = The point to test.
2019-04-20 00:02:17 +00:00
// edge = Array of two points forming the line segment to test against.
2019-06-18 01:55:10 +00:00
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
function point_on_segment2d ( point , edge , eps = EPSILON ) =
approx ( point , edge [ 0 ] , eps = eps ) || approx ( point , edge [ 1 ] , eps = eps ) || // The point is an endpoint
2019-04-20 00:02:17 +00:00
sign ( edge [ 0 ] . x - point . x ) = = sign ( point . x - edge [ 1 ] . x ) // point is in between the
2019-06-26 00:57:03 +00:00
&& sign ( edge [ 0 ] . y - point . y ) = = sign ( point . y - edge [ 1 ] . y ) // edge endpoints
2019-06-18 01:55:10 +00:00
&& approx ( point_left_of_segment2d ( point , edge ) , 0 , eps = eps ) ; // and on the line defined by edge
2019-04-20 00:02:17 +00:00
2019-05-02 02:28:02 +00:00
// Function: point_left_of_segment2d()
2019-04-20 00:02:17 +00:00
// Usage:
2019-05-02 02:28:02 +00:00
// point_left_of_segment2d(point, edge);
2019-04-20 00:02:17 +00:00
// Description:
// Return >0 if point is left of the line defined by edge.
// Return =0 if point is on the line.
// Return <0 if point is right of the line.
// Arguments:
// point = The point to check position of.
// edge = Array of two points forming the line segment to test against.
2019-05-02 02:28:02 +00:00
function point_left_of_segment2d ( point , edge ) =
2019-04-20 00:02:17 +00:00
( edge [ 1 ] . x - edge [ 0 ] . x ) * ( point . y - edge [ 0 ] . y ) - ( point . x - edge [ 0 ] . x ) * ( edge [ 1 ] . y - edge [ 0 ] . y ) ;
2019-06-26 00:57:03 +00:00
2019-04-20 00:02:17 +00:00
// Internal non-exposed function.
2019-07-05 06:47:42 +00:00
function _point_above_below_segment ( point , edge ) =
edge [ 0 ] . y < = point . y ? (
( edge [ 1 ] . y > point . y && point_left_of_segment2d ( point , edge ) > 0 ) ? 1 : 0
2019-04-20 00:02:17 +00:00
) : (
2019-07-05 06:47:42 +00:00
( edge [ 1 ] . y < = point . y && point_left_of_segment2d ( point , edge ) < 0 ) ? - 1 : 0
2019-04-20 00:02:17 +00:00
) ;
// Function: collinear()
// Usage:
// collinear(a, b, c, [eps]);
// Description:
// Returns true if three points are co-linear.
// Arguments:
// a = First point.
// b = Second point.
// c = Third point.
2019-05-01 06:45:05 +00:00
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
2019-04-20 00:02:17 +00:00
function collinear ( a , b , c , eps = EPSILON ) =
2019-05-01 06:45:05 +00:00
distance_from_line ( [ a , b ] , c ) < eps ;
2019-04-20 00:02:17 +00:00
// Function: collinear_indexed()
// Usage:
// collinear_indexed(points, a, b, c, [eps]);
// Description:
// Returns true if three points are co-linear.
// Arguments:
// points = A list of points.
// a = Index in `points` of first point.
// b = Index in `points` of second point.
// c = Index in `points` of third point.
// eps = Acceptable max angle variance. Default: EPSILON (1e-9) degrees.
function collinear_indexed ( points , a , b , c , eps = EPSILON ) =
let (
p1 = points [ a ] ,
p2 = points [ b ] ,
p3 = points [ c ]
2019-05-01 06:45:05 +00:00
) collinear ( p1 , p2 , p3 , eps ) ;
// Function: distance_from_line()
// Usage:
// distance_from_line(line, pt);
// Description:
// Finds the perpendicular distance of a point `pt` from the line `line`.
// Arguments:
// line = A list of two points, defining a line that both are on.
// pt = A point to find the distance of from the line.
// Example:
// distance_from_line([[-10,0], [10,0]], [3,8]); // Returns: 8
function distance_from_line ( line , pt ) =
let ( a = line [ 0 ] , n = normalize ( line [ 1 ] - a ) , d = a - pt )
norm ( d - ( ( d * n ) * n ) ) ;
2019-04-20 00:02:17 +00:00
2019-05-29 01:44:41 +00:00
// Function: line_normal()
// Usage:
// line_normal([P1,P2])
// line_normal(p1,p2)
2019-08-16 09:06:04 +00:00
// Description:
// Returns the 2D normal vector to the given 2D line. This is otherwise known as the perpendicular vector counter-clockwise to the given ray.
2019-05-29 01:44:41 +00:00
// Arguments:
// p1 = First point on 2D line.
// p2 = Second point on 2D line.
2019-08-16 09:06:04 +00:00
// Example(2D):
// p1 = [10,10];
// p2 = [50,30];
// n = line_normal(p1,p2);
// stroke([p1,p2], endcap2="arrow2");
// color("green") stroke([p1,p1+10*n], endcap2="arrow2");
// color("blue") place_copies([p1,p2]) circle(d=2, $fn=12);
2019-05-29 01:44:41 +00:00
function line_normal ( p1 , p2 ) =
is_undef ( p2 ) ? line_normal ( p1 [ 0 ] , p1 [ 1 ] ) :
normalize ( [ p1 . y - p2 . y , p2 . x - p1 . x ] ) ;
// 2D Line intersection from two segments.
// This function returns [p,t,u] where p is the intersection point of
2019-09-19 09:44:28 +00:00
// the lines defined by the two segments, t is the proportional distance
// of the intersection point along s1, and u is the proportional distance
// of the intersection point along s2. The proportional values run over
// the range of 0 to 1 for each segment, so if it is in this range, then
// the intersection lies on the segment. Otherwise it lies somewhere on
// the extension of the segment.
2019-06-18 01:55:10 +00:00
function _general_line_intersection ( s1 , s2 , eps = EPSILON ) =
let (
denominator = det2 ( [ s1 [ 0 ] , s2 [ 0 ] ] - [ s1 [ 1 ] , s2 [ 1 ] ] )
) approx ( denominator , 0 , eps = eps ) ? [ undef , undef , undef ] : let (
t = det2 ( [ s1 [ 0 ] , s2 [ 0 ] ] - s2 ) / denominator ,
2019-09-19 09:44:28 +00:00
u = det2 ( [ s1 [ 0 ] , s1 [ 0 ] ] - [ s2 [ 0 ] , s1 [ 1 ] ] ) / denominator
2019-06-18 01:55:10 +00:00
) [ s1 [ 0 ] + t * ( s1 [ 1 ] - s1 [ 0 ] ) , t , u ] ;
2019-05-29 01:44:41 +00:00
// Function: line_intersection()
// Usage:
// line_intersection(l1, l2);
// Description:
// Returns the 2D intersection point of two unbounded 2D lines.
// Returns `undef` if the lines are parallel.
// Arguments:
// l1 = First 2D line, given as a list of two 2D points on the line.
// l2 = Second 2D line, given as a list of two 2D points on the line.
2019-06-18 01:55:10 +00:00
function line_intersection ( l1 , l2 , eps = EPSILON ) =
let ( isect = _general_line_intersection ( l1 , l2 , eps = eps ) ) isect [ 0 ] ;
2019-05-29 01:44:41 +00:00
// Function: segment_intersection()
// Usage:
// segment_intersection(s1, s2);
// Description:
// Returns the 2D intersection point of two 2D line segments.
// Returns `undef` if they do not intersect.
// Arguments:
// s1 = First 2D segment, given as a list of the two 2D endpoints of the line segment.
// s2 = Second 2D segment, given as a list of the two 2D endpoints of the line segment.
2019-06-18 01:55:10 +00:00
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
function segment_intersection ( s1 , s2 , eps = EPSILON ) =
2019-05-30 01:01:00 +00:00
let (
2019-06-18 01:55:10 +00:00
isect = _general_line_intersection ( s1 , s2 , eps = eps )
2019-05-30 01:01:00 +00:00
) isect [ 1 ] < 0 - eps || isect [ 1 ] > 1 + eps || isect [ 2 ] < 0 - eps || isect [ 2 ] > 1 + eps ? undef : isect [ 0 ] ;
2019-05-29 01:44:41 +00:00
// Function: line_segment_intersection()
// Usage:
// line_segment_intersection(line, segment);
// Description:
// Returns the 2D intersection point of an unbounded 2D line, and a bounded 2D line segment.
// Returns `undef` if they do not intersect.
// Arguments:
// line = The unbounded 2D line, defined by two 2D points on the line.
// segment = The bounded 2D line segment, given as a list of the two 2D endpoints of the segment.
2019-06-18 01:55:10 +00:00
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
function line_segment_intersection ( line , segment , eps = EPSILON ) =
2019-05-30 01:01:00 +00:00
let (
2019-06-18 01:55:10 +00:00
isect = _general_line_intersection ( line , segment , eps = eps )
2019-05-30 01:01:00 +00:00
) isect [ 2 ] < 0 - eps || isect [ 2 ] > 1 + eps ? undef : isect [ 0 ] ;
2019-05-29 01:44:41 +00:00
2019-08-17 04:22:41 +00:00
// Function: line_closest_point()
// Usage:
// line_closest_point(line,pt);
// Description:
// Returns the point on the given `line` that is closest to the given point `pt`.
// Arguments:
// line = A list of two points that are on the unbounded line.
// pt = The point to find the closest point on the line to.
function line_closest_point ( line , pt ) =
let (
n = line_normal ( line ) ,
isect = _general_line_intersection ( line , [ pt , pt + n ] )
) isect [ 0 ] ;
// Function: segment_closest_point()
// Usage:
// segment_closest_point(seg,pt);
// Description:
// Returns the point on the given line segment `seg` that is closest to the given point `pt`.
// Arguments:
// seg = A list of two points that are the endpoints of the bounded line segment.
// pt = The point to find the closest point on the segment to.
function segment_closest_point ( seg , pt ) =
let (
n = line_normal ( seg ) ,
isect = _general_line_intersection ( seg , [ pt , pt + n ] )
)
2019-08-20 04:11:19 +00:00
norm ( n ) = = 0 ? seg [ 0 ] :
2019-08-17 04:22:41 +00:00
isect [ 1 ] < = 0 ? seg [ 0 ] :
isect [ 1 ] >= 1 ? seg [ 1 ] :
isect [ 0 ] ;
2019-06-12 09:27:42 +00:00
// Function: find_circle_2tangents()
// Usage:
// find_circle_2tangents(pt1, pt2, pt3, r|d);
// Description:
// Returns [centerpoint, normal] of a circle of known size that is between and tangent to two rays with the same starting point.
// Both rays start at `pt2`, and one passes through `pt1`, while the other passes through `pt3`.
// If the rays given are 180º apart, `undef` is returned. If the rays are 3D, the normal returned is the plane normal of the circle.
// Arguments:
// pt1 = A point that the first ray passes though.
// pt2 = The starting point of both rays.
// pt3 = A point that the second ray passes though.
// r = The radius of the circle to find.
// d = The diameter of the circle to find.
2019-08-16 09:06:04 +00:00
// Example(2D):
// pts = [[60,40], [10,10], [65,5]];
// rad = 10;
// stroke([pts[1],pts[0]], endcap2="arrow2");
// stroke([pts[1],pts[2]], endcap2="arrow2");
// circ = find_circle_2tangents(pt1=pts[0], pt2=pts[1], pt3=pts[2], r=rad);
// translate(circ[0]) {
// color("green") {
// stroke(circle(r=rad),closed=true);
// stroke([[0,0],rad*[cos(315),sin(315)]]);
// }
// }
// place_copies(pts) color("blue") circle(d=2, $fn=12);
// translate(circ[0]) color("red") circle(d=2, $fn=12);
// labels = [[pts[0], "pt1"], [pts[1],"pt2"], [pts[2],"pt3"], [circ[0], "CP"], [circ[0]+[cos(315),sin(315)]*rad*0.7, "r"]];
// for(l=labels) translate(l[0]+[0,2]) color("black") text(text=l[1], size=2.5, halign="center");
2019-06-12 09:27:42 +00:00
function find_circle_2tangents ( pt1 , pt2 , pt3 , r = undef , d = undef ) =
2019-10-30 05:46:00 +00:00
let ( r = get_radius ( r = r , d = d , dflt = undef ) )
assert ( r ! = undef , "Must specify either r or d." )
( is_undef ( pt2 ) && is_undef ( pt3 ) && is_list ( pt1 ) ) ? find_circle_2tangents ( pt1 [ 0 ] , pt1 [ 1 ] , pt1 [ 2 ] , r = r ) :
2019-06-12 09:27:42 +00:00
let (
v1 = normalize ( pt1 - pt2 ) ,
v2 = normalize ( pt3 - pt2 )
) approx ( norm ( v1 + v2 ) ) ? undef :
let (
a = vector_angle ( v1 , v2 ) ,
n = vector_axis ( v1 , v2 ) ,
v = normalize ( mean ( [ v1 , v2 ] ) ) ,
s = r / sin ( a / 2 ) ,
cp = pt2 + s * v / norm ( v )
) [ cp , n ] ;
2019-07-15 23:41:01 +00:00
// Function: find_circle_3points()
// Usage:
// find_circle_3points(pt1, pt2, pt3);
// Description:
// Returns the [CENTERPOINT, RADIUS, NORMAL] of the circle that passes through three non-collinear
// points. The centerpoint will be a 2D or 3D vector, depending on the points input. If all three
// points are 2D, then the resulting centerpoint will be 2D, and the normal will be UP ([0,0,1]).
// If any of the points are 3D, then the resulting centerpoint will be 3D. If the three points are
// collinear, then `[undef,undef,undef]` will be returned. The normal will be a normalized 3D
// vector with a non-negative Z axis.
// Arguments:
// pt1 = The first point.
// pt2 = The second point.
// pt3 = The third point.
2019-08-16 09:06:04 +00:00
// Example(2D):
// pts = [[60,40], [10,10], [65,5]];
// circ = find_circle_3points(pts[0], pts[1], pts[2]);
// translate(circ[0]) color("green") stroke(circle(r=circ[1]),closed=true,$fn=72);
// translate(circ[0]) color("red") circle(d=3, $fn=12);
// place_copies(pts) color("blue") circle(d=3, $fn=12);
2019-07-15 23:41:01 +00:00
function find_circle_3points ( pt1 , pt2 , pt3 ) =
2019-10-30 05:46:00 +00:00
( is_undef ( pt2 ) && is_undef ( pt3 ) && is_list ( pt1 ) ) ? find_circle_3points ( pt1 [ 0 ] , pt1 [ 1 ] , pt1 [ 2 ] ) :
2019-07-15 23:41:01 +00:00
collinear ( pt1 , pt2 , pt3 ) ? [ undef , undef , undef ] :
let (
v1 = pt1 - pt2 ,
v2 = pt3 - pt2 ,
n = vector_axis ( v1 , v2 ) ,
n2 = n . z < 0 ? - n : n
) len ( pt1 ) + len ( pt2 ) + len ( pt3 ) > 6 ? (
let (
a = project_plane ( pt1 , pt1 , pt2 , pt3 ) ,
b = project_plane ( pt2 , pt1 , pt2 , pt3 ) ,
c = project_plane ( pt3 , pt1 , pt2 , pt3 ) ,
res = find_circle_3points ( a , b , c )
) res [ 0 ] = = undef ? [ undef , undef , undef ] : let (
cp = lift_plane ( res [ 0 ] , pt1 , pt2 , pt3 ) ,
2019-10-30 05:46:00 +00:00
r = norm ( pt2 - cp )
2019-07-15 23:41:01 +00:00
) [ cp , r , n2 ]
) : let (
mp1 = pt2 + v1 / 2 ,
mp2 = pt2 + v2 / 2 ,
mpv1 = rot ( 90 , v = n , p = v1 ) ,
mpv2 = rot ( 90 , v = n , p = v2 ) ,
l1 = [ mp1 , mp1 + mpv1 ] ,
l2 = [ mp2 , mp2 + mpv2 ] ,
isect = line_intersection ( l1 , l2 )
) is_undef ( isect ) ? [ undef , undef , undef ] : let (
2019-08-16 09:06:04 +00:00
r = norm ( pt2 - isect )
2019-07-15 23:41:01 +00:00
) [ isect , r , n2 ] ;
2019-10-22 01:59:35 +00:00
// Function: find_circle_tangents()
// Usage:
// tangents = find_circle_tangents(r|d, cp, pt);
// Description:
// Given a circle and a point outside that circle, finds the tangent point(s) on the circle for a
// line passing through the point. Returns list of zero or more sublists of [ANG, TANGPT]
// Arguments:
// r = Radius of the circle.
// d = Diameter of the circle.
// cp = The coordinates of the circle centerpoint.
// pt = The coordinates of the external point.
// Example(2D):
// cp = [-10,-10]; r = 30; pt = [30,10];
// tanpts = subindex(find_circle_tangents(r=r, cp=cp, pt=pt),1);
// color("yellow") translate(cp) circle(r=r);
// color("cyan") for(tp=tanpts) {stroke([tp,pt]); stroke([tp,cp]);}
// color("red") place_copies(tanpts) circle(d=3,$fn=12);
// color("blue") place_copies([cp,pt]) circle(d=3,$fn=12);
function find_circle_tangents ( r , d , cp , pt ) =
assert ( is_num ( r ) || is_num ( d ) )
assert ( is_vector ( cp ) )
assert ( is_vector ( pt ) )
let (
r = get_radius ( r = r , d = d , dflt = 1 ) ,
delta = pt - cp ,
dist = norm ( delta ) ,
baseang = atan2 ( delta . y , delta . x )
) dist < r ? [ ] :
approx ( dist , r ) ? [ [ baseang , pt ] ] :
let (
relang = acos ( r / dist ) ,
angs = [ baseang + relang , baseang - relang ]
) [ for ( ang = angs ) [ ang , cp + r * [ cos ( ang ) , sin ( ang ) ] ] ] ;
2019-07-17 08:49:51 +00:00
// Function: tri_calc()
// Usage:
// tri_calc(ang,ang2,adj,opp,hyp);
// Description:
// Given a side length and an angle, or two side lengths, calculates the rest of the side lengths
// and angles of a right triangle. Returns [ADJACENT, OPPOSITE, HYPOTENUSE, ANGLE, ANGLE2] where
// ADJACENT is the length of the side adjacent to ANGLE, and OPPOSITE is the length of the side
// opposite of ANGLE and adjacent to ANGLE2. ANGLE and ANGLE2 are measured in degrees.
// This is certainly more verbose and slower than writing your own calculations, but has the nice
// benefit that you can just specify the info you have, and don't have to figure out which trig
// formulas you need to use.
// Figure(2D):
// color("#ccc") {
// stroke(closed=false, width=0.5, [[45,0], [45,5], [50,5]]);
// stroke(closed=false, width=0.5, arc(N=6, r=15, cp=[0,0], start=0, angle=30));
// stroke(closed=false, width=0.5, arc(N=6, r=14, cp=[50,30], start=212, angle=58));
// }
// color("black") stroke(closed=true, [[0,0], [50,30], [50,0]]);
// color("#0c0") {
// translate([10.5,2.5]) text(size=3,text="ang",halign="center",valign="center");
// translate([44.5,22]) text(size=3,text="ang2",halign="center",valign="center");
// }
// color("blue") {
// translate([25,-3]) text(size=3,text="Adjacent",halign="center",valign="center");
// translate([53,15]) rotate(-90) text(size=3,text="Opposite",halign="center",valign="center");
// translate([25,18]) rotate(30) text(size=3,text="Hypotenuse",halign="center",valign="center");
// }
// Arguments:
// ang = The angle in degrees of the primary corner of the triangle.
// ang2 = The angle in degrees of the other non-right corner of the triangle.
// adj = The length of the side adjacent to the primary corner.
// opp = The length of the side opposite to the primary corner.
// hyp = The length of the hypotenuse.
// Example:
// tri = tri_calc(opp=15,hyp=30);
// echo(adjacent=tri[0], opposite=tri[1], hypotenuse=tri[2], angle=tri[3], angle2=tri[4]);
// Examples:
// adj = tri_calc(ang=30,opp=10)[0];
// opp = tri_calc(ang=20,hyp=30)[1];
// hyp = tri_calc(ang2=50,adj=20)[2];
// ang = tri_calc(adj=20,hyp=30)[3];
// ang2 = tri_calc(adj=20,hyp=40)[4];
function tri_calc ( ang , ang2 , adj , opp , hyp ) =
2019-10-30 05:46:00 +00:00
assert ( ang = = undef || ang2 = = undef , "You cannot specify both ang and ang2." )
2019-07-17 08:49:51 +00:00
assert ( num_defined ( [ ang , ang2 , adj , opp , hyp ] ) = = 2 , "You must specify exactly two arguments." )
let (
ang = ang ! = undef ? assert ( ang > 0 && ang < 90 ) ang :
ang2 ! = undef ? ( 90 - ang2 ) :
adj = = undef ? asin ( constrain ( opp / hyp , - 1 , 1 ) ) :
opp = = undef ? acos ( constrain ( adj / hyp , - 1 , 1 ) ) :
atan2 ( opp , adj ) ,
ang2 = ang2 ! = undef ? assert ( ang2 > 0 && ang2 < 90 ) ang2 : ( 90 - ang ) ,
adj = adj ! = undef ? assert ( adj > 0 ) adj :
( opp ! = undef ? ( opp / tan ( ang ) ) : ( hyp * cos ( ang ) ) ) ,
opp = opp ! = undef ? assert ( opp > 0 ) opp :
( adj ! = undef ? ( adj * tan ( ang ) ) : ( hyp * sin ( ang ) ) ) ,
hyp = hyp ! = undef ? assert ( hyp > 0 ) assert ( adj < hyp ) assert ( opp < hyp ) hyp :
( adj ! = undef ? ( adj / cos ( ang ) ) : ( opp / sin ( ang ) ) )
)
[ adj , opp , hyp , ang , ang2 ] ;
2019-10-30 05:46:00 +00:00
// Function: hyp_opp_to_adj()
// Usage:
// adj = hyp_opp_to_adj(hyp,opp);
// Description:
// Given the lengths of the hypotenuse and opposite side of a right triangle, returns the length
// of the adjacent side.
// Arguments:
// hyp = The length of the hypotenuse of the right triangle.
// opp = The length of the side of the right triangle that is opposite from the primary angle.
// Example:
// hyp = hyp_opp_to_adj(5,3); // Returns: 4
function hyp_opp_to_adj ( hyp , opp ) =
assert ( is_num ( hyp ) && hyp >= 0 )
assert ( is_num ( opp ) && opp >= 0 )
sqrt ( hyp * hyp - opp * opp ) ;
// Function: hyp_ang_to_adj()
// Usage:
// adj = hyp_ang_to_adj(hyp,ang);
// Description:
// Given the length of the hypotenuse and the angle of the primary corner of a right triangle,
// returns the length of the adjacent side.
// Arguments:
// hyp = The length of the hypotenuse of the right triangle.
// ang = The angle in degrees of the primary corner of the right triangle.
// Example:
// adj = hyp_ang_to_adj(8,60); // Returns: 4
function hyp_ang_to_adj ( hyp , ang ) =
assert ( is_num ( hyp ) && hyp >= 0 )
assert ( is_num ( ang ) && ang > 0 && ang < 90 )
hyp * cos ( ang ) ;
// Function: opp_ang_to_adj()
// Usage:
// adj = opp_ang_to_adj(opp,ang);
// Description:
// Given the angle of the primary corner of a right triangle, and the length of the side opposite of it,
// returns the length of the adjacent side.
// Arguments:
// opp = The length of the side of the right triangle that is opposite from the primary angle.
// ang = The angle in degrees of the primary corner of the right triangle.
// Example:
// adj = opp_ang_to_adj(8,30); // Returns: 4
function opp_ang_to_adj ( opp , ang ) =
assert ( is_num ( opp ) && opp >= 0 )
assert ( is_num ( ang ) && ang > 0 && ang < 90 )
opp / tan ( ang ) ;
// Function: hyp_adj_to_opp()
// Usage:
// opp = hyp_adj_to_opp(hyp,adj);
// Description:
// Given the length of the hypotenuse and the adjacent side, returns the length of the opposite side.
// Arguments:
// hyp = The length of the hypotenuse of the right triangle.
// adj = The length of the side of the right triangle that is adjacent to the primary angle.
// Example:
// opp = hyp_adj_to_opp(5,4); // Returns: 3
function hyp_adj_to_opp ( hyp , adj ) =
assert ( is_num ( hyp ) && hyp >= 0 )
assert ( is_num ( adj ) && adj >= 0 )
sqrt ( hyp * hyp - adj * adj ) ;
// Function: hyp_ang_to_opp()
// Usage:
// opp = hyp_ang_to_opp(hyp,adj);
// Description:
// Given the length of the hypotenuse of a right triangle, and the angle of the corner, returns the length of the opposite side.
// Arguments:
// hyp = The length of the hypotenuse of the right triangle.
// ang = The angle in degrees of the primary corner of the right triangle.
// Example:
// opp = hyp_ang_to_opp(8,30); // Returns: 4
function hyp_ang_to_opp ( hyp , ang ) =
assert ( is_num ( hyp ) && hyp >= 0 )
assert ( is_num ( ang ) && ang > 0 && ang < 90 )
hyp * sin ( ang ) ;
// Function: adj_ang_to_opp()
// Usage:
// opp = adj_ang_to_opp(adj,ang);
// Description:
// Given the length of the adjacent side of a right triangle, and the angle of the corner, returns the length of the opposite side.
// Arguments:
// adj = The length of the side of the right triangle that is adjacent to the primary angle.
// ang = The angle in degrees of the primary corner of the right triangle.
// Example:
// opp = adj_ang_to_opp(8,45); // Returns: 8
function adj_ang_to_opp ( adj , ang ) =
assert ( is_num ( adj ) && adj >= 0 )
assert ( is_num ( ang ) && ang > 0 && ang < 90 )
adj * tan ( ang ) ;
// Function: adj_opp_to_hyp()
// Usage:
// hyp = adj_opp_to_hyp(adj,opp);
// Description:
// Given the length of the adjacent and opposite sides of a right triangle, returns the length of thee hypotenuse.
// Arguments:
// adj = The length of the side of the right triangle that is adjacent to the primary angle.
// opp = The length of the side of the right triangle that is opposite from the primary angle.
// Example:
// hyp = adj_opp_to_hyp(3,4); // Returns: 5
function adj_opp_to_hyp ( adj , opp ) =
assert ( is_num ( adj ) && adj >= 0 )
assert ( is_num ( opp ) && opp >= 0 )
norm ( [ opp , adj ] ) ;
// Function: adj_ang_to_hyp()
// Usage:
// hyp = adj_ang_to_hyp(adj,ang);
// Description:
// For a right triangle, given the length of the adjacent side, and the corner angle, returns the length of the hypotenuse.
// Arguments:
// adj = The length of the side of the right triangle that is adjacent to the primary angle.
// ang = The angle in degrees of the primary corner of the right triangle.
// Example:
// hyp = adj_ang_to_hyp(4,60); // Returns: 8
function adj_ang_to_hyp ( adj , ang ) =
assert ( is_num ( adj ) && adj >= 0 )
assert ( is_num ( ang ) && ang >= 0 && ang < 90 )
adj / cos ( ang ) ;
// Function: opp_ang_to_hyp()
// Usage:
// hyp = opp_ang_to_hyp(opp,ang);
// Description:
// For a right triangle, given the length of the opposite side, and the corner angle, returns the length of the hypotenuse.
// Arguments:
// opp = The length of the side of the right triangle that is opposite from the primary angle.
// ang = The angle in degrees of the primary corner of the right triangle.
// Example:
// hyp = opp_ang_to_hyp(4,30); // Returns: 8
function opp_ang_to_hyp ( opp , ang ) =
assert ( is_num ( opp ) && opp >= 0 )
assert ( is_num ( ang ) && ang > 0 && ang < = 90 )
opp / sin ( ang ) ;
// Function: hyp_adj_to_ang()
// Usage:
// ang = hyp_adj_to_ang(hyp,adj);
// Description:
// For a right triangle, given the lengths of the hypotenuse and the adjacent sides, returns the angle of the corner.
// Arguments:
// hyp = The length of the hypotenuse of the right triangle.
// adj = The length of the side of the right triangle that is adjacent to the primary angle.
// Example:
// ang = hyp_adj_to_ang(8,4); // Returns: 60 degrees
function hyp_adj_to_ang ( hyp , adj ) =
assert ( is_num ( hyp ) && hyp > 0 )
assert ( is_num ( adj ) && adj >= 0 )
acos ( adj / hyp ) ;
// Function: hyp_opp_to_ang()
// Usage:
// ang = hyp_opp_to_ang(hyp,opp);
// Description:
// For a right triangle, given the lengths of the hypotenuse and the opposite sides, returns the angle of the corner.
// Arguments:
// hyp = The length of the hypotenuse of the right triangle.
// opp = The length of the side of the right triangle that is opposite from the primary angle.
// Example:
// ang = hyp_opp_to_ang(8,4); // Returns: 30 degrees
function hyp_opp_to_ang ( hyp , opp ) =
assert ( is_num ( hyp ) && hyp > 0 )
assert ( is_num ( opp ) && opp >= 0 )
asin ( opp / hyp ) ;
// Function: adj_opp_to_ang()
// Usage:
// ang = adj_opp_to_ang(adj,opp);
// Description:
// For a right triangle, given the lengths of the adjacent and opposite sides, returns the angle of the corner.
// Arguments:
// adj = The length of the side of the right triangle that is adjacent to the primary angle.
// opp = The length of the side of the right triangle that is opposite from the primary angle.
// Example:
// ang = adj_opp_to_ang(sqrt(3)/2,0.5); // Returns: 30 degrees
function adj_opp_to_ang ( adj , opp ) =
assert ( is_num ( adj ) && adj >= 0 )
assert ( is_num ( opp ) && opp >= 0 )
atan2 ( opp , adj ) ;
2019-07-17 08:49:51 +00:00
2019-07-10 20:37:23 +00:00
// Function: triangle_area()
2019-04-20 00:02:17 +00:00
// Usage:
2019-07-10 20:37:23 +00:00
// triangle_area(a,b,c);
2019-04-20 00:02:17 +00:00
// Description:
2019-07-10 20:37:23 +00:00
// Returns the area of a triangle formed between three 2D or 3D vertices.
// Result will be negative if the points are 2D and in in clockwise order.
2019-04-20 00:02:17 +00:00
// Examples:
2019-07-10 20:37:23 +00:00
// triangle_area([0,0], [5,10], [10,0]); // Returns -50
// triangle_area([10,0], [5,10], [0,0]); // Returns 50
function triangle_area ( a , b , c ) =
len ( a ) = = 3 ? 0.5 * norm ( cross ( c - a , c - b ) ) : (
2019-06-26 00:57:03 +00:00
a . x * ( b . y - c . y ) +
b . x * ( c . y - a . y ) +
2019-04-20 00:02:17 +00:00
c . x * ( a . y - b . y )
) / 2 ;
2019-06-17 06:57:05 +00:00
2019-04-20 00:02:17 +00:00
// Section: Planes
// Function: plane3pt()
// Usage:
// plane3pt(p1, p2, p3);
// Description:
2019-05-01 06:45:05 +00:00
// Generates the cartesian equation of a plane from three non-collinear points on the plane.
2019-04-20 00:02:17 +00:00
// Returns [A,B,C,D] where Ax+By+Cz+D=0 is the equation of a plane.
// Arguments:
// p1 = The first point on the plane.
// p2 = The second point on the plane.
// p3 = The third point on the plane.
function plane3pt ( p1 , p2 , p3 ) =
2019-05-01 06:45:05 +00:00
let (
p1 = point3d ( p1 ) ,
p2 = point3d ( p2 ) ,
p3 = point3d ( p3 ) ,
normal = normalize ( cross ( p3 - p1 , p2 - p1 ) )
) concat ( normal , [ normal * p1 ] ) ;
2019-04-20 00:02:17 +00:00
// Function: plane3pt_indexed()
// Usage:
// plane3pt_indexed(points, i1, i2, i3);
// Description:
2019-10-22 01:59:35 +00:00
// Given a list of points, and the indices of three of those points,
2019-04-20 00:02:17 +00:00
// generates the cartesian equation of a plane that those points all
// lie on. Requires that the three indexed points be non-collinear.
// Returns [A,B,C,D] where Ax+By+Cz+D=0 is the equation of a plane.
// Arguments:
// points = A list of points.
// i1 = The index into `points` of the first point on the plane.
// i2 = The index into `points` of the second point on the plane.
// i3 = The index into `points` of the third point on the plane.
function plane3pt_indexed ( points , i1 , i2 , i3 ) =
let (
p1 = points [ i1 ] ,
p2 = points [ i2 ] ,
2019-05-01 06:45:05 +00:00
p3 = points [ i3 ]
) plane3pt ( p1 , p2 , p3 ) ;
2019-04-20 00:02:17 +00:00
2019-08-21 03:47:29 +00:00
// Function: plane_from_pointslist()
// Usage:
// plane_from_pointslist(points);
// Description:
2019-10-22 01:59:35 +00:00
// Given a list of 3 or more coplanar points, returns the cartesian equation of a plane.
2019-08-21 03:47:29 +00:00
// Returns [A,B,C,D] where Ax+By+Cz+D=0 is the equation of the plane.
function plane_from_pointslist ( points ) =
let (
points = deduplicate ( points ) ,
2019-11-04 02:45:53 +00:00
indices = sort ( find_noncollinear_points ( points ) ) ,
2019-08-21 03:47:29 +00:00
p1 = points [ indices [ 0 ] ] ,
p2 = points [ indices [ 1 ] ] ,
2019-10-30 05:46:00 +00:00
p3 = points [ indices [ 2 ] ] ,
2019-11-04 02:45:53 +00:00
plane = plane3pt ( p1 , p2 , p3 )
) plane ;
2019-08-21 03:47:29 +00:00
2019-05-29 01:44:41 +00:00
// Function: plane_normal()
// Usage:
// plane_normal(plane);
// Description:
// Returns the normal vector for the given plane.
function plane_normal ( plane ) = [ for ( i = [ 0 : 2 ] ) plane [ i ] ] ;
2019-04-20 00:02:17 +00:00
// Function: distance_from_plane()
// Usage:
// distance_from_plane(plane, point)
// Description:
// Given a plane as [A,B,C,D] where the cartesian equation for that plane
// is Ax+By+Cz+D=0, determines how far from that plane the given point is.
// The returned distance will be positive if the point is in front of the
// plane; on the same side of the plane as the normal of that plane points
// towards. If the point is behind the plane, then the distance returned
// will be negative. The normal of the plane is the same as [A,B,C].
// Arguments:
// plane = The [A,B,C,D] values for the equation of the plane.
// point = The point to test.
function distance_from_plane ( plane , point ) =
2019-10-30 05:46:00 +00:00
[ plane . x , plane . y , plane . z ] * point3d ( point ) - plane [ 3 ] ;
2019-04-20 00:02:17 +00:00
// Function: coplanar()
// Usage:
// coplanar(plane, point);
// Description:
// Given a plane as [A,B,C,D] where the cartesian equation for that plane
// is Ax+By+Cz+D=0, determines if the given point is on that plane.
// Returns true if the point is on that plane.
// Arguments:
// plane = The [A,B,C,D] values for the equation of the plane.
// point = The point to test.
function coplanar ( plane , point ) =
abs ( distance_from_plane ( plane , point ) ) < = EPSILON ;
// Function: in_front_of_plane()
// Usage:
// in_front_of_plane(plane, point);
// Description:
// Given a plane as [A,B,C,D] where the cartesian equation for that plane
// is Ax+By+Cz+D=0, determines if the given point is on the side of that
// plane that the normal points towards. The normal of the plane is the
// same as [A,B,C].
// Arguments:
// plane = The [A,B,C,D] values for the equation of the plane.
// point = The point to test.
function in_front_of_plane ( plane , point ) =
distance_from_plane ( plane , point ) > EPSILON ;
// Section: Paths and Polygons
2019-06-17 07:10:01 +00:00
// Function: is_path()
// Usage:
// is_path(x);
// Description:
2019-07-10 20:12:32 +00:00
// Returns true if the given item looks like a path. A path is defined as a list of two or more points.
function is_path ( x ) = is_list ( x ) && is_vector ( x . x ) && len ( x ) > 1 ;
2019-06-17 07:10:01 +00:00
// Function: is_closed_path()
// Usage:
// is_closed_path(path, [eps]);
// Description:
// Returns true if the first and last points in the given path are coincident.
2019-06-18 01:55:10 +00:00
function is_closed_path ( path , eps = EPSILON ) = approx ( path [ 0 ] , path [ len ( path ) - 1 ] , eps = eps ) ;
2019-06-17 07:10:01 +00:00
2019-07-19 04:48:32 +00:00
// Function: close_path()
2019-06-17 07:10:01 +00:00
// Usage:
// close_path(path);
// Description:
// If a path's last point does not coincide with its first point, closes the path so it does.
2019-06-18 01:55:10 +00:00
function close_path ( path , eps = EPSILON ) = is_closed_path ( path , eps = eps ) ? path : concat ( path , [ path [ 0 ] ] ) ;
2019-06-17 07:10:01 +00:00
2019-07-19 04:48:32 +00:00
// Function: cleanup_path()
// Usage:
// cleanup_path(path);
// Description:
// If a path's last point coincides with its first point, deletes the last point in the path.
function cleanup_path ( path , eps = EPSILON ) = is_closed_path ( path , eps = eps ) ? select ( path , 0 , - 2 ) : path ;
2019-09-19 09:44:28 +00:00
// Function: path_self_intersections()
// Usage:
// isects = path_self_intersections(path, [eps]);
// Description:
// Locates all self intersections of the given path. Returns a list of intersections, where
// each intersection is a list like [POINT, SEGNUM1, PROPORTION1, SEGNUM2, PROPORTION2] where
// POINT is the coordinates of the intersection point, SEGNUMs are the integer indices of the
// intersecting segments along the path, and the PROPORTIONS are the 0.0 to 1.0 proportions
// of how far along those segments they intersect at. A proportion of 0.0 indicates the start
// of the segment, and a proportion of 1.0 indicates the end of the segment.
// Arguments:
// path = The path to find self intersections of.
// closed = If true, treat path like a closed polygon. Default: true
// eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9)
// Example(2D):
// path = [
// [-100,100], [0,-50], [100,100], [100,-100], [0,50], [-100,-100]
// ];
// isects = path_self_intersections(path, closed=true);
// // isects == [[[-33.3333, 0], 0, 0.666667, 4, 0.333333], [[33.3333, 0], 1, 0.333333, 3, 0.666667]]
// stroke(path, closed=true, width=1);
// for (isect=isects) translate(isect[0]) color("blue") sphere(d=10);
function path_self_intersections ( path , closed = true , eps = EPSILON ) =
let (
2019-09-25 06:32:30 +00:00
path = cleanup_path ( path , eps = eps ) ,
plen = len ( path )
2019-09-19 09:44:28 +00:00
) [
2019-09-25 06:32:30 +00:00
for ( i = [ 0 : 1 : plen - ( closed ? 2 : 3 ) ] , j = [ i + 1 : 1 : plen - ( closed ? 1 : 2 ) ] ) let (
a1 = path [ i ] ,
a2 = path [ ( i + 1 ) % plen ] ,
b1 = path [ j ] ,
b2 = path [ ( j + 1 ) % plen ] ,
isect =
( max ( a1 . x , a2 . x ) < min ( b1 . x , b2 . x ) ) ? undef :
( min ( a1 . x , a2 . x ) > max ( b1 . x , b2 . x ) ) ? undef :
( max ( a1 . y , a2 . y ) < min ( b1 . y , b2 . y ) ) ? undef :
( min ( a1 . y , a2 . y ) > max ( b1 . y , b2 . y ) ) ? undef :
let (
c = a1 - a2 ,
d = b1 - b2 ,
denom = ( c . x * d . y ) - ( c . y * d . x )
) abs ( denom ) < eps ? undef : let (
e = a1 - b1 ,
t = ( ( e . x * d . y ) - ( e . y * d . x ) ) / denom ,
u = ( ( e . x * c . y ) - ( e . y * c . x ) ) / denom
) [ a1 + t * ( a2 - a1 ) , t , u ]
) if (
isect ! = undef &&
isect [ 1 ] > eps && isect [ 1 ] < = 1 + eps &&
isect [ 2 ] > eps && isect [ 2 ] < = 1 + eps
) [ isect [ 0 ] , i , isect [ 1 ] , j , isect [ 2 ] ]
2019-09-19 09:44:28 +00:00
] ;
2019-09-24 10:58:45 +00:00
function _tag_self_crossing_subpaths ( path , closed = true , eps = EPSILON ) =
let (
subpaths = split_path_at_self_crossings (
path , closed = closed , eps = eps
)
) [
for ( subpath = subpaths ) let (
seg = select ( subpath , 0 , 1 ) ,
mp = mean ( seg ) ,
n = line_normal ( seg ) / 2048 ,
p1 = mp + n ,
p2 = mp - n ,
p1in = point_in_polygon ( p1 , path ) >= 0 ,
p2in = point_in_polygon ( p2 , path ) >= 0 ,
tag = ( p1in && p2in ) ? "I" : "O"
) [ tag , subpath ]
] ;
2019-09-19 09:44:28 +00:00
// Function: decompose_path()
// Usage:
// splitpaths = decompose_path(path, [closed], [eps]);
// Description:
2019-09-24 10:58:45 +00:00
// Given a possibly self-crossing path, decompose it into non-crossing paths that are on the perimeter
// of the areas bounded by that path.
2019-09-19 09:44:28 +00:00
// Arguments:
// path = The path to split up.
// closed = If true, treat path like a closed polygon. Default: true
// eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9)
// Example(2D):
// path = [
// [-100,100], [0,-50], [100,100], [100,-100], [0,50], [-100,-100]
// ];
// splitpaths = decompose_path(path, closed=true);
// rainbow(splitpaths) stroke($item, closed=true, width=3);
function decompose_path ( path , closed = true , eps = EPSILON ) =
let (
path = cleanup_path ( path , eps = eps ) ,
2019-09-24 10:58:45 +00:00
tagged = _tag_self_crossing_subpaths ( path , closed = closed , eps = eps ) ,
kept = [ for ( sub = tagged ) if ( sub [ 0 ] = = "O" ) sub [ 1 ] ] ,
outregion = assemble_path_fragments ( kept , eps = eps )
) outregion ;
2019-09-19 09:44:28 +00:00
2019-07-19 04:48:32 +00:00
// Function: path_subselect()
2019-06-17 07:10:01 +00:00
// Usage:
2019-09-21 08:57:50 +00:00
// path_subselect(path,s1,u1,s2,u2,[closed]):
2019-06-17 07:10:01 +00:00
// Description:
// Returns a portion of a path, from between the `u1` part of segment `s1`, to the `u2` part of
// segment `s2`. Both `u1` and `u2` are values between 0.0 and 1.0, inclusive, where 0 is the start
// of the segment, and 1 is the end. Both `s1` and `s2` are integers, where 0 is the first segment.
// Arguments:
2019-09-21 08:57:50 +00:00
// path = The path to get a section of.
2019-06-17 07:10:01 +00:00
// s1 = The number of the starting segment.
// u1 = The proportion along the starting segment, between 0.0 and 1.0, inclusive.
// s2 = The number of the ending segment.
// u2 = The proportion along the ending segment, between 0.0 and 1.0, inclusive.
2019-09-21 08:57:50 +00:00
// closed = If true, treat path as a closed polygon.
function path_subselect ( path , s1 , u1 , s2 , u2 , closed = false ) =
2019-06-17 07:10:01 +00:00
let (
2019-09-21 08:57:50 +00:00
lp = len ( path ) ,
l = lp - ( closed ? 0 : 1 ) ,
2019-06-17 07:10:01 +00:00
u1 = s1 < 0 ? 0 : s1 > l ? 1 : u1 ,
u2 = s2 < 0 ? 0 : s2 > l ? 1 : u2 ,
s1 = constrain ( s1 , 0 , l ) ,
s2 = constrain ( s2 , 0 , l ) ,
pathout = concat (
2019-09-21 08:57:50 +00:00
( s1 < l && u1 < 1 ) ? [ lerp ( path [ s1 ] , path [ ( s1 + 1 ) % lp ] , u1 ) ] : [ ] ,
2019-06-17 07:10:01 +00:00
[ for ( i = [ s1 + 1 : 1 : s2 ] ) path [ i ] ] ,
2019-09-21 08:57:50 +00:00
( s2 < l && u2 > 0 ) ? [ lerp ( path [ s2 ] , path [ ( s2 + 1 ) % lp ] , u2 ) ] : [ ]
2019-06-17 07:10:01 +00:00
)
) pathout ;
2019-07-10 20:37:23 +00:00
// Function: polygon_area()
// Usage:
// area = polygon_area(vertices);
// Description:
// Given a polygon, returns the area of that polygon. If the polygon is self-crossing, the results are undefined.
function polygon_area ( vertices ) =
0.5 * sum ( [ for ( i = [ 0 : len ( vertices ) - 1 ] ) det2 ( select ( vertices , i , i + 1 ) ) ] ) ;
2019-08-22 06:52:03 +00:00
// Function: polygon_shift()
// Usage:
// polygon_shift(poly, i);
// Description:
// Given a polygon `poly`, rotates the point ordering so that the first point in the polygon path is the one at index `i`.
// Arguments:
// poly = The list of points in the polygon path.
// i = The index of the point to shift to the front of the path.
// Example:
// polygon_shift([[3,4], [8,2], [0,2], [-4,0]], 2); // Returns [[0,2], [-4,0], [3,4], [8,2]]
2019-09-19 09:44:28 +00:00
function polygon_shift ( poly , i ) =
2019-10-30 08:19:34 +00:00
list_rotate ( cleanup_path ( poly ) , i ) ;
2019-08-22 06:52:03 +00:00
2019-08-21 03:47:29 +00:00
// Function: polygon_shift_to_closest_point()
// Usage:
// polygon_shift_to_closest_point(path, pt);
// Description:
// Given a polygon `path`, rotates the point ordering so that the first point in the path is the one closest to the given point `pt`.
function polygon_shift_to_closest_point ( path , pt ) =
let (
path = cleanup_path ( path ) ,
closest = path_closest_point ( path , pt ) ,
seg = select ( path , closest [ 0 ] , closest [ 0 ] + 1 ) ,
u = norm ( closest [ 1 ] - seg [ 0 ] ) / norm ( seg [ 1 ] - seg [ 0 ] ) ,
segnum = closest [ 0 ] + ( u > 0.5 ? 1 : 0 )
) select ( path , segnum , segnum + len ( path ) - 1 ) ;
2019-08-20 04:11:19 +00:00
// Function: first_noncollinear()
// Usage:
// first_noncollinear(i1, i2, points);
// Description:
2019-10-31 00:44:03 +00:00
// Returns index of the first point in `points` that is not collinear with the points indexed by `i1` and `i2`.
2019-08-20 04:11:19 +00:00
// Arguments:
// i1 = The first point.
// i2 = The second point.
// points = The list of points to find a non-collinear point from.
2019-10-25 22:16:48 +00:00
function first_noncollinear ( i1 , i2 , points ) =
[ for ( j = idx ( points ) ) if ( j ! = i1 && j ! = i2 && ! collinear_indexed ( points , i1 , i2 , j ) ) j ] [ 0 ] ;
2019-08-20 04:11:19 +00:00
2019-10-25 22:16:48 +00:00
// Function: find_noncollinear_points()
2019-08-20 04:11:19 +00:00
// Usage:
// find_noncollinear_points(points);
// Description:
2019-10-22 01:59:35 +00:00
// Finds the indices of three good non-collinear points from the points list `points`.
2019-08-20 04:11:19 +00:00
function find_noncollinear_points ( points ) =
let (
a = 0 ,
2019-10-25 22:16:48 +00:00
b = furthest_point ( points [ a ] , points ) ,
2019-10-31 00:44:03 +00:00
c = max_index ( [
for ( p = points )
sin ( vector_angle ( points [ a ] - p , points [ b ] - p ) ) *
norm ( p - points [ a ] ) * norm ( p - points [ b ] )
] )
2019-08-20 04:11:19 +00:00
) [ a , b , c ] ;
2019-07-16 00:14:29 +00:00
// Function: centroid()
// Usage:
// centroid(vertices)
// Description:
2019-08-20 04:11:19 +00:00
// Given a simple 2D polygon, returns the coordinates of the polygon's centroid.
2019-07-16 00:14:29 +00:00
// If the polygon is self-intersecting, the results are undefined.
function centroid ( vertices ) =
2019-07-17 08:49:51 +00:00
sum ( [
for ( i = [ 0 : len ( vertices ) - 1 ] )
let ( segment = select ( vertices , i , i + 1 ) )
det2 ( segment ) * sum ( segment )
] ) / 6 / polygon_area ( vertices ) ;
2019-07-16 00:14:29 +00:00
2019-09-24 10:58:45 +00:00
function _extreme_angle_fragment ( seg , fragments , rightmost = true , eps = EPSILON ) =
! fragments ? [ undef , [ ] ] :
let (
delta = seg [ 1 ] - seg [ 0 ] ,
segang = atan2 ( delta . y , delta . x ) ,
frags = [
for ( i = idx ( fragments ) ) let (
fragment = fragments [ i ] ,
fwdmatch = approx ( seg [ 1 ] , fragment [ 0 ] , eps = eps ) ,
bakmatch = approx ( seg [ 1 ] , select ( fragment , - 1 ) , eps = eps )
) [
fwdmatch ,
bakmatch ,
bakmatch ? reverse ( fragment ) : fragment
]
] ,
angs = [
for ( frag = frags )
( frag [ 0 ] || frag [ 1 ] ) ? let (
delta2 = frag [ 2 ] [ 1 ] - frag [ 2 ] [ 0 ] ,
segang2 = atan2 ( delta2 . y , delta2 . x )
) modang ( segang2 - segang ) : (
rightmost ? 999 : - 999
)
] ,
fi = rightmost ? min_index ( angs ) : max_index ( angs )
) abs ( angs [ fi ] ) > 360 ? [ undef , fragments ] : let (
remainder = [ for ( i = idx ( fragments ) ) if ( i ! = fi ) fragments [ i ] ] ,
frag = frags [ fi ] ,
foundfrag = frag [ 2 ]
) [ foundfrag , remainder ] ;
2019-09-25 06:32:30 +00:00
// Function: assemble_a_path_from_fragments()
2019-06-17 07:10:01 +00:00
// Usage:
2019-09-25 06:32:30 +00:00
// assemble_a_path_from_fragments(subpaths);
2019-06-17 07:10:01 +00:00
// Description:
2019-09-25 06:32:30 +00:00
// Given a list of incomplete paths, assembles them together into one complete closed path, and
// remainder fragments. Returns [PATH, FRAGMENTS] where FRAGMENTS is the list of remaining
// polyline path fragments.
// Arguments:
// fragments = List of polylines to be assembled into complete polygons.
// rightmost = If true, assemble paths using rightmost turns. Leftmost if false.
// eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9)
function assemble_a_path_from_fragments ( fragments , rightmost = true , eps = EPSILON ) =
2019-09-24 10:58:45 +00:00
len ( fragments ) = = 0 ? _finished :
2019-06-17 07:10:01 +00:00
let (
2019-09-24 10:58:45 +00:00
path = fragments [ 0 ] ,
2019-09-25 06:32:30 +00:00
newfrags = slice ( fragments , 1 , - 1 )
2019-06-27 01:56:33 +00:00
) is_closed_path ( path , eps = eps ) ? (
2019-09-24 10:58:45 +00:00
// starting fragment is already closed
2019-09-25 06:32:30 +00:00
[ path , newfrags ]
2019-06-27 01:56:33 +00:00
) : let (
2019-09-24 10:58:45 +00:00
// Find rightmost/leftmost continuation fragment
2019-09-25 06:32:30 +00:00
seg = select ( path , - 2 , - 1 ) ,
frags = slice ( fragments , 1 , - 1 ) ,
extrema = _extreme_angle_fragment ( seg = seg , fragments = frags , rightmost = rightmost , eps = eps ) ,
2019-09-24 10:58:45 +00:00
foundfrag = extrema [ 0 ] ,
remainder = extrema [ 1 ] ,
2019-09-25 06:32:30 +00:00
newfrags = remainder
2019-09-24 10:58:45 +00:00
) is_undef ( foundfrag ) ? (
// No remaining fragments connect! INCOMPLETE PATH!
// Treat it as complete.
2019-09-25 06:32:30 +00:00
[ path , newfrags ]
2019-09-24 10:58:45 +00:00
) : is_closed_path ( foundfrag , eps = eps ) ? (
let (
2019-09-25 06:32:30 +00:00
newfrags = concat ( [ path ] , remainder )
2019-06-17 07:10:01 +00:00
)
2019-09-24 10:58:45 +00:00
// Found fragment is already closed
2019-09-25 06:32:30 +00:00
[ foundfrag , newfrags ]
2019-07-05 06:47:42 +00:00
) : let (
2019-09-24 10:58:45 +00:00
fragend = select ( foundfrag , - 1 ) ,
hits = [ for ( i = idx ( path , end = - 2 ) ) if ( approx ( path [ i ] , fragend , eps = eps ) ) i ]
) hits ? (
let (
// Found fragment intersects with initial path
hitidx = select ( hits , - 1 ) ,
newpath = slice ( path , 0 , hitidx + 1 ) ,
newfrags = concat ( len ( newpath ) > 1 ? [ newpath ] : [ ] , remainder ) ,
2019-09-25 06:32:30 +00:00
outpath = concat ( slice ( path , hitidx , - 2 ) , foundfrag )
2019-06-17 07:10:01 +00:00
)
2019-09-25 06:32:30 +00:00
[ outpath , newfrags ]
2019-09-24 10:58:45 +00:00
) : let (
// Path still incomplete. Continue building it.
newpath = concat ( path , slice ( foundfrag , 1 , - 1 ) ) ,
newfrags = concat ( [ newpath ] , remainder )
)
2019-09-25 06:32:30 +00:00
assemble_a_path_from_fragments (
fragments = newfrags ,
rightmost = rightmost ,
eps = eps
) ;
// Function: assemble_path_fragments()
// Usage:
// assemble_path_fragments(subpaths);
// Description:
// Given a list of incomplete paths, assembles them together into complete closed paths if it can.
// Arguments:
// fragments = List of polylines to be assembled into complete polygons.
// rightmost = If true, assemble paths using rightmost turns. Leftmost if false.
// eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9)
function assemble_path_fragments ( fragments , rightmost = true , eps = EPSILON , _finished = [ ] ) =
len ( fragments ) = = 0 ? _finished :
let (
result = assemble_a_path_from_fragments (
fragments = fragments ,
rightmost = rightmost ,
eps = eps
) ,
newpath = result [ 0 ] ,
remainder = result [ 1 ] ,
finished = concat ( _finished , [ newpath ] )
) assemble_path_fragments (
fragments = remainder ,
rightmost = rightmost , eps = eps ,
_finished = finished
) ;
2019-06-17 07:10:01 +00:00
2019-04-20 00:02:17 +00:00
// Function: simplify_path()
// Description:
// Takes a path and removes unnecessary collinear points.
// Usage:
// simplify_path(path, [eps])
// Arguments:
// path = A list of 2D path points.
2019-05-02 02:28:02 +00:00
// eps = Largest positional variance allowed. Default: `EPSILON` (1-e9)
function simplify_path ( path , eps = EPSILON ) =
len ( path ) < = 2 ? path : let (
2019-05-27 05:34:46 +00:00
indices = concat ( [ 0 ] , [ for ( i = [ 1 : 1 : len ( path ) - 2 ] ) if ( ! collinear_indexed ( path , i - 1 , i , i + 1 , eps = eps ) ) i ] , [ len ( path ) - 1 ] )
2019-05-02 02:28:02 +00:00
) [ for ( i = indices ) path [ i ] ] ;
2019-04-20 00:02:17 +00:00
// Function: simplify_path_indexed()
// Description:
2019-10-22 01:59:35 +00:00
// Takes a list of points, and a path as a list of indices into `points`,
2019-04-20 00:02:17 +00:00
// and removes all path points that are unecessarily collinear.
// Usage:
// simplify_path_indexed(path, eps)
// Arguments:
// points = A list of points.
2019-10-22 01:59:35 +00:00
// path = A list of indices into `points` that forms a path.
2019-04-20 00:02:17 +00:00
// eps = Largest angle variance allowed. Default: EPSILON (1-e9) degrees.
2019-05-02 02:28:02 +00:00
function simplify_path_indexed ( points , path , eps = EPSILON ) =
len ( path ) < = 2 ? path : let (
2019-05-27 05:34:46 +00:00
indices = concat ( [ 0 ] , [ for ( i = [ 1 : 1 : len ( path ) - 2 ] ) if ( ! collinear_indexed ( points , path [ i - 1 ] , path [ i ] , path [ i + 1 ] , eps = eps ) ) i ] , [ len ( path ) - 1 ] )
2019-05-02 02:28:02 +00:00
) [ for ( i = indices ) path [ i ] ] ;
2019-04-20 00:02:17 +00:00
// Function: point_in_polygon()
// Usage:
// point_in_polygon(point, path)
// Description:
// This function tests whether the given point is inside, outside or on the boundary of
2019-05-29 01:44:41 +00:00
// the specified 2D polygon using the Winding Number method.
// The polygon is given as a list of 2D points, not including the repeated end point.
2019-04-20 00:02:17 +00:00
// Returns -1 if the point is outside the polyon.
// Returns 0 if the point is on the boundary.
// Returns 1 if the point lies in the interior.
// The polygon does not need to be simple: it can have self-intersections.
// But the polygon cannot have holes (it must be simply connected).
// Rounding error may give mixed results for points on or near the boundary.
// Arguments:
// point = The point to check position of.
// path = The list of 2D path points forming the perimeter of the polygon.
2019-06-18 01:55:10 +00:00
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
function point_in_polygon ( point , path , eps = EPSILON ) =
2019-07-05 06:47:42 +00:00
// Original algorithm from http://geomalgorithms.com/a03-_inclusion.html
2019-06-26 00:57:03 +00:00
// Does the point lie on any edges? If so return 0.
2019-07-05 06:47:42 +00:00
sum ( [ for ( i = [ 0 : 1 : len ( path ) - 1 ] ) let ( seg = select ( path , i , i + 1 ) ) if ( ! approx ( seg [ 0 ] , seg [ 1 ] , eps = eps ) ) point_on_segment2d ( point , seg , eps = eps ) ? 1 : 0 ] ) > 0 ? 0 :
2019-04-20 00:02:17 +00:00
// Otherwise compute winding number and return 1 for interior, -1 for exterior
2019-07-05 06:47:42 +00:00
sum ( [ for ( i = [ 0 : 1 : len ( path ) - 1 ] ) let ( seg = select ( path , i , i + 1 ) ) if ( ! approx ( seg [ 0 ] , seg [ 1 ] , eps = eps ) ) _point_above_below_segment ( point , seg ) ] ) ! = 0 ? 1 : - 1 ;
2019-04-20 00:02:17 +00:00
// Function: pointlist_bounds()
// Usage:
// pointlist_bounds(pts);
// Description:
2019-05-29 01:44:41 +00:00
// Finds the bounds containing all the 2D or 3D points in `pts`.
2019-08-16 09:06:04 +00:00
// Returns `[[MINX, MINY, MINZ], [MAXX, MAXY, MAXZ]]`
2019-04-20 00:02:17 +00:00
// Arguments:
// pts = List of points.
function pointlist_bounds ( pts ) = [
[ for ( a = [ 0 : 2 ] ) min ( [ for ( x = pts ) point3d ( x ) [ a ] ] ) ] ,
[ for ( a = [ 0 : 2 ] ) max ( [ for ( x = pts ) point3d ( x ) [ a ] ] ) ]
] ;
2019-08-20 04:11:19 +00:00
// Function: closest_point()
// Usage:
// closest_point(pt, points);
// Description:
2019-10-25 22:16:48 +00:00
// Given a list of `points`, finds the index of the closest point to `pt`.
2019-08-20 04:11:19 +00:00
// Arguments:
// pt = The point to find the closest point to.
// points = The list of points to search.
function closest_point ( pt , points ) =
2019-10-25 22:16:48 +00:00
min_index ( [ for ( p = points ) norm ( p - pt ) ] ) ;
2019-08-20 04:11:19 +00:00
// Function: furthest_point()
// Usage:
// furthest_point(pt, points);
// Description:
2019-10-25 22:16:48 +00:00
// Given a list of `points`, finds the index of the furthest point from `pt`.
2019-08-20 04:11:19 +00:00
// Arguments:
// pt = The point to find the farthest point from.
// points = The list of points to search.
2019-10-25 22:16:48 +00:00
// Example:
2019-08-20 04:11:19 +00:00
function furthest_point ( pt , points ) =
2019-10-25 22:16:48 +00:00
max_index ( [ for ( p = points ) norm ( p - pt ) ] ) ;
2019-08-20 04:11:19 +00:00
2019-08-21 03:47:29 +00:00
// Function: polygon_is_clockwise()
2019-05-29 01:44:41 +00:00
// Usage:
2019-08-21 03:47:29 +00:00
// polygon_is_clockwise(path);
2019-05-29 01:44:41 +00:00
// Description:
// Return true if the given 2D simple polygon is in clockwise order, false otherwise.
// Results for complex (self-intersecting) polygon are indeterminate.
// Arguments:
// path = The list of 2D path points for the perimeter of the polygon.
2019-08-21 03:47:29 +00:00
function polygon_is_clockwise ( path ) =
2019-09-19 09:44:28 +00:00
let (
2019-06-26 00:57:03 +00:00
minx = min ( subindex ( path , 0 ) ) ,
lowind = search ( minx , path , 0 , 0 ) ,
lowpts = select ( path , lowind ) ,
miny = min ( subindex ( lowpts , 1 ) ) ,
extreme_sub = search ( miny , lowpts , 1 , 1 ) [ 0 ] ,
extreme = select ( lowind , extreme_sub )
) det2 ( [ select ( path , extreme + 1 ) - path [ extreme ] , select ( path , extreme - 1 ) - path [ extreme ] ] ) < 0 ;
2019-05-29 01:44:41 +00:00
2019-08-21 03:47:29 +00:00
// Function: clockwise_polygon()
// Usage:
// clockwise_polygon(path);
// Description:
// Given a polygon path, returns the clockwise winding version of that path.
function clockwise_polygon ( path ) =
2019-11-18 02:19:55 +00:00
polygon_is_clockwise ( path ) ? path : reverse_polygon ( path ) ;
2019-08-21 03:47:29 +00:00
// Function: ccw_polygon()
// Usage:
// ccw_polygon(path);
// Description:
// Given a polygon path, returns the counter-clockwise winding version of that path.
function ccw_polygon ( path ) =
2019-11-18 02:19:55 +00:00
polygon_is_clockwise ( path ) ? reverse_polygon ( path ) : path ;
// Function: reverse_polygon()
// Usage:
// reverse_polygon(poly)
// Description:
// Reverses a polygon's winding direction, while still using the same start point.
function reverse_polygon ( poly ) =
let ( lp = len ( poly ) ) [ for ( i = idx ( poly ) ) poly [ ( lp - i ) % lp ] ] ;
2019-08-21 03:47:29 +00:00
2019-06-17 06:57:05 +00:00
// Section: Regions and Boolean 2D Geometry
// Function: is_region()
// Usage:
// is_region(x);
// Description:
2019-07-10 20:12:32 +00:00
// Returns true if the given item looks like a region. A region is defined as a list of zero or more paths.
2019-06-17 06:57:05 +00:00
function is_region ( x ) = is_list ( x ) && is_path ( x . x ) ;
2019-07-19 04:48:32 +00:00
// Function: close_region()
2019-06-17 06:57:05 +00:00
// Usage:
// close_region(region);
// Description:
// Closes all paths within a given region.
2019-06-18 01:55:10 +00:00
function close_region ( region , eps = EPSILON ) = [ for ( path = region ) close_path ( path , eps = eps ) ] ;
2019-06-17 06:57:05 +00:00
2019-07-19 01:27:57 +00:00
// Function: check_and_fix_path()
2019-07-18 23:21:08 +00:00
// Usage:
2019-07-23 02:40:49 +00:00
// check_and_fix_path(path, [valid_dim], [closed])
2019-07-18 23:21:08 +00:00
// Description:
// Checks that the input is a path. If it is a region with one component, converts it to a path.
// valid_dim specfies the allowed dimension of the points in the path.
2019-07-19 01:27:57 +00:00
// If the path is closed, removed duplicate endpoint if present.
// Arguments:
// path = path to process
// valid_dim = list of allowed dimensions for the points in the path, e.g. [2,3] to require 2 or 3 dimensional input. If left undefined do not perform this check. Default: undef
// closed = set to true if the path is closed, which enables a check for endpoint duplication
2019-09-21 08:57:50 +00:00
function check_and_fix_path ( path , valid_dim = undef , closed = false ) =
let (
path = is_region ( path ) ? (
assert ( len ( path ) = = 1 , "Region supplied as path does not have exactly one component" )
path [ 0 ]
) : (
assert ( is_path ( path ) , "Input is not a path" )
path
) ,
dim = array_dim ( path )
)
assert ( dim [ 0 ] > 1 , "Path must have at least 2 points" )
assert ( len ( dim ) = = 2 , "Invalid path: path is either a list of scalars or a list of matrices" )
assert ( is_def ( dim [ 1 ] ) , "Invalid path: entries in the path have variable length" )
let ( valid = is_undef ( valid_dim ) || in_list ( dim [ 1 ] , valid_dim ) )
assert (
valid , str (
"The points on the path have length " ,
dim [ 1 ] , " but length must be " ,
len ( valid_dim ) = = 1 ? valid_dim [ 0 ] : str ( "one of " , valid_dim )
)
)
closed && approx ( path [ 0 ] , select ( path , - 1 ) ) ? slice ( path , 0 , - 2 ) : path ;
2019-07-18 23:21:08 +00:00
2019-06-17 06:57:05 +00:00
2019-07-19 04:48:32 +00:00
// Function: cleanup_region()
// Usage:
// cleanup_region(region);
// Description:
// For all paths in the given region, if the last point coincides with the first point, removes the last point.
2019-09-21 08:57:50 +00:00
// Arguments:
// region = The region to clean up. Given as a list of polygon paths.
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
function cleanup_region ( region , eps = EPSILON ) =
[ for ( path = region ) cleanup_path ( path , eps = eps ) ] ;
2019-07-19 04:48:32 +00:00
2019-08-21 03:47:29 +00:00
// Function: point_in_region()
// Usage:
// point_in_region(point, region);
// Description:
// Tests if a point is inside, outside, or on the border of a region.
// Returns -1 if the point is outside the region.
// Returns 0 if the point is on the boundary.
// Returns 1 if the point lies inside the region.
// Arguments:
// point = The point to test.
// region = The region to test against. Given as a list of polygon paths.
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
function point_in_region ( point , region , eps = EPSILON , _i = 0 , _cnt = 0 ) =
( _i >= len ( region ) ) ? ( ( _cnt % 2 = = 1 ) ? 1 : - 1 ) : let (
pip = point_in_polygon ( point , region [ _i ] , eps = eps )
) pip = = 0 ? 0 : point_in_region ( point , region , eps = eps , _i = _i + 1 , _cnt = _cnt + ( pip > 0 ? 1 : 0 ) ) ;
2019-06-17 06:57:05 +00:00
// Function: region_path_crossings()
// Usage:
// region_path_crossings(path, region);
// Description:
// Returns a sorted list of [SEGMENT, U] that describe where a given path is crossed by a second path.
// Arguments:
// path = The path to find crossings on.
// region = Region to test for crossings of.
2019-09-21 08:57:50 +00:00
// closed = If true, treat path as a closed polygon. Default: true
2019-06-18 01:55:10 +00:00
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
2019-09-21 08:57:50 +00:00
function region_path_crossings ( path , region , closed = true , eps = EPSILON ) = sort ( [
let (
segs = pair ( closed ? close_path ( path ) : cleanup_path ( path ) )
) for (
si = idx ( segs ) ,
p = close_region ( region ) ,
s2 = pair ( p )
) let (
isect = _general_line_intersection ( segs [ si ] , s2 , eps = eps )
2019-06-17 06:57:05 +00:00
) if (
! is_undef ( isect ) &&
2019-06-18 04:44:50 +00:00
isect [ 1 ] >= 0 - eps && isect [ 1 ] < 1 + eps &&
isect [ 2 ] >= 0 - eps && isect [ 2 ] < 1 + eps
)
2019-09-21 08:57:50 +00:00
[ si , isect [ 1 ] ]
2019-06-17 06:57:05 +00:00
] ) ;
2019-06-26 00:57:03 +00:00
function _offset_chamfer ( center , points , delta ) =
let (
dist = sign ( delta ) * norm ( center - line_intersection ( select ( points , [ 0 , 2 ] ) , [ center , points [ 1 ] ] ) ) ,
endline = _shift_segment ( select ( points , [ 0 , 2 ] ) , delta - dist )
) [
line_intersection ( endline , select ( points , [ 0 , 1 ] ) ) ,
line_intersection ( endline , select ( points , [ 1 , 2 ] ) )
] ;
2019-06-27 01:56:33 +00:00
2019-06-26 00:57:03 +00:00
function _shift_segment ( segment , d ) =
move ( d * line_normal ( segment ) , segment ) ;
2019-06-27 01:56:33 +00:00
2019-06-26 00:57:03 +00:00
// Extend to segments to their intersection point. First check if the segments already have a point in common,
// which can happen if two colinear segments are input to the path variant of `offset()`
function _segment_extension ( s1 , s2 ) =
norm ( s1 [ 1 ] - s2 [ 0 ] ) < 1e-6 ? s1 [ 1 ] : line_intersection ( s1 , s2 ) ;
2019-06-27 01:56:33 +00:00
2019-06-26 00:57:03 +00:00
function _makefaces ( direction , startind , good , pointcount , closed ) =
let (
lenlist = list_bset ( good , pointcount ) ,
numfirst = len ( lenlist ) ,
numsecond = sum ( lenlist ) ,
prelim_faces = _makefaces_recurse ( startind , startind + len ( lenlist ) , numfirst , numsecond , lenlist , closed )
)
direction ? [ for ( entry = prelim_faces ) reverse ( entry ) ] : prelim_faces ;
function _makefaces_recurse ( startind1 , startind2 , numfirst , numsecond , lenlist , closed , firstind = 0 , secondind = 0 , faces = [ ] ) =
// We are done if *both* firstind and secondind reach their max value, which is the last point if !closed or one past
// the last point if closed (wrapping around). If you don't check both you can leave a triangular gap in the output.
( ( firstind = = numfirst - ( closed ? 0 : 1 ) ) && ( secondind = = numsecond - ( closed ? 0 : 1 ) ) ) ? faces :
_makefaces_recurse (
startind1 , startind2 , numfirst , numsecond , lenlist , closed , firstind + 1 , secondind + lenlist [ firstind ] ,
lenlist [ firstind ] = = 0 ? (
// point in original path has been deleted in offset path, so it has no match. We therefore
// make a triangular face using the current point from the offset (second) path
// (The current point in the second path can be equal to numsecond if firstind is the last point)
concat ( faces , [ [ secondind % numsecond + startind2 , firstind + startind1 , ( firstind + 1 ) % numfirst + startind1 ] ] )
// in this case a point or points exist in the offset path corresponding to the original path
) : (
concat ( faces ,
// First generate triangular faces for all of the extra points (if there are any---loop may be empty)
[ for ( i = [ 0 : 1 : lenlist [ firstind ] - 2 ] ) [ firstind + startind1 , secondind + i + 1 + startind2 , secondind + i + startind2 ] ] ,
// Finish (unconditionally) with a quadrilateral face
[
[
firstind + startind1 ,
( firstind + 1 ) % numfirst + startind1 ,
( secondind + lenlist [ firstind ] ) % numsecond + startind2 ,
( secondind + lenlist [ firstind ] - 1 ) % numsecond + startind2
]
]
)
)
) ;
2019-06-27 01:56:33 +00:00
2019-06-26 00:57:03 +00:00
// Determine which of the shifted segments are good
function _good_segments ( path , d , shiftsegs , closed , quality ) =
let (
maxind = len ( path ) - ( closed ? 1 : 2 ) ,
pathseg = [ for ( i = [ 0 : maxind ] ) select ( path , i + 1 ) - path [ i ] ] ,
pathseg_len = [ for ( seg = pathseg ) norm ( seg ) ] ,
pathseg_unit = [ for ( i = [ 0 : maxind ] ) pathseg [ i ] / pathseg_len [ i ] ] ,
// Order matters because as soon as a valid point is found, the test stops
// This order works better for circular paths because they succeed in the center
alpha = concat ( [ for ( i = [ 1 : 1 : quality ] ) i / ( quality + 1 ) ] , [ 0 , 1 ] )
) [
for ( i = [ 0 : len ( shiftsegs ) - 1 ] )
( i > maxind ) ? true :
2019-08-29 01:15:41 +00:00
_segment_good ( path , pathseg_unit , pathseg_len , d - 1e-7 , shiftsegs [ i ] , alpha )
2019-06-26 00:57:03 +00:00
] ;
// Determine if a segment is good (approximately)
// Input is the path, the path segments normalized to unit length, the length of each path segment
// the distance threshold, the segment to test, and the locations on the segment to test (normalized to [0,1])
// The last parameter, index, gives the current alpha index.
//
// A segment is good if any part of it is farther than distance d from the path. The test is expensive, so
// we want to quit as soon as we find a point with distance > d, hence the recursive code structure.
//
// This test is approximate because it only samples the points listed in alpha. Listing more points
// will make the test more accurate, but slower.
function _segment_good ( path , pathseg_unit , pathseg_len , d , seg , alpha , index = 0 ) =
index = = len ( alpha ) ? false :
_point_dist ( path , pathseg_unit , pathseg_len , alpha [ index ] * seg [ 0 ] + ( 1 - alpha [ index ] ) * seg [ 1 ] ) > d ? true :
_segment_good ( path , pathseg_unit , pathseg_len , d , seg , alpha , index + 1 ) ;
// Input is the path, the path segments normalized to unit length, the length of each path segment
// and a test point. Computes the (minimum) distance from the path to the point, taking into
// account that the minimal distance may be anywhere along a path segment, not just at the ends.
function _point_dist ( path , pathseg_unit , pathseg_len , pt ) =
min ( [
for ( i = [ 0 : len ( pathseg_unit ) - 1 ] ) let (
v = pt - path [ i ] ,
projection = v * pathseg_unit [ i ] ,
segdist = projection < 0 ? norm ( pt - path [ i ] ) :
projection > pathseg_len [ i ] ? norm ( pt - select ( path , i + 1 ) ) :
norm ( v - projection * pathseg_unit [ i ] )
) segdist
] ) ;
2019-06-27 01:56:33 +00:00
function _offset_region (
paths , r , delta , chamfer , closed ,
maxstep , check_valid , quality ,
return_faces , firstface_index ,
flip_faces , _acc = [ ] , _i = 0
) =
_i >= len ( paths ) ? _acc :
_offset_region (
paths , _i = _i + 1 ,
_acc = ( paths [ _i ] . x % 2 = = 0 ) ? (
union ( _acc , [
offset (
paths [ _i ] . y ,
r = r , delta = delta , chamfer = chamfer , closed = closed ,
maxstep = maxstep , check_valid = check_valid , quality = quality ,
return_faces = return_faces , firstface_index = firstface_index ,
flip_faces = flip_faces
)
] )
) : (
difference ( _acc , [
offset (
paths [ _i ] . y ,
r = - r , delta = - delta , chamfer = chamfer , closed = closed ,
maxstep = maxstep , check_valid = check_valid , quality = quality ,
return_faces = return_faces , firstface_index = firstface_index ,
flip_faces = flip_faces
)
] )
) ,
r = r , delta = delta , chamfer = chamfer , closed = closed ,
maxstep = maxstep , check_valid = check_valid , quality = quality ,
return_faces = return_faces , firstface_index = firstface_index , flip_faces = flip_faces
) ;
2019-06-26 00:57:03 +00:00
// Function: offset()
//
// Description:
2019-07-16 22:18:00 +00:00
// Takes an input path and returns a path offset by the specified amount. As with the built-in
// offset() module, you can use `r` to specify rounded offset and `delta` to specify offset with
// corners. Positive offsets shift the path to the left (relative to the direction of the path).
2019-07-16 21:17:32 +00:00
//
2019-07-16 22:18:00 +00:00
// When offsets shrink the path, segments cross and become invalid. By default `offset()` checks
// for this situation. To test validity the code checks that segments have distance larger than (r
// or delta) from the input path. This check takes O(N^2) time and may mistakenly eliminate
// segments you wanted included in various situations, so you can disable it if you wish by setting
// check_valid=false. Another situation is that the test is not sufficiently thorough and some
// segments persist that should be eliminated. In this case, increase `quality` to 2 or 3. (This
// increases the number of samples on the segment that are checked.) Run time will increase. In
// some situations you may be able to decrease run time by setting quality to 0, which causes only
// segment ends to be checked.
2019-07-16 21:17:32 +00:00
//
2019-07-16 22:18:00 +00:00
// For construction of polyhedra `offset()` can also return face lists. These list faces between
// the original path and the offset path where the vertices are ordered with the original path
// first, starting at `firstface_index` and the offset path vertices appearing afterwords. The
// direction of the faces can be flipped using `flip_faces`. When you request faces the return
// value is a list: [offset_path, face_list].
2019-06-26 00:57:03 +00:00
// Arguments:
// path = the path to process. A list of 2d points.
// r = offset radius. Distance to offset. Will round over corners.
// delta = offset distance. Distance to offset with pointed corners.
// chamfer = chamfer corners when you specify `delta`. Default: false
// closed = path is a closed curve. Default: False.
// check_valid = perform segment validity check. Default: True.
// quality = validity check quality parameter, a small integer. Default: 1.
// return_faces = return face list. Default: False.
// firstface_index = starting index for face list. Default: 0.
// flip_faces = flip face direction. Default: false
// Example(2D):
// star = star(5, r=100, ir=30);
2019-07-12 20:11:13 +00:00
// #stroke(closed=true, star);
2019-07-18 23:21:08 +00:00
// stroke(closed=true, offset(star, delta=10, closed=true));
2019-06-26 00:57:03 +00:00
// Example(2D):
// star = star(5, r=100, ir=30);
2019-07-12 20:11:13 +00:00
// #stroke(closed=true, star);
2019-07-18 23:21:08 +00:00
// stroke(closed=true, offset(star, delta=10, chamfer=true, closed=true));
2019-06-26 00:57:03 +00:00
// Example(2D):
// star = star(5, r=100, ir=30);
2019-07-12 20:11:13 +00:00
// #stroke(closed=true, star);
2019-07-18 23:21:08 +00:00
// stroke(closed=true, offset(star, r=10, closed=true));
2019-06-26 00:57:03 +00:00
// Example(2D):
// star = star(5, r=100, ir=30);
2019-07-12 20:11:13 +00:00
// #stroke(closed=true, star);
2019-07-18 23:21:08 +00:00
// stroke(closed=true, offset(star, delta=-10, closed=true));
2019-06-26 00:57:03 +00:00
// Example(2D):
// star = star(5, r=100, ir=30);
2019-07-12 20:11:13 +00:00
// #stroke(closed=true, star);
// stroke(closed=true, offset(star, delta=-10, chamfer=true, closed=true));
2019-06-26 00:57:03 +00:00
// Example(2D):
// star = star(5, r=100, ir=30);
2019-07-12 20:11:13 +00:00
// #stroke(closed=true, star);
2019-07-18 23:21:08 +00:00
// stroke(closed=true, offset(star, r=-10, closed=true));
2019-07-16 20:51:53 +00:00
// Example(2D): This case needs `quality=2` for success
// test = [[0,0],[10,0],[10,7],[0,7], [-1,-3]];
2019-07-18 23:21:08 +00:00
// polygon(offset(test,r=-1.9, closed=true, quality=2));
// //polygon(offset(test,r=-1.9, closed=true, quality=1)); // Fails with erroneous 180 deg path error
2019-07-16 20:51:53 +00:00
// %down(.1)polygon(test);
// Example(2D): This case fails if `check_valid=true` when delta is large enough because segments are too close to the opposite side of the curve.
// star = star(5, r=22, ir=13);
2019-07-18 23:21:08 +00:00
// stroke(star,width=.2,closed=true);
2019-07-16 20:51:53 +00:00
// color("green")
2019-07-18 23:21:08 +00:00
// stroke(offset(star, delta=-9, closed=true),width=.2,closed=true); // Works with check_valid=true (the default)
2019-07-16 20:51:53 +00:00
// color("red")
2019-07-18 23:21:08 +00:00
// stroke(offset(star, delta=-10, closed=true, check_valid=false), // Fails if check_valid=true
// width=.2,closed=true);
2019-07-16 20:51:53 +00:00
// Example(2D): But if you use rounding with offset then you need `check_valid=true` when `r` is big enough. It works without the validity check as long as the offset shape retains a some of the straight edges at the star tip, but once the shape shrinks smaller than that, it fails. There is no simple way to get a correct result for the case with `r=10`, because as in the previous example, it will fail if you turn on validity checks.
// star = star(5, r=22, ir=13);
// color("green")
2019-07-18 23:21:08 +00:00
// stroke(offset(star, r=-8, closed=true,check_valid=false), width=.1, closed=true);
2019-07-16 20:51:53 +00:00
// color("red")
2019-07-18 23:21:08 +00:00
// stroke(offset(star, r=-10, closed=true,check_valid=false), width=.1, closed=true);
2019-07-16 20:51:53 +00:00
// Example(2D): The extra triangles in this example show that the validity check cannot be skipped
2019-07-18 23:21:08 +00:00
// ellipse = scale([20,4], p=circle(r=1,$fn=64));
2019-07-16 20:51:53 +00:00
// stroke(ellipse, closed=true, width=0.3);
// stroke(offset(ellipse, r=-3, check_valid=false, closed=true), width=0.3, closed=true);
// Example(2D): The triangles are removed by the validity check
2019-07-18 23:21:08 +00:00
// ellipse = scale([20,4], p=circle(r=1,$fn=64));
2019-07-16 20:51:53 +00:00
// stroke(ellipse, closed=true, width=0.3);
// stroke(offset(ellipse, r=-3, check_valid=true, closed=true), width=0.3, closed=true);
2019-07-18 23:21:08 +00:00
// Example(2D): Open path. The path moves from left to right and the positive offset shifts to the left of the initial red path.
2019-06-26 00:57:03 +00:00
// sinpath = 2*[for(theta=[-180:5:180]) [theta/4,45*sin(theta)]];
// #stroke(sinpath);
// stroke(offset(sinpath, r=17.5));
2019-06-27 01:56:33 +00:00
// Example(2D): Region
// rgn = difference(circle(d=100), union(square([20,40], center=true), square([40,20], center=true)));
2019-07-12 20:11:13 +00:00
// #linear_extrude(height=1.1) for (p=rgn) stroke(closed=true, width=0.5, p);
2019-06-27 01:56:33 +00:00
// region(offset(rgn, r=-5));
2019-06-26 00:57:03 +00:00
function offset (
path , r = undef , delta = undef , chamfer = false ,
maxstep = 0.1 , closed = false , check_valid = true ,
quality = 1 , return_faces = false , firstface_index = 0 ,
flip_faces = false
) =
2019-06-27 01:56:33 +00:00
is_region ( path ) ? (
2019-08-09 04:10:41 +00:00
assert ( ! return_faces , "return_faces not supported for regions." )
2019-06-27 01:56:33 +00:00
let (
2019-08-21 03:47:29 +00:00
path = [ for ( p = path ) polygon_is_clockwise ( p ) ? p : reverse ( p ) ] ,
2019-06-27 01:56:33 +00:00
rgn = exclusive_or ( [ for ( p = path ) [ p ] ] ) ,
pathlist = sort ( idx = 0 , [
for ( i = [ 0 : 1 : len ( rgn ) - 1 ] ) [
2019-07-17 23:33:08 +00:00
sum ( concat ( [ 0 ] , [
2019-06-27 01:56:33 +00:00
for ( j = [ 0 : 1 : len ( rgn ) - 1 ] ) if ( i ! = j )
point_in_polygon ( rgn [ i ] [ 0 ] , rgn [ j ] ) >= 0 ? 1 : 0
2019-07-17 23:33:08 +00:00
] ) ) ,
2019-06-27 01:56:33 +00:00
rgn [ i ]
]
] )
) _offset_region (
pathlist , r = r , delta = delta , chamfer = chamfer , closed = true ,
maxstep = maxstep , check_valid = check_valid , quality = quality ,
return_faces = return_faces , firstface_index = firstface_index ,
flip_faces = flip_faces
)
) : let ( rcount = num_defined ( [ r , delta ] ) )
2019-06-26 00:57:03 +00:00
assert ( rcount = = 1 , "Must define exactly one of 'delta' and 'r'" )
let (
chamfer = is_def ( r ) ? false : chamfer ,
quality = max ( 0 , round ( quality ) ) ,
2019-09-21 08:57:50 +00:00
flip_dir = closed && ! polygon_is_clockwise ( path ) ? - 1 : 1 ,
2019-07-18 23:21:08 +00:00
d = flip_dir * ( is_def ( r ) ? r : delta ) ,
2019-06-26 00:57:03 +00:00
shiftsegs = [ for ( i = [ 0 : len ( path ) - 1 ] ) _shift_segment ( select ( path , i , i + 1 ) , d ) ] ,
// good segments are ones where no point on the segment is less than distance d from any point on the path
good = check_valid ? _good_segments ( path , abs ( d ) , shiftsegs , closed , quality ) : replist ( true , len ( shiftsegs ) ) ,
goodsegs = bselect ( shiftsegs , good ) ,
goodpath = bselect ( path , good )
)
assert ( len ( goodsegs ) > 0 , "Offset of path is degenerate" )
let (
// Extend the shifted segments to their intersection points
sharpcorners = [ for ( i = [ 0 : len ( goodsegs ) - 1 ] ) _segment_extension ( select ( goodsegs , i - 1 ) , select ( goodsegs , i ) ) ] ,
// If some segments are parallel then the extended segments are undefined. This case is not handled
// Note if !closed the last corner doesn't matter, so exclude it
parallelcheck =
( len ( sharpcorners ) = = 2 && ! closed ) ||
all_defined ( select ( sharpcorners , closed ? 0 : 1 , - 1 ) )
)
assert ( parallelcheck , "Path turns back on itself (180 deg turn)" )
let (
// This is a boolean array that indicates whether a corner is an outside or inside corner
// For outside corners, the newcorner is an extension (angle 0), for inside corners, it turns backward
// If either side turns back it is an inside corner---must check both.
// Outside corners can get rounded (if r is specified and there is space to round them)
outsidecorner = [
for ( i = [ 0 : len ( goodsegs ) - 1 ] ) let (
prevseg = select ( goodsegs , i - 1 )
) (
( goodsegs [ i ] [ 1 ] - goodsegs [ i ] [ 0 ] ) *
( goodsegs [ i ] [ 0 ] - sharpcorners [ i ] ) > 0
) && (
( prevseg [ 1 ] - prevseg [ 0 ] ) *
( sharpcorners [ i ] - prevseg [ 1 ] ) > 0
)
] ,
steps = is_def ( delta ) ? [ ] : [
for ( i = [ 0 : len ( goodsegs ) - 1 ] )
ceil (
abs ( r ) * vector_angle (
select ( goodsegs , i - 1 ) [ 1 ] - goodpath [ i ] ,
goodsegs [ i ] [ 0 ] - goodpath [ i ]
) * PI / 180 / maxstep
)
] ,
// If rounding is true then newcorners replaces sharpcorners with rounded arcs where needed
// Otherwise it's the same as sharpcorners
// If rounding is on then newcorners[i] will be the point list that replaces goodpath[i] and newcorners later
// gets flattened. If rounding is off then we set it to [sharpcorners] so we can later flatten it and get
// plain sharpcorners back.
newcorners = is_def ( delta ) && ! chamfer ? [ sharpcorners ] : [
for ( i = [ 0 : len ( goodsegs ) - 1 ] ) (
( ! chamfer && steps [ i ] < = 2 ) //Chamfer all points but only round if steps is 3 or more
|| ! outsidecorner [ i ] // Don't round inside corners
|| ( ! closed && ( i = = 0 || i = = len ( goodsegs ) - 1 ) ) // Don't round ends of an open path
) ? [ sharpcorners [ i ] ] : (
chamfer ?
_offset_chamfer (
goodpath [ i ] , [
select ( goodsegs , i - 1 ) [ 1 ] ,
sharpcorners [ i ] ,
goodsegs [ i ] [ 0 ]
] , d
) :
arc (
cp = goodpath [ i ] ,
points = [
select ( goodsegs , i - 1 ) [ 1 ] ,
goodsegs [ i ] [ 0 ]
] ,
N = steps [ i ]
)
)
] ,
pointcount = ( is_def ( delta ) && ! chamfer ) ?
replist ( 1 , len ( sharpcorners ) ) :
[ for ( i = [ 0 : len ( goodsegs ) - 1 ] ) len ( newcorners [ i ] ) ] ,
start = [ goodsegs [ 0 ] [ 0 ] ] ,
end = [ goodsegs [ len ( goodsegs ) - 2 ] [ 1 ] ] ,
edges = closed ?
flatten ( newcorners ) :
concat ( start , slice ( flatten ( newcorners ) , 1 , - 2 ) , end ) ,
faces = ! return_faces ? [ ] :
_makefaces (
flip_faces , firstface_index , good ,
pointcount , closed
)
) return_faces ? [ edges , faces ] : edges ;
2019-09-21 08:57:50 +00:00
// Function: split_path_at_self_crossings()
// Usage:
// polylines = split_path_at_self_crossings(path, [closed], [eps]);
// Description:
// Splits a path into polyline sections wherever the path crosses itself.
// Splits may occur mid-segment, so new vertices will be created at the intersection points.
// Arguments:
// path = The path to split up.
// closed = If true, treat path as a closed polygon. Default: true
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
// Example(2D):
// path = [ [-100,100], [0,-50], [100,100], [100,-100], [0,50], [-100,-100] ];
// polylines = split_path_at_self_crossings(path);
// rainbow(polylines) stroke($item, closed=false, width=2);
function split_path_at_self_crossings ( path , closed = true , eps = EPSILON ) =
let (
path = cleanup_path ( path , eps = eps ) ,
isects = deduplicate (
eps = eps ,
concat (
[ [ 0 , 0 ] ] ,
sort ( [
for (
a = path_self_intersections ( path , closed = closed , eps = eps ) ,
ss = [ [ a [ 1 ] , a [ 2 ] ] , [ a [ 3 ] , a [ 4 ] ] ]
) if ( ss [ 0 ] ! = undef ) ss
] ) ,
[ [ len ( path ) - ( closed ? 1 : 2 ) , 1 ] ]
)
)
) [
for ( p = pair ( isects ) )
let (
s1 = p [ 0 ] [ 0 ] ,
u1 = p [ 0 ] [ 1 ] ,
s2 = p [ 1 ] [ 0 ] ,
u2 = p [ 1 ] [ 1 ] ,
section = path_subselect ( path , s1 , u1 , s2 , u2 , closed = closed ) ,
outpath = deduplicate ( eps = eps , section )
)
outpath
] ;
// Function: split_path_at_region_crossings()
// Usage:
// polylines = split_path_at_region_crossings(path, region, [eps]);
// Description:
// Splits a path into polyline sections wherever the path crosses the perimeter of a region.
// Splits may occur mid-segment, so new vertices will be created at the intersection points.
// Arguments:
// path = The path to split up.
// region = The region to check for perimeter crossings of.
// closed = If true, treat path as a closed polygon. Default: true
// eps = Acceptable variance. Default: `EPSILON` (1e-9)
// Example(2D):
// path = square(50,center=false);
// region = [circle(d=80), circle(d=40)];
// polylines = split_path_at_region_crossings(path, region);
// color("#aaa") region(region);
// rainbow(polylines) stroke($item, closed=false, width=2);
function split_path_at_region_crossings ( path , region , closed = true , eps = EPSILON ) =
2019-06-17 06:57:05 +00:00
let (
path = deduplicate ( path , eps = eps ) ,
region = [ for ( path = region ) deduplicate ( path , eps = eps ) ] ,
2019-09-21 08:57:50 +00:00
xings = region_path_crossings ( path , region , closed = closed , eps = eps ) ,
2019-06-18 01:55:10 +00:00
crossings = deduplicate (
2019-09-21 08:57:50 +00:00
concat ( [ [ 0 , 0 ] ] , xings , [ [ len ( path ) - 1 , 1 ] ] ) ,
2019-06-18 01:55:10 +00:00
eps = eps
2019-06-18 04:44:50 +00:00
) ,
subpaths = [
for ( p = pair ( crossings ) )
deduplicate ( eps = eps ,
2019-09-21 08:57:50 +00:00
path_subselect ( path , p [ 0 ] [ 0 ] , p [ 0 ] [ 1 ] , p [ 1 ] [ 0 ] , p [ 1 ] [ 1 ] , closed = closed )
2019-06-18 04:44:50 +00:00
)
]
)
subpaths ;
2019-06-17 06:57:05 +00:00
2019-06-18 01:55:10 +00:00
function _tag_subpaths ( path , region , eps = EPSILON ) =
2019-06-17 06:57:05 +00:00
let (
2019-09-21 08:57:50 +00:00
subpaths = split_path_at_region_crossings ( path , region , eps = eps ) ,
2019-06-17 06:57:05 +00:00
tagged = [
2019-06-18 01:55:10 +00:00
for ( sub = subpaths ) let (
subpath = deduplicate ( sub )
) if ( len ( sub ) > 1 ) let (
2019-06-17 06:57:05 +00:00
midpt = lerp ( subpath [ 0 ] , subpath [ 1 ] , 0.5 ) ,
2019-06-18 01:55:10 +00:00
rel = point_in_region ( midpt , region , eps = eps )
2019-06-17 06:57:05 +00:00
) rel < 0 ? [ "O" , subpath ] : rel > 0 ? [ "I" , subpath ] : let (
2019-06-18 01:55:10 +00:00
vec = normalize ( subpath [ 1 ] - subpath [ 0 ] ) ,
perp = rot ( 90 , planar = true , p = vec ) ,
sidept = midpt + perp * 0.01 ,
rel1 = point_in_polygon ( sidept , path , eps = eps ) > 0 ,
rel2 = point_in_region ( sidept , region , eps = eps ) > 0
) rel1 = = rel2 ? [ "S" , subpath ] : [ "U" , subpath ]
2019-06-17 06:57:05 +00:00
]
) tagged ;
2019-06-18 01:55:10 +00:00
function _tag_region_subpaths ( region1 , region2 , eps = EPSILON ) =
[ for ( path = region1 ) each _tag_subpaths ( path , region2 , eps = eps ) ] ;
2019-06-17 06:57:05 +00:00
2019-06-18 01:55:10 +00:00
function _tagged_region ( region1 , region2 , keep1 , keep2 , eps = EPSILON ) =
2019-06-17 06:57:05 +00:00
let (
2019-06-18 01:55:10 +00:00
region1 = close_region ( region1 , eps = eps ) ,
region2 = close_region ( region2 , eps = eps ) ,
tagged1 = _tag_region_subpaths ( region1 , region2 , eps = eps ) ,
tagged2 = _tag_region_subpaths ( region2 , region1 , eps = eps ) ,
2019-06-17 06:57:05 +00:00
tagged = concat (
[ for ( tagpath = tagged1 ) if ( in_list ( tagpath [ 0 ] , keep1 ) ) tagpath [ 1 ] ] ,
[ for ( tagpath = tagged2 ) if ( in_list ( tagpath [ 0 ] , keep2 ) ) tagpath [ 1 ] ]
) ,
2019-06-18 01:55:10 +00:00
outregion = assemble_path_fragments ( tagged , eps = eps )
2019-06-17 06:57:05 +00:00
) outregion ;
2019-06-18 07:46:05 +00:00
// Function&Module: union()
2019-06-17 06:57:05 +00:00
// Usage:
2019-06-18 07:46:05 +00:00
// union() {...}
// region = union(regions);
// region = union(REGION1,REGION2);
// region = union(REGION1,REGION2,REGION3);
2019-06-17 06:57:05 +00:00
// Description:
2019-06-18 07:46:05 +00:00
// When called as a function and given a list of regions, where each region is a list of closed
// 2D paths, returns the boolean union of all given regions. Result is a single region.
// When called as the built-in module, makes the boolean union of the given children.
2019-06-17 06:57:05 +00:00
// Arguments:
// regions = List of regions to union. Each region is a list of closed paths.
// Example(2D):
// shape1 = move([-8,-8,0], p=circle(d=50));
// shape2 = move([ 8, 8,0], p=circle(d=50));
2019-07-12 20:11:13 +00:00
// for (shape = [shape1,shape2]) color("red") stroke(shape, width=0.5, closed=true);
2019-06-17 06:57:05 +00:00
// color("green") region(union(shape1,shape2));
2019-06-18 01:55:10 +00:00
function union ( regions = [ ] , b = undef , c = undef , eps = EPSILON ) =
b ! = undef ? union ( concat ( [ regions ] , [ b ] , c = = undef ? [ ] : [ c ] ) , eps = eps ) :
2019-06-17 06:57:05 +00:00
len ( regions ) < = 1 ? regions [ 0 ] :
union (
let ( regions = [ for ( r = regions ) is_path ( r ) ? [ r ] : r ] )
concat (
2019-06-18 01:55:10 +00:00
[ _tagged_region ( regions [ 0 ] , regions [ 1 ] , [ "O" , "S" ] , [ "O" ] , eps = eps ) ] ,
2019-06-17 06:57:05 +00:00
[ for ( i = [ 2 : 1 : len ( regions ) - 1 ] ) regions [ i ] ]
2019-06-18 01:55:10 +00:00
) ,
eps = eps
2019-06-17 06:57:05 +00:00
) ;
2019-06-18 07:46:05 +00:00
// Function&Module: difference()
2019-06-17 06:57:05 +00:00
// Usage:
2019-06-18 07:46:05 +00:00
// difference() {...}
// region = difference(regions);
// region = difference(REGION1,REGION2);
// region = difference(REGION1,REGION2,REGION3);
2019-06-17 06:57:05 +00:00
// Description:
2019-06-18 07:46:05 +00:00
// When called as a function, and given a list of regions, where each region is a list of closed
// 2D paths, takes the first region and differences away all other regions from it. The resulting
// region is returned.
// When called as the built-in module, makes the boolean difference of the given children.
2019-06-17 06:57:05 +00:00
// Arguments:
// regions = List of regions to difference. Each region is a list of closed paths.
// Example(2D):
// shape1 = move([-8,-8,0], p=circle(d=50));
// shape2 = move([ 8, 8,0], p=circle(d=50));
2019-07-12 20:11:13 +00:00
// for (shape = [shape1,shape2]) color("red") stroke(shape, width=0.5, closed=true);
2019-06-17 06:57:05 +00:00
// color("green") region(difference(shape1,shape2));
2019-06-18 01:55:10 +00:00
function difference ( regions = [ ] , b = undef , c = undef , eps = EPSILON ) =
b ! = undef ? difference ( concat ( [ regions ] , [ b ] , c = = undef ? [ ] : [ c ] ) , eps = eps ) :
2019-06-17 06:57:05 +00:00
len ( regions ) < = 1 ? regions [ 0 ] :
difference (
let ( regions = [ for ( r = regions ) is_path ( r ) ? [ r ] : r ] )
concat (
2019-06-18 01:55:10 +00:00
[ _tagged_region ( regions [ 0 ] , regions [ 1 ] , [ "O" , "U" ] , [ "I" ] , eps = eps ) ] ,
2019-06-17 06:57:05 +00:00
[ for ( i = [ 2 : 1 : len ( regions ) - 1 ] ) regions [ i ] ]
2019-06-18 01:55:10 +00:00
) ,
eps = eps
2019-06-17 06:57:05 +00:00
) ;
2019-06-18 07:46:05 +00:00
// Function&Module: intersection()
2019-06-17 06:57:05 +00:00
// Usage:
2019-06-18 07:46:05 +00:00
// intersection() {...}
// region = intersection(regions);
// region = intersection(REGION1,REGION2);
// region = intersection(REGION1,REGION2,REGION3);
2019-06-17 06:57:05 +00:00
// Description:
2019-06-18 07:46:05 +00:00
// When called as a function, and given a list of regions, where each region is a list of closed
// 2D paths, returns the boolean intersection of all given regions. Result is a single region.
// When called as the built-in module, makes the boolean intersection of all the given children.
2019-06-17 06:57:05 +00:00
// Arguments:
// regions = List of regions to intersection. Each region is a list of closed paths.
// Example(2D):
// shape1 = move([-8,-8,0], p=circle(d=50));
// shape2 = move([ 8, 8,0], p=circle(d=50));
2019-07-12 20:11:13 +00:00
// for (shape = [shape1,shape2]) color("red") stroke(shape, width=0.5, closed=true);
2019-06-17 06:57:05 +00:00
// color("green") region(intersection(shape1,shape2));
2019-06-18 01:55:10 +00:00
function intersection ( regions = [ ] , b = undef , c = undef , eps = EPSILON ) =
b ! = undef ? intersection ( concat ( [ regions ] , [ b ] , c = = undef ? [ ] : [ c ] ) , eps = eps ) :
2019-06-17 06:57:05 +00:00
len ( regions ) < = 1 ? regions [ 0 ] :
intersection (
let ( regions = [ for ( r = regions ) is_path ( r ) ? [ r ] : r ] )
concat (
2019-06-18 01:55:10 +00:00
[ _tagged_region ( regions [ 0 ] , regions [ 1 ] , [ "I" , "S" ] , [ "I" ] , eps = eps ) ] ,
2019-06-17 06:57:05 +00:00
[ for ( i = [ 2 : 1 : len ( regions ) - 1 ] ) regions [ i ] ]
2019-06-18 01:55:10 +00:00
) ,
eps = eps
2019-06-17 06:57:05 +00:00
) ;
2019-06-18 07:09:51 +00:00
// Function&Module: exclusive_or()
2019-06-17 06:57:05 +00:00
// Usage:
2019-06-18 07:09:51 +00:00
// exclusive_or() {...}
2019-06-18 07:46:05 +00:00
// region = exclusive_or(regions);
// region = exclusive_or(REGION1,REGION2);
// region = exclusive_or(REGION1,REGION2,REGION3);
2019-06-17 06:57:05 +00:00
// Description:
2019-06-18 07:09:51 +00:00
// When called as a function and given a list of regions, where each region is a list of closed
2019-06-18 07:46:05 +00:00
// 2D paths, returns the boolean exclusive_or of all given regions. Result is a single region.
2019-06-18 07:09:51 +00:00
// When called as a module, performs a boolean exclusive-or of up to 10 children.
2019-06-17 06:57:05 +00:00
// Arguments:
// regions = List of regions to exclusive_or. Each region is a list of closed paths.
2019-06-18 07:46:05 +00:00
// Example(2D): As Function
2019-06-17 06:57:05 +00:00
// shape1 = move([-8,-8,0], p=circle(d=50));
// shape2 = move([ 8, 8,0], p=circle(d=50));
2019-06-18 07:46:05 +00:00
// for (shape = [shape1,shape2])
2019-07-12 20:11:13 +00:00
// color("red") stroke(shape, width=0.5, closed=true);
2019-06-17 06:57:05 +00:00
// color("green") region(exclusive_or(shape1,shape2));
2019-06-18 07:46:05 +00:00
// Example(2D): As Module
// exclusive_or() {
// square(40,center=false);
// circle(d=40);
// }
2019-06-18 01:55:10 +00:00
function exclusive_or ( regions = [ ] , b = undef , c = undef , eps = EPSILON ) =
b ! = undef ? exclusive_or ( concat ( [ regions ] , [ b ] , c = = undef ? [ ] : [ c ] ) , eps = eps ) :
2019-06-17 06:57:05 +00:00
len ( regions ) < = 1 ? regions [ 0 ] :
exclusive_or (
let ( regions = [ for ( r = regions ) is_path ( r ) ? [ r ] : r ] )
concat (
[ union ( [
2019-06-18 01:55:10 +00:00
difference ( [ regions [ 0 ] , regions [ 1 ] ] , eps = eps ) ,
difference ( [ regions [ 1 ] , regions [ 0 ] ] , eps = eps )
] , eps = eps ) ] ,
2019-06-17 06:57:05 +00:00
[ for ( i = [ 2 : 1 : len ( regions ) - 1 ] ) regions [ i ] ]
2019-06-18 01:55:10 +00:00
) ,
eps = eps
2019-06-17 06:57:05 +00:00
) ;
2019-06-18 07:09:51 +00:00
module exclusive_or ( ) {
if ( $children = = 1 ) {
children ( ) ;
} else if ( $children = = 2 ) {
difference ( ) {
children ( 0 ) ;
children ( 1 ) ;
}
difference ( ) {
children ( 1 ) ;
children ( 0 ) ;
}
} else if ( $children = = 3 ) {
exclusive_or ( ) {
exclusive_or ( ) {
children ( 0 ) ;
children ( 1 ) ;
}
children ( 2 ) ;
}
} else if ( $children = = 4 ) {
exclusive_or ( ) {
exclusive_or ( ) {
children ( 0 ) ;
children ( 1 ) ;
}
exclusive_or ( ) {
children ( 2 ) ;
children ( 3 ) ;
}
}
} else if ( $children = = 5 ) {
exclusive_or ( ) {
exclusive_or ( ) {
children ( 0 ) ;
children ( 1 ) ;
children ( 2 ) ;
children ( 3 ) ;
}
children ( 4 ) ;
}
} else if ( $children = = 6 ) {
exclusive_or ( ) {
exclusive_or ( ) {
children ( 0 ) ;
children ( 1 ) ;
children ( 2 ) ;
children ( 3 ) ;
}
children ( 4 ) ;
children ( 5 ) ;
}
} else if ( $children = = 7 ) {
exclusive_or ( ) {
exclusive_or ( ) {
children ( 0 ) ;
children ( 1 ) ;
children ( 2 ) ;
children ( 3 ) ;
}
children ( 4 ) ;
children ( 5 ) ;
children ( 6 ) ;
}
} else if ( $children = = 8 ) {
exclusive_or ( ) {
exclusive_or ( ) {
children ( 0 ) ;
children ( 1 ) ;
children ( 2 ) ;
children ( 3 ) ;
}
exclusive_or ( ) {
children ( 4 ) ;
children ( 5 ) ;
children ( 6 ) ;
children ( 7 ) ;
}
}
} else if ( $children = = 9 ) {
exclusive_or ( ) {
exclusive_or ( ) {
children ( 0 ) ;
children ( 1 ) ;
children ( 2 ) ;
children ( 3 ) ;
}
exclusive_or ( ) {
children ( 4 ) ;
children ( 5 ) ;
children ( 6 ) ;
children ( 7 ) ;
}
children ( 8 ) ;
}
} else if ( $children = = 10 ) {
exclusive_or ( ) {
exclusive_or ( ) {
children ( 0 ) ;
children ( 1 ) ;
children ( 2 ) ;
children ( 3 ) ;
}
exclusive_or ( ) {
children ( 4 ) ;
children ( 5 ) ;
children ( 6 ) ;
children ( 7 ) ;
}
children ( 8 ) ;
children ( 9 ) ;
}
2019-08-12 05:15:37 +00:00
} else {
assert ( $children < = 10 , "exclusive_or() can only handle up to 10 children." ) ;
2019-06-18 07:09:51 +00:00
}
}
2019-06-17 06:57:05 +00:00
// Module: region()
// Usage:
// region(r);
// Description:
2019-06-18 07:46:05 +00:00
// Creates 2D polygons for the given region. The region given is a list of closed 2D paths.
// Each path will be effectively exclusive-ORed from all other paths in the region, so if a
// path is inside another path, it will be effectively subtracted from it.
// Example(2D):
// region([circle(d=50), square(25,center=true)]);
2019-06-17 06:57:05 +00:00
// Example(2D):
2019-06-18 07:46:05 +00:00
// rgn = concat(
// [for (d=[50:-10:10]) circle(d=d-5)],
// [square([60,10], center=true)]
// );
// region(rgn);
2019-06-17 06:57:05 +00:00
module region ( r )
{
points = flatten ( r ) ;
paths = [
for ( i = [ 0 : 1 : len ( r ) - 1 ] ) let (
start = default ( sum ( [ for ( j = [ 0 : 1 : i - 1 ] ) len ( r [ j ] ) ] ) , 0 )
) [ for ( k = [ 0 : 1 : len ( r [ i ] ) - 1 ] ) start + k ]
] ;
polygon ( points = points , paths = paths ) ;
}
2019-04-20 00:02:17 +00:00
// vim: noexpandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap