From 1ee6e3a2e7330111be92d293158d077e71b84be7 Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Sat, 6 Mar 2021 02:20:32 -0800 Subject: [PATCH 1/4] Added bezier polar construction functions. --- beziers.scad | 166 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/beziers.scad b/beziers.scad index c5c7e2d..2a74ac3 100644 --- a/beziers.scad +++ b/beziers.scad @@ -19,6 +19,172 @@ // Spline Steps = The number of straight-line segments to split a bezier segment into, to approximate the bezier curve. The more spline steps, the closer the approximation will be to the curve, but the slower it will be to generate. Usually defaults to 16. +// Section: Bezier Path Construction + +// Function: bez_begin() +// Topics: Bezier Paths +// See Also: bez_tang(), bez_joint(), bez_end() +// Usage: +// pts = bez_begin(pt,a,r,); +// pts = bez_begin(pt,VECTOR,,); +// Description: +// This is used to create the first endpoint and control point of a cubic bezier path. +// Arguments: +// pt = The starting endpoint for the bezier path. +// a = If given a scalar, specifies the theta (XY plane) angle in degrees from X+. If given a vector, specifies the direction and possibly distance of the first control point. +// r = Specifies the distance of the control point from the endpoint `pt`. +// p = If given, specifies the number of degrees away from the Z+ axis. +// Example(2D): 2D Bezier Path by Angle +// bezpath = flatten([ +// bez_begin([-50, 0], 45,20), +// bez_tang ([ 0, 0],-135,20), +// bez_joint([ 20,-25], 135, 90, 10, 15), +// bez_end ([ 50, 0], -90,20), +// ]); +// trace_bezier(bezpath); +// Example(2D): 2D Bezier Path by Vector +// bezpath = flatten([ +// bez_begin([-50,0],[0,-20]), +// bez_tang ([-10,0],[0,-20]), +// bez_joint([ 20,-25], [-10,10], [0,15]), +// bez_end ([ 50,0],[0, 20]), +// ]); +// trace_bezier(bezpath); +// Example(2D): 2D Bezier Path by Vector and Distance +// bezpath = flatten([ +// bez_begin([-30,0],FWD, 30), +// bez_tang ([ 0,0],FWD, 30), +// bez_joint([ 20,-25], 135, 90, 10, 15), +// bez_end ([ 30,0],BACK,30), +// ]); +// trace_bezier(bezpath); +// Example(3D,FlatSpin,VPD=200): 3D Bezier Path by Angle +// bezpath = flatten([ +// bez_begin([-30,0,0],90,20,p=135), +// bez_tang ([ 0,0,0],-90,20,p=135), +// bez_joint([20,-25,0], 135, 90, 15, 10, p1=135, p2=45), +// bez_end ([ 30,0,0],-90,20,p=45), +// ]); +// trace_bezier(bezpath); +// Example(3D,FlatSpin,VPD=225): 3D Bezier Path by Vector +// bezpath = flatten([ +// bez_begin([-30,0,0],[0,-20, 20]), +// bez_tang ([ 0,0,0],[0,-20,-20]), +// bez_joint([20,-25,0],[0,10,-10],[0,15,15]), +// bez_end ([ 30,0,0],[0,-20,-20]), +// ]); +// trace_bezier(bezpath); +// Example(3D,FlatSpin,VPD=225): 3D Bezier Path by Vector and Distance +// bezpath = flatten([ +// bez_begin([-30,0,0],FWD, 20), +// bez_tang ([ 0,0,0],DOWN,20), +// bez_joint([20,-25,0],LEFT,DOWN,r1=20,r2=15), +// bez_end ([ 30,0,0],DOWN,20), +// ]); +// trace_bezier(bezpath); +function bez_begin(pt,a,r,p) = + assert(is_finite(r) || is_vector(a)) + assert(len(pt)==3 || is_undef(p)) + is_vector(a)? [pt, pt+(is_undef(r)? a : r*unit(a))] : + is_finite(a)? [pt, pt+spherical_to_xyz(r,a,default(p,90))] : + assert(false, "Bad arguments."); + + +// Function: bez_tang() +// Topics: Bezier Paths +// See Also: bez_begin(), bez_joint(), bez_end() +// Usage: +// pts = bez_tang(pt,a,r1,r2,); +// pts = bez_tang(pt,VECTOR,,,); +// Description: +// This creates a smooth joint in a cubic bezier path. It creates three points, being the +// approaching control point, the fixed bezier control point, and the departing control +// point. The two control points will be collinear with the fixed point, making for a +// smooth bezier curve at the fixed point. See {{bez_begin()}} for examples. +// Arguments: +// pt = The fixed point for the bezier path. +// a = If given a scalar, specifies the theta (XY plane) angle in degrees from X+. If given a vector, specifies the direction and possibly distance of the departing control point. +// r1 = Specifies the distance of the approching control point from the fixed point. Overrides the distance component of the vector if `a` contains a vector. +// r2 = Specifies the distance of the departing control point from the fixed point. Overrides the distance component of the vector if `a` contains a vector. If `r1` is given and `r2` is not, uses the value of `r1` for `r2`. +// p = If given, specifies the number of degrees away from the Z+ axis. +function bez_tang(pt,a,r1,r2,p) = + assert(is_finite(r1) || is_vector(a)) + assert(len(pt)==3 || is_undef(p)) + let( + r1 = is_num(r1)? r1 : norm(a), + r2 = default(r2,r1), + p = default(p, 90) + ) + is_vector(a)? [pt-r1*unit(a), pt, pt+r2*unit(a)] : + is_finite(a)? [ + pt-spherical_to_xyz(r1,a,p), + pt, + pt+spherical_to_xyz(r2,a,p) + ] : + assert(false, "Bad arguments."); + + +// Function: bez_joint() +// Topics: Bezier Paths +// See Also: bez_begin(), bez_tang(), bez_end() +// Usage: +// pts = bez_joint(pt,a1,a2,r1,r2,,); +// pts = bez_joint(pt,VEC1,VEC2,,,,); +// Description: +// This creates a disjoint corner joint in a cubic bezier path. It creates three points, being +// the aproaching control point, the fixed bezier control point, and the departing control point. +// The two control points can be directed in different arbitrary directions from the fixed bezier +// point. See {{bez_begin()}} for examples. +// Arguments: +// pt = The fixed point for the bezier path. +// a1 = If given a scalar, specifies the theta (XY plane) angle in degrees from X+. If given a vector, specifies the direction and possibly distance of the approaching control point. +// a2 = If given a scalar, specifies the theta (XY plane) angle in degrees from X+. If given a vector, specifies the direction and possibly distance of the departing control point. +// r1 = Specifies the distance of the approching control point from the fixed point. Overrides the distance component of the vector if `a1` contains a vector. +// r2 = Specifies the distance of the departing control point from the fixed point. Overrides the distance component of the vector if `a2` contains a vector. +// p1 = If given, specifies the number of degrees away from the Z+ axis of the approaching control point. +// p2 = If given, specifies the number of degrees away from the Z+ axis of the departing control point. +function bez_joint(pt,a1,a2,r1,r2,p1,p2) = + assert(is_finite(r1) || is_vector(a1)) + assert(is_finite(r2) || is_vector(a2)) + assert(len(pt)==3 || (is_undef(p1) && is_undef(p2))) + let( + r1 = is_num(r1)? r1 : norm(a1), + r2 = is_num(r2)? r2 : norm(a2), + p1 = default(p1, 90), + p2 = default(p2, 90) + ) [ + if (is_vector(a1)) (pt+r1*unit(a1)) + else if (is_finite(a1)) (pt+spherical_to_xyz(r1,a1,p1)) + else assert(false, "Bad Arguments"), + pt, + if (is_vector(a2)) (pt+r2*unit(a2)) + else if (is_finite(a2)) (pt+spherical_to_xyz(r2,a2,p2)) + else assert(false, "Bad Arguments") + ]; + + +// Function: bez_end() +// Topics: Bezier Paths +// See Also: bez_tang(), bez_joint(), bez_end() +// Usage: +// pts = bez_end(pt,a,r,); +// pts = bez_end(pt,VECTOR,,); +// Description: +// This is used to create the approaching control point, and the endpoint of a cubic bezier path. +// See {{bez_begin()}} for examples. +// Arguments: +// pt = The starting endpoint for the bezier path. +// a = If given a scalar, specifies the theta (XY plane) angle in degrees from X+. If given a vector, specifies the direction and possibly distance of the first control point. +// r = Specifies the distance of the control point from the endpoint `pt`. +// p = If given, specifies the number of degrees away from the Z+ axis. +function bez_end(pt,a,r,p) = + assert(is_finite(r) || is_vector(a)) + assert(len(pt)==3 || is_undef(p)) + is_vector(a)? [pt+(is_undef(r)? a : r*unit(a)), pt] : + is_finite(a)? [pt+spherical_to_xyz(r,a,default(p,90)), pt] : + assert(false, "Bad arguments."); + + // Section: Segment Functions // Function: bezier_points() From 3682f7b27cb72cbfc79a8c3aa311c4745bfd2791 Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Sat, 6 Mar 2021 02:26:39 -0800 Subject: [PATCH 2/4] Added dashed_stroke(). path_cut() -> path_cut_points(). --- coords.scad | 134 ++++++++++++++++++------ geometry.scad | 25 +---- paths.scad | 120 ++++++++++++++------- rounding.scad | 8 +- shapes2d.scad | 285 ++++++++++++++++++++++++++++++++++++++------------ 5 files changed, 405 insertions(+), 167 deletions(-) diff --git a/coords.scad b/coords.scad index 8ab429b..1843c7a 100644 --- a/coords.scad +++ b/coords.scad @@ -9,9 +9,12 @@ // Section: Coordinate Manipulation // Function: point2d() +// Usage: +// pt = point2d(p, ); +// Topics: Coordinates, Points +// See Also: path2d(), point3d(), path3d() // Description: -// Returns a 2D vector/point from a 2D or 3D vector. -// If given a 3D point, removes the Z coordinate. +// Returns a 2D vector/point from a 2D or 3D vector. If given a 3D point, removes the Z coordinate. // Arguments: // p = The coordinates to force into a 2D vector/point. // fill = Value to fill missing values in vector with. @@ -19,11 +22,14 @@ function point2d(p, fill=0) = [for (i=[0:1]) (p[i]==undef)? fill : p[i]]; // Function: path2d() +// Usage: +// pts = path2d(points); +// Topics: Coordinates, Points, Paths +// See Also: point2d(), point3d(), path3d() // Description: -// Returns a list of 2D vectors/points from a list of 2D, 3D or higher -// dimensional vectors/points. Removes the extra coordinates from -// higher dimensional points. The input must be a path, where -// every vector has the same length. +// Returns a list of 2D vectors/points from a list of 2D, 3D or higher dimensional vectors/points. +// Removes the extra coordinates from higher dimensional points. The input must be a path, where +// every vector has the same length. // Arguments: // points = A list of 2D or 3D points/vectors. function path2d(points) = @@ -34,6 +40,10 @@ function path2d(points) = // Function: point3d() +// Usage: +// pt = point3d(p, ); +// Topics: Coordinates, Points +// See Also: path2d(), point2d(), path3d() // Description: // Returns a 3D vector/point from a 2D or 3D vector. // Arguments: @@ -43,6 +53,10 @@ function point3d(p, fill=0) = [for (i=[0:2]) (p[i]==undef)? fill : p[i]]; // Function: path3d() +// Usage: +// pts = path3d(points, ); +// Topics: Coordinates, Points, Paths +// See Also: point2d(), path2d(), point3d() // Description: // Returns a list of 3D vectors/points from a list of 2D or higher dimensional vectors/points // by removing extra coordinates or adding the z coordinate. @@ -63,6 +77,10 @@ function path3d(points, fill=0) = // Function: point4d() +// Usage: +// pt = point4d(p, ); +// Topics: Coordinates, Points +// See Also: point2d(), path2d(), point3d(), path3d(), path4d() // Description: // Returns a 4D vector/point from a 2D or 3D vector. // Arguments: @@ -72,12 +90,15 @@ function point4d(p, fill=0) = [for (i=[0:3]) (p[i]==undef)? fill : p[i]]; // Function: path4d() +// Usage: +// pt = path4d(points, ); +// Topics: Coordinates, Points, Paths +// See Also: point2d(), path2d(), point3d(), path3d(), point4d() // Description: // Returns a list of 4D vectors/points from a list of 2D or 3D vectors/points. // Arguments: // points = A list of 2D or 3D points/vectors. // fill = Value to fill missing values in vectors with. - function path4d(points, fill=0) = assert(is_num(fill) || is_vector(fill)) assert(is_path(points, dim=undef, fast=true), "Input to path4d is not a path") @@ -102,8 +123,10 @@ function path4d(points, fill=0) = // Function: polar_to_xy() // Usage: -// polar_to_xy(r, theta); -// polar_to_xy([r, theta]); +// pt = polar_to_xy(r, theta); +// pt = polar_to_xy([r, theta]); +// Topics: Coordinates, Points, Paths +// See Also: xy_to_polar(), xyz_to_cylindrical(), cylindrical_to_xyz(), xyz_to_spherical(), spherical_to_xyz() // Description: // Convert polar coordinates to 2D cartesian coordinates. // Returns [X,Y] cartesian coordinates. @@ -114,6 +137,13 @@ function path4d(points, fill=0) = // xy = polar_to_xy(20,45); // Returns: ~[14.1421365, 14.1421365] // xy = polar_to_xy(40,30); // Returns: ~[34.6410162, 15] // xy = polar_to_xy([40,30]); // Returns: ~[34.6410162, 15] +// Example(2D): +// r=40; ang=30; $fn=36; +// pt = polar_to_xy(r,ang); +// stroke(circle(r=r), closed=true, width=0.5); +// color("black") stroke([[r,0], [0,0], pt], width=0.5); +// color("black") stroke(arc(r=15, angle=ang), width=0.5); +// color("red") move(pt) circle(d=3); function polar_to_xy(r,theta=undef) = let( rad = theta==undef? r[0] : r, t = theta==undef? r[1] : theta @@ -122,8 +152,10 @@ function polar_to_xy(r,theta=undef) = let( // Function: xy_to_polar() // Usage: -// xy_to_polar(x,y); -// xy_to_polar([X,Y]); +// r_theta = xy_to_polar(x,y); +// r_theta = xy_to_polar([X,Y]); +// Topics: Coordinates, Points, Paths +// See Also: polar_to_xy(), xyz_to_cylindrical(), cylindrical_to_xyz(), xyz_to_spherical(), spherical_to_xyz() // Description: // Convert 2D cartesian coordinates to polar coordinates. // Returns [radius, theta] where theta is the angle counter-clockwise of X+. @@ -133,6 +165,13 @@ function polar_to_xy(r,theta=undef) = let( // Examples: // plr = xy_to_polar(20,30); // plr = xy_to_polar([40,60]); +// Example(2D): +// pt = [-20,30]; $fn = 36; +// rt = xy_to_polar(pt); +// r = rt[0]; ang = rt[1]; +// stroke(circle(r=r), closed=true, width=0.5); +// zrot(ang) stroke([[0,0],[r,0]],width=0.5); +// color("red") move(pt) circle(d=3); function xy_to_polar(x,y=undef) = let( xx = y==undef? x[0] : x, yy = y==undef? x[1] : y @@ -141,11 +180,13 @@ function xy_to_polar(x,y=undef) = let( // Function: project_plane() // Usage: With the plane defined by 3 Points -// xyz = project_plane(point, a, b, c); +// pt = project_plane(point, a, b, c); // Usage: With the plane defined by Pointlist -// xyz = project_plane(point, POINTLIST); +// pt = project_plane(point, POINTLIST); // Usage: With the plane defined by Plane Definition [A,B,C,D] Where Ax+By+Cz=D -// xyz = project_plane(point, PLANE); +// pt = project_plane(point, PLANE); +// Topics: Coordinates, Points, Paths +// See Also: lift_plane() // Description: // Converts the given 3D points from global coordinates to the 2D planar coordinates of the closest // points on the plane. This coordinate system can be useful in taking a set of nearly coplanar @@ -165,6 +206,20 @@ function xy_to_polar(x,y=undef) = let( // a=[0,0,0]; b=[10,-10,0]; c=[10,0,10]; // xy = project_plane(pt, a, b, c); // xy2 = project_plane(pt, [a,b,c]); +// Example(3D): +// points = move([10,20,30], p=yrot(25, p=path3d(circle(d=100, $fn=36)))); +// plane = plane_from_normal([1,0,1]); +// proj = project_plane(points,plane); +// n = plane_normal(plane); +// cp = centroid(proj); +// color("red") move_copies(points) sphere(d=2,$fn=12); +// color("blue") rot(from=UP,to=n,cp=cp) move_copies(proj) sphere(d=2,$fn=12); +// move(cp) { +// rot(from=UP,to=n) { +// anchor_arrow(30); +// %cube([120,150,0.1],center=true); +// } +// } function project_plane(point, a, b, c) = is_undef(b) && is_undef(c) && is_list(a)? let( mat = is_vector(a,4)? plane_transform(a) : @@ -195,6 +250,8 @@ function project_plane(point, a, b, c) = // xyz = lift_plane(point, POINTLIST); // Usage: With Plane Definition [A,B,C,D] Where Ax+By+Cz=D // xyz = lift_plane(point, PLANE); +// Topics: Coordinates, Points, Paths +// See Also: project_plane() // Description: // Converts the given 2D point from planar coordinates to the global 3D coordinates of the point on the plane. // Can be called one of three ways: @@ -232,8 +289,10 @@ function lift_plane(point, a, b, c) = // Function: cylindrical_to_xyz() // Usage: -// cylindrical_to_xyz(r, theta, z) -// cylindrical_to_xyz([r, theta, z]) +// pt = cylindrical_to_xyz(r, theta, z); +// pt = cylindrical_to_xyz([r, theta, z]); +// Topics: Coordinates, Points, Paths +// See Also: xyz_to_cylindrical(), xyz_to_spherical(), spherical_to_xyz() // Description: // Convert cylindrical coordinates to 3D cartesian coordinates. Returns [X,Y,Z] cartesian coordinates. // Arguments: @@ -252,12 +311,13 @@ function cylindrical_to_xyz(r,theta=undef,z=undef) = let( // Function: xyz_to_cylindrical() // Usage: -// xyz_to_cylindrical(x,y,z) -// xyz_to_cylindrical([X,Y,Z]) +// rtz = xyz_to_cylindrical(x,y,z); +// rtz = xyz_to_cylindrical([X,Y,Z]); +// Topics: Coordinates, Points, Paths +// See Also: cylindrical_to_xyz(), xyz_to_spherical(), spherical_to_xyz() // Description: -// Convert 3D cartesian coordinates to cylindrical coordinates. -// Returns [radius,theta,Z]. Theta is the angle counter-clockwise -// of X+ on the XY plane. Z is height above the XY plane. +// Convert 3D cartesian coordinates to cylindrical coordinates. Returns [radius,theta,Z]. +// Theta is the angle counter-clockwise of X+ on the XY plane. Z is height above the XY plane. // Arguments: // x = X coordinate. // y = Y coordinate. @@ -272,11 +332,12 @@ function xyz_to_cylindrical(x,y=undef,z=undef) = let( // Function: spherical_to_xyz() // Usage: -// spherical_to_xyz(r, theta, phi); -// spherical_to_xyz([r, theta, phi]); +// pt = spherical_to_xyz(r, theta, phi); +// pt = spherical_to_xyz([r, theta, phi]); // Description: -// Convert spherical coordinates to 3D cartesian coordinates. -// Returns [X,Y,Z] cartesian coordinates. +// Convert spherical coordinates to 3D cartesian coordinates. Returns [X,Y,Z] cartesian coordinates. +// Topics: Coordinates, Points, Paths +// See Also: cylindrical_to_xyz(), xyz_to_spherical(), xyz_to_cylindrical() // Arguments: // r = distance from origin. // theta = angle in degrees, counter-clockwise of X+ on the XY plane. @@ -293,12 +354,13 @@ function spherical_to_xyz(r,theta=undef,phi=undef) = let( // Function: xyz_to_spherical() // Usage: -// xyz_to_spherical(x,y,z) -// xyz_to_spherical([X,Y,Z]) +// r_theta_phi = xyz_to_spherical(x,y,z) +// r_theta_phi = xyz_to_spherical([X,Y,Z]) +// Topics: Coordinates, Points, Paths +// See Also: cylindrical_to_xyz(), spherical_to_xyz(), xyz_to_cylindrical() // Description: -// Convert 3D cartesian coordinates to spherical coordinates. -// Returns [r,theta,phi], where phi is the angle from the Z+ pole, -// and theta is degrees counter-clockwise of X+ on the XY plane. +// Convert 3D cartesian coordinates to spherical coordinates. Returns [r,theta,phi], where phi is +// the angle from the Z+ pole, and theta is degrees counter-clockwise of X+ on the XY plane. // Arguments: // x = X coordinate. // y = Y coordinate. @@ -313,8 +375,10 @@ function xyz_to_spherical(x,y=undef,z=undef) = let( // Function: altaz_to_xyz() // Usage: -// altaz_to_xyz(alt, az, r); -// altaz_to_xyz([alt, az, r]); +// pt = altaz_to_xyz(alt, az, r); +// pt = altaz_to_xyz([alt, az, r]); +// Topics: Coordinates, Points, Paths +// See Also: cylindrical_to_xyz(), xyz_to_spherical(), spherical_to_xyz(), xyz_to_cylindrical(), xyz_to_altaz() // Description: // Convert altitude/azimuth/range coordinates to 3D cartesian coordinates. // Returns [X,Y,Z] cartesian coordinates. @@ -334,8 +398,10 @@ function altaz_to_xyz(alt,az=undef,r=undef) = let( // Function: xyz_to_altaz() // Usage: -// xyz_to_altaz(x,y,z); -// xyz_to_altaz([X,Y,Z]); +// alt_az_r = xyz_to_altaz(x,y,z); +// alt_az_r = xyz_to_altaz([X,Y,Z]); +// Topics: Coordinates, Points, Paths +// See Also: cylindrical_to_xyz(), xyz_to_spherical(), spherical_to_xyz(), xyz_to_cylindrical(), altaz_to_xyz() // Description: // Convert 3D cartesian coordinates to altitude/azimuth/range coordinates. // Returns [altitude,azimuth,range], where altitude is angle above the diff --git a/geometry.scad b/geometry.scad index 95ef2ec..f5413c2 100644 --- a/geometry.scad +++ b/geometry.scad @@ -999,10 +999,11 @@ function plane_transform(plane) = // Function: projection_on_plane() // Usage: -// projection_on_plane(points); +// pts = projection_on_plane(plane, points); // Description: -// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or 3d points, return the 3D orthogonal -// projection of the points on the plane. +// Given a plane definition `[A,B,C,D]`, where `Ax+By+Cz=D`, and a list of 2d or +// 3d points, return the 3D orthogonal projection of the points on the plane. +// In other words, for every point given, returns the closest point to it on the plane. // Arguments: // plane = The `[A,B,C,D]` plane definition where `Ax+By+Cz=D` is the formula of the plane. // points = List of points to project @@ -1061,24 +1062,6 @@ function distance_from_plane(plane, point) = point3d(plane)* point - plane[3]; -// Function: closest_point_on_plane() -// Usage: -// pt = closest_point_on_plane(plane, point); -// Description: -// Takes a point, and a plane [A,B,C,D] where the equation of that plane is `Ax+By+Cz=D`. -// Returns the coordinates of the closest point on that plane to the given `point`. -// Arguments: -// plane = The [A,B,C,D] coefficients for the equation of the plane. -// point = The 3D point to find the closest point to. -function closest_point_on_plane(plane, point) = - assert( _valid_plane(plane), "Invalid input plane." ) - assert( is_vector(point,3), "Invalid point." ) - let( plane = normalize_plane(plane), - n = point3d(plane), - d = n*point - plane[3] // distance from plane - ) - point - n*d; - // Returns [POINT, U] if line intersects plane at one point. // Returns [LINE, undef] if the line is on the plane. diff --git a/paths.scad b/paths.scad index c870b62..7939b38 100644 --- a/paths.scad +++ b/paths.scad @@ -114,15 +114,15 @@ function path_subselect(path, s1, u1, s2, u2, closed=false) = function simplify_path(path, eps=EPSILON) = assert( is_path(path), "Invalid path." ) assert( is_undef(eps) || (is_finite(eps) && (eps>=0) ), "Invalid tolerance." ) - len(path)<=2 ? path - : let( - indices = [ 0, - for (i=[1:1:len(path)-2]) - if (!collinear(path[i-1],path[i],path[i+1], eps=eps)) i, - len(path)-1 - ] - ) - [for (i = indices) path[i] ]; + len(path)<=2 ? path : + let( + indices = [ + 0, + for (i=[1:1:len(path)-2]) + if (!collinear(path[i-1], path[i], path[i+1], eps=eps)) i, + len(path)-1 + ] + ) [for (i=indices) path[i]]; // Function: simplify_path_indexed() @@ -137,19 +137,21 @@ function simplify_path(path, eps=EPSILON) = // indices = A list of indices into `points` that forms a path. // eps = Largest angle variance allowed. Default: EPSILON (1-e9) degrees. function simplify_path_indexed(points, indices, eps=EPSILON) = - len(indices)<=2? indices - : let( - indices = concat( indices[0], - [for (i=[1:1:len(indices)-2]) - let( - i1 = indices[i-1], - i2 = indices[i], - i3 = indices[i+1] - ) - if (!collinear(points[i1],points[i2],points[i3], eps=eps)) indices[i]], - indices[len(indices)-1] ) - ) - indices; + len(indices)<=2? indices : + let( + indices = concat( + indices[0], + [ + for (i=[1:1:len(indices)-2]) let( + i1 = indices[i-1], + i2 = indices[i], + i3 = indices[i+1] + ) if (!collinear(points[i1], points[i2], points[i3], eps=eps)) + indices[i] + ], + indices[len(indices)-1] + ) + ) indices; // Function: path_length() @@ -178,9 +180,9 @@ function path_length(path,closed=false) = // closed = true if the path is closed. Default: false function path_segment_lengths(path, closed=false) = [ - for (i=[0:1:len(path)-2]) norm(path[i+1]-path[i]), - if (closed) norm(path[0]-path[len(path)-1]) - ]; + for (i=[0:1:len(path)-2]) norm(path[i+1]-path[i]), + if (closed) norm(path[0]-select(path,-1)) + ]; // Function: path_pos_from_start() @@ -1193,7 +1195,7 @@ module path_spread(path, n, spacing, sp=undef, rotate_children=true, closed=fals ); distOK = is_def(n) || (min(distances)>=0 && max(distances)<=length); assert(distOK,"Cannot fit all of the copies"); - cutlist = path_cut(path, distances, closed, direction=true); + cutlist = path_cut_points(path, distances, closed, direction=true); planar = len(path[0])==2; if (true) for(i=[0:1:len(cutlist)-1]) { $pos = cutlist[i][0]; @@ -1215,18 +1217,56 @@ module path_spread(path, n, spacing, sp=undef, rotate_children=true, closed=fals } } +// Function: path_cut_segs() +// Usage: +// segs = path_cut_segs(path, cutlens, ); +// Topics: Paths +// See Also: path_cut_points() +// Description: +// Given a path and a list of path lengths at which to cut, returns a list of +// sub-paths cut from the original path at those path lengths. +// Arguments: +// path = The original path to split. +// cutlens = The list of path lengths to cut at. +// closed = If true, treat the path as a closed polygon. +// Example(2D): +// path = circle(d=100); +// segs = path_cut_segs(path, [50, 200], closed=true); +// rainbow(segs) stroke($item); +function path_cut_segs(path,cutlens,closed=false) = + let( + path = closed? close_path(path) : path, + cutlist = path_cut_points(path, cutlens), + cuts = len(cutlist) + ) [ + concat( + select(path,0,cutlist[0][1]-1), + [cutlist[0][0]] + ), + for(i=[0:cuts-2]) concat( + [cutlist[i][0]], + slice(path, cutlist[i][1], cutlist[i+1][1]), + [cutlist[i+1][0]] + ), + concat( + [cutlist[cuts-1][0]], + select(path, cutlist[cuts-1][1], -1) + ) + ]; -// Function: path_cut() + + +// Function: path_cut_points() // // Usage: -// path_cut(path, dists, [closed], [direction]) +// path_cut_points(path, dists, [closed], [direction]) // // Description: // Cuts a path at a list of distances from the first point in the path. Returns a list of the cut // points and indices of the next point in the path after that point. So for example, a return // value entry of [[2,3], 5] means that the cut point was [2,3] and the next point on the path after -// this point is path[5]. If the path is too short then path_cut returns undef. If you set -// `direction` to true then `path_cut` will also return the tangent vector to the path and a normal +// this point is path[5]. If the path is too short then path_cut_points returns undef. If you set +// `direction` to true then `path_cut_points` will also return the tangent vector to the path and a normal // vector to the path. It tries to find a normal vector that is coplanar to the path near the cut // point. If this fails it will return a normal vector parallel to the xy plane. The output with // direction vectors will be `[point, next_index, tangent, normal]`. @@ -1239,15 +1279,15 @@ module path_spread(path, n, spacing, sp=undef, rotate_children=true, closed=fals // // Example(NORENDER): // square=[[0,0],[1,0],[1,1],[0,1]]; -// path_cut(square, [.5,1.5,2.5]); // Returns [[[0.5, 0], 1], [[1, 0.5], 2], [[0.5, 1], 3]] -// path_cut(square, [0,1,2,3]); // Returns [[[0, 0], 1], [[1, 0], 2], [[1, 1], 3], [[0, 1], 4]] -// path_cut(square, [0,0.8,1.6,2.4,3.2], closed=true); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], [[0, 0.8], 4]] -// path_cut(square, [0,0.8,1.6,2.4,3.2]); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], undef] -function path_cut(path, dists, closed=false, direction=false) = +// path_cut_points(square, [.5,1.5,2.5]); // Returns [[[0.5, 0], 1], [[1, 0.5], 2], [[0.5, 1], 3]] +// path_cut_points(square, [0,1,2,3]); // Returns [[[0, 0], 1], [[1, 0], 2], [[1, 1], 3], [[0, 1], 4]] +// path_cut_points(square, [0,0.8,1.6,2.4,3.2], closed=true); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], [[0, 0.8], 4]] +// path_cut_points(square, [0,0.8,1.6,2.4,3.2]); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], undef] +function path_cut_points(path, dists, closed=false, direction=false) = let(long_enough = len(path) >= (closed ? 3 : 2)) assert(long_enough,len(path)<2 ? "Two points needed to define a path" : "Closed path must include three points") - !is_list(dists)? path_cut(path, [dists],closed, direction)[0] - : let(cuts = _path_cut(path,dists,closed)) + !is_list(dists)? path_cut_points(path, [dists],closed, direction)[0] + : let(cuts = _path_cut_points(path,dists,closed)) !direction ? cuts : let( @@ -1257,7 +1297,7 @@ function path_cut(path, dists, closed=false, direction=false) = hstack(cuts, array_group(dir,1), array_group(normals,1)); // Main recursive path cut function -function _path_cut(path, dists, closed=false, pind=0, dtotal=0, dind=0, result=[]) = +function _path_cut_points(path, dists, closed=false, pind=0, dtotal=0, dind=0, result=[]) = dind == len(dists) ? result : let( lastpt = len(result)>0? select(result,-1)[0] : [], @@ -1267,7 +1307,7 @@ function _path_cut(path, dists, closed=false, pind=0, dtotal=0, dind=0, result=[ _path_cut_single(path, dists[dind]-dtotal-dpartial, closed, pind) ) is_undef(nextpoint)? concat(result, repeat(undef,len(dists)-dind)) : - _path_cut(path, dists, closed, nextpoint[1], dists[dind],dind+1, concat(result, [nextpoint])); + _path_cut_points(path, dists, closed, nextpoint[1], dists[dind],dind+1, concat(result, [nextpoint])); // Search for a single cut point in the path function _path_cut_single(path, dist, closed=false, ind=0, eps=1e-7) = @@ -1479,7 +1519,7 @@ function resample_path(path, N, spacing, closed=false) = N = is_def(N) ? N : round(length/spacing) + (closed?0:1), spacing = length/(closed?N:N-1), // Note: worried about round-off error, so don't include distlist = list_range(closed?N:N-1,step=spacing), // last point when closed=false - cuts = path_cut(path, distlist, closed=closed) + cuts = path_cut_points(path, distlist, closed=closed) ) concat(subindex(cuts,0),closed?[]:[select(path,-1)]); // Then add last point here diff --git a/rounding.scad b/rounding.scad index abfe0ba..8f0a646 100644 --- a/rounding.scad +++ b/rounding.scad @@ -640,8 +640,8 @@ function _path_join(paths,joint,k=0.5,i=0,result=[],relocate=true,closed=false) ) assert(d_first>=0 && d_next>=0, str("Joint value negative when adding path ",i+1)) let( - firstcut = path_cut(revresult, d_first, direction=true), - nextcut = path_cut(nextpath, d_next, direction=true) + firstcut = path_cut_points(revresult, d_first, direction=true), + nextcut = path_cut_points(nextpath, d_next, direction=true) ) assert(is_def(firstcut),str("Path ",i," is too short for specified cut distance ",d_first)) assert(is_def(nextcut),str("Path ",i+1," is too short for specified cut distance ",d_next)) @@ -1589,8 +1589,8 @@ function _stroke_end(width,left, right, spec) = 90-vector_angle([newright[1],newright[0],newleft[0]])/2, jointleft = 8*cutleft/cos(leftangle)/(1+4*bez_k), jointright = 8*cutright/cos(rightangle)/(1+4*bez_k), - pathcutleft = path_cut(newleft,abs(jointleft)), - pathcutright = path_cut(newright,abs(jointright)), + pathcutleft = path_cut_points(newleft,abs(jointleft)), + pathcutright = path_cut_points(newright,abs(jointright)), leftdelete = intright? pathcutleft[1] : pathcutleft[1] + pathclip[1] -1, rightdelete = intright? pathcutright[1] + pathclip[1] -1 : pathcutright[1], leftcorner = line_intersection([pathcutleft[0], newleft[pathcutleft[1]]], [newright[0],newleft[0]]), diff --git a/shapes2d.scad b/shapes2d.scad index 87c0094..85c07e1 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -16,37 +16,54 @@ // Description: // Draws a 2D or 3D path with a given line width. Endcaps can be specified for each end individually. // Figure(Med,NoAxes,VPR=[0,0,0],VPD=250): Endcap Types -// endcaps = [ -// ["butt", "square", "round", "chisel", "tail", "tail2"], -// ["line", "cross", "dot", "diamond", "x", "arrow", "arrow2"] +// cap_pairs = [ +// ["butt", "chisel" ], +// ["round", "square" ], +// ["line", "cross" ], +// ["x", "diamond"], +// ["dot", "block" ], +// ["tail", "arrow" ], +// ["tail2", "arrow2" ] // ]; -// for (x=idx(endcaps), y=idx(endcaps[x])) { -// cap = endcaps[x][y]; -// right(x*60-60+5) fwd(y*10-30) { -// right(28) color("black") text(text=cap, size=5, halign="left", valign="center"); -// stroke([[0,0], [20,0]], width=3, endcap_width=3, endcap1=false, endcap2=cap); -// color("black") stroke([[0,0], [20,0]], width=0.25, endcaps=false); +// for (i = idx(cap_pairs)) { +// fwd((i-len(cap_pairs)/2+0.5)*13) { +// stroke([[-20,0], [20,0]], width=3, endcap1=cap_pairs[i][0], endcap2=cap_pairs[i][1]); +// color("black") { +// stroke([[-20,0], [20,0]], width=0.25, endcaps=false); +// left(28) text(text=cap_pairs[i][0], size=5, halign="right", valign="center"); +// right(28) text(text=cap_pairs[i][1], size=5, halign="left", valign="center"); +// } // } // } // Arguments: -// path = The 2D path to draw along. +// path = The path to draw along. // width = The width of the line to draw. If given as a list of widths, (one for each path point), draws the line with varying thickness to each point. // closed = If true, draw an additional line from the end of the path to the start. +// plots = Specifies the plot point shape for every point of the line. If a 2D path is given, use that to draw custom plot points. +// joints = Specifies the joint shape for each joint of the line. If a 2D path is given, use that to draw custom joints. // endcaps = Specifies the endcap type for both ends of the line. If a 2D path is given, use that to draw custom endcaps. // endcap1 = Specifies the endcap type for the start of the line. If a 2D path is given, use that to draw a custom endcap. // endcap2 = Specifies the endcap type for the end of the line. If a 2D path is given, use that to draw a custom endcap. -// endcap_width = Some endcap types are wider than the line. This specifies the size of endcaps, in multiples of the line width. Default: 3.5 -// endcap_width1 = This specifies the size of starting endcap, in multiples of the line width. Default: 3.5 -// endcap_width2 = This specifies the size of ending endcap, in multiples of the line width. Default: 3.5 -// endcap_length = Length of endcaps, in multiples of the line width. Default: `endcap_width*0.5` -// endcap_length1 = Length of starting endcap, in multiples of the line width. Default: `endcap_width1*0.5` -// endcap_length2 = Length of ending endcap, in multiples of the line width. Default: `endcap_width2*0.5` -// endcap_extent = Extents length of endcaps, in multiples of the line width. Default: `endcap_width*0.5` -// endcap_extent1 = Extents length of starting endcap, in multiples of the line width. Default: `endcap_width1*0.5` -// endcap_extent2 = Extents length of ending endcap, in multiples of the line width. Default: `endcap_width2*0.5` -// endcap_angle = Extra axial rotation given to flat endcaps for 3D paths, in degrees. If not given, the endcaps are fully spun. Default: `undef` (Fully spun cap) -// endcap_angle1 = Extra axial rotation given to a flat starting endcap for 3D paths, in degrees. If not given, the endcap is fully spun. Default: `undef` (Fully spun cap) -// endcap_angle2 = Extra axial rotation given to a flat ending endcap for 3D paths, in degrees. If not given, the endcap is fully spun. Default: `undef` (Fully spun cap) +// plot_width = Some plot point shapes are wider than the line. This specifies the width of the shape, in multiples of the line width. +// joint_width = Some joint shapes are wider than the line. This specifies the width of the shape, in multiples of the line width. +// endcap_width = Some endcap types are wider than the line. This specifies the size of endcaps, in multiples of the line width. +// endcap_width1 = This specifies the size of starting endcap, in multiples of the line width. +// endcap_width2 = This specifies the size of ending endcap, in multiples of the line width. +// plot_length = Length of plot point shape, in multiples of the line width. +// joint_length = Length of joint shape, in multiples of the line width. +// endcap_length = Length of endcaps, in multiples of the line width. +// endcap_length1 = Length of starting endcap, in multiples of the line width. +// endcap_length2 = Length of ending endcap, in multiples of the line width. +// plot_extent = Extents length of plot point shape, in multiples of the line width. +// joint_extent = Extents length of joint shape, in multiples of the line width. +// endcap_extent = Extents length of endcaps, in multiples of the line width. +// endcap_extent1 = Extents length of starting endcap, in multiples of the line width. +// endcap_extent2 = Extents length of ending endcap, in multiples of the line width. +// plot_angle = Extra rotation given to plot point shapes, in degrees. If not given, the shapes are fully spun. +// joint_angle = Extra rotation given to joint shapes, in degrees. If not given, the shapes are fully spun. +// endcap_angle = Extra rotation given to endcaps, in degrees. If not given, the endcaps are fully spun. +// endcap_angle1 = Extra rotation given to a starting endcap, in degrees. If not given, the endcap is fully spun. +// endcap_angle2 = Extra rotation given to a ending endcap, in degrees. If not given, the endcap is fully spun. // trim = Trim the the start and end line segments by this much, to keep them from interfering with custom endcaps. // trim1 = Trim the the starting line segment by this much, to keep it from interfering with a custom endcap. // trim2 = Trim the the ending line segment by this much, to keep it from interfering with a custom endcap. @@ -67,6 +84,12 @@ // Example(2D): Mixed Endcaps // path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; // stroke(path, width=10, endcap1="tail2", endcap2="arrow2"); +// Example(2D): Plotting Points +// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]]; +// stroke(path, width=3, joints="diamond", endcaps="arrow2", plot_angle=0, plot_width=5); +// Example(2D): Joints and Endcaps +// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]]; +// stroke(path, width=3, joints="dot", endcaps="arrow2", joint_angle=0); // Example(2D): Custom Endcap Shapes // path = [[0,100], [100,100], [200,0], [100,-100], [100,0]]; // arrow = [[0,0], [2,-3], [0.5,-2.3], [2,-4], [0.5,-3.5], [-0.5,-3.5], [-2,-4], [-0.5,-2.3], [-2,-3]]; @@ -84,32 +107,56 @@ // Example: 3D Path with Mixed Endcaps // path = rot([15,30,0], p=path3d(pentagon(d=50))); // stroke(path, width=2, endcap1="arrow2", endcap2="tail", endcap_angle2=0, $fn=18); +// Example: 3D Path with Joints and Endcaps +// path = [for (i=[0:10:360]) [(i-180)/2,20*cos(3*i),20*sin(3*i)]]; +// stroke(path, width=2, joints="dot", endcap1="round", endcap2="arrow2", joint_width=2.0, endcap_width2=3, $fn=18); module stroke( path, width=1, closed=false, - endcaps, endcap1, endcap2, + endcaps, endcap1, endcap2, joints, plots, + endcap_width, endcap_width1, endcap_width2, joint_width, plot_width, + endcap_length, endcap_length1, endcap_length2, joint_length, plot_length, + endcap_extent, endcap_extent1, endcap_extent2, joint_extent, plot_extent, + endcap_angle, endcap_angle1, endcap_angle2, joint_angle, plot_angle, trim, trim1, trim2, - endcap_width, endcap_width1, endcap_width2, - endcap_length, endcap_length1, endcap_length2, - endcap_extent, endcap_extent1, endcap_extent2, - endcap_angle, endcap_angle1, endcap_angle2, convexity=10, hull=true ) { - function _endcap_shape(cap,linewidth,w,l,l2) = ( - let(sq2=sqrt(2), l3=l-l2) - (cap=="round" || cap==true)? circle(d=1, $fn=max(8, segs(w/2))) : - cap=="chisel"? [[-0.5,0], [0,0.5], [0.5,0], [0,-0.5]] : - cap=="square"? [[-0.5,-0.5], [-0.5,0.5], [0.5,0.5], [0.5,-0.5]] : - cap=="diamond"? [[0,w/2], [w/2,0], [0,-w/2], [-w/2,0]] : - cap=="dot"? circle(d=3, $fn=max(12, segs(w*3/2))) : - cap=="x"? [for (a=[0:90:270]) each rot(a,p=[[w+sq2/2,w-sq2/2]/2, [w-sq2/2,w+sq2/2]/2, [0,sq2/2]]) ] : - cap=="cross"? [for (a=[0:90:270]) each rot(a,p=[[1,w]/2, [-1,w]/2, [-1,1]/2]) ] : - cap=="line"? [[w/2,0.5], [w/2,-0.5], [-w/2,-0.5], [-w/2,0.5]] : - cap=="arrow"? [[0,0], [w/2,-l2], [w/2,-l2-l], [0,-l], [-w/2,-l2-l], [-w/2,-l2]] : - cap=="arrow2"? [[0,0], [w/2,-l2-l], [0,-l], [-w/2,-l2-l]] : - cap=="tail"? [[0,0], [w/2,l2], [w/2,l2-l], [0,-l], [-w/2,l2-l], [-w/2,l2]] : - cap=="tail2"? [[w/2,0], [w/2,-l], [0,-l-l2], [-w/2,-l], [-w/2,0]] : + function _shape_defaults(cap) = + cap==undef? [1.00, 0.00, 0.00] : + cap==false? [1.00, 0.00, 0.00] : + cap==true? [1.00, 1.00, 0.00] : + cap=="butt"? [1.00, 0.00, 0.00] : + cap=="round"? [1.00, 1.00, 0.00] : + cap=="chisel"? [1.00, 1.00, 0.00] : + cap=="square"? [1.00, 1.00, 0.00] : + cap=="block"? [3.00, 1.00, 0.00] : + cap=="diamond"? [3.50, 1.00, 0.00] : + cap=="dot"? [3.00, 1.00, 0.00] : + cap=="x"? [3.50, 0.40, 0.00] : + cap=="cross"? [4.50, 0.22, 0.00] : + cap=="line"? [4.50, 0.22, 0.00] : + cap=="arrow"? [3.50, 0.40, 0.50] : + cap=="arrow2"? [3.50, 1.00, 0.14] : + cap=="tail"? [3.50, 0.47, 0.50] : + cap=="tail2"? [3.50, 0.28, 0.50] : + is_path(cap)? [0.00, 0.00, 0.00] : + assert(false, str("Invalid cap or joint: ",cap)); + + function _shape_path(cap,linewidth,w,l,l2) = ( + (cap=="butt" || cap==false || cap==undef)? [] : + (cap=="round" || cap==true)? scale([w,l], p=circle(d=1, $fn=max(8, segs(w/2)))) : + cap=="chisel"? scale([w,l], p=circle(d=1,$fn=4)) : + cap=="diamond"? circle(d=w,$fn=4) : + cap=="square"? scale([w,l], p=square(1,center=true)) : + cap=="block"? scale([w,l], p=square(1,center=true)) : + cap=="dot"? circle(d=w, $fn=max(12, segs(w*3/2))) : + cap=="x"? [for (a=[0:90:270]) each rot(a,p=[[w+l/2,w-l/2]/2, [w-l/2,w+l/2]/2, [0,l/2]]) ] : + cap=="cross"? [for (a=[0:90:270]) each rot(a,p=[[l,w]/2, [-l,w]/2, [-l,l]/2]) ] : + cap=="line"? scale([w,l], p=square(1,center=true)) : + cap=="arrow"? [[0,0], [w/2,-l2], [w/2,-l2-l], [0,-l], [-w/2,-l2-l], [-w/2,-l2]] : + cap=="arrow2"? [[0,0], [w/2,-l2-l], [0,-l], [-w/2,-l2-l]] : + cap=="tail"? [[0,0], [w/2,l2], [w/2,l2-l], [0,-l], [-w/2,l2-l], [-w/2,l2]] : + cap=="tail2"? [[w/2,0], [w/2,-l], [0,-l-l2], [-w/2,-l], [-w/2,0]] : is_path(cap)? cap : - is_undef(cap)? [] : assert(false, str("Invalid endcap: ",cap)) ) * linewidth; @@ -123,33 +170,47 @@ module stroke( assert(is_num(width) || (is_vector(width) && len(width)==len(path))); width = is_num(width)? [for (x=path) width] : width; - endcap1 = first_defined([endcap1, endcaps, "round"]); - endcap2 = first_defined([endcap2, endcaps, "round"]); + endcap1 = first_defined([endcap1, endcaps, if(!closed) plots, "round"]); + endcap2 = first_defined([endcap2, endcaps, plots, "round"]); + joints = first_defined([joints, plots, "round"]); assert(is_bool(endcap1) || is_string(endcap1) || is_path(endcap1)); assert(is_bool(endcap2) || is_string(endcap2) || is_path(endcap2)); + assert(is_bool(joints) || is_string(joints) || is_path(joints)); - endcap_width1 = first_defined([endcap_width1, endcap_width, 3.5]); - endcap_width2 = first_defined([endcap_width2, endcap_width, 3.5]); + endcap1_dflts = _shape_defaults(endcap1); + endcap2_dflts = _shape_defaults(endcap2); + joint_dflts = _shape_defaults(joints); + + endcap_width1 = first_defined([endcap_width1, endcap_width, plot_width, endcap1_dflts[0]]); + endcap_width2 = first_defined([endcap_width2, endcap_width, plot_width, endcap2_dflts[0]]); + joint_width = first_defined([joint_width, plot_width, joint_dflts[0]]); assert(is_num(endcap_width1)); assert(is_num(endcap_width2)); + assert(is_num(joint_width)); - endcap_length1 = first_defined([endcap_length1, endcap_length, endcap_width1*0.5]); - endcap_length2 = first_defined([endcap_length2, endcap_length, endcap_width2*0.5]); + endcap_length1 = first_defined([endcap_length1, endcap_length, plot_length, endcap1_dflts[1]*endcap_width1]); + endcap_length2 = first_defined([endcap_length2, endcap_length, plot_length, endcap2_dflts[1]*endcap_width2]); + joint_length = first_defined([joint_length, plot_length, joint_dflts[1]*joint_width]); assert(is_num(endcap_length1)); assert(is_num(endcap_length2)); + assert(is_num(joint_length)); - endcap_extent1 = first_defined([endcap_extent1, endcap_extent, endcap_width1*0.5]); - endcap_extent2 = first_defined([endcap_extent2, endcap_extent, endcap_width2*0.5]); + endcap_extent1 = first_defined([endcap_extent1, endcap_extent, plot_extent, endcap1_dflts[2]*endcap_width1]); + endcap_extent2 = first_defined([endcap_extent2, endcap_extent, plot_extent, endcap2_dflts[2]*endcap_width2]); + joint_extent = first_defined([joint_extent, plot_extent, joint_dflts[2]*joint_width]); assert(is_num(endcap_extent1)); assert(is_num(endcap_extent2)); + assert(is_num(joint_extent)); - endcap_angle1 = first_defined([endcap_angle1, endcap_angle]); - endcap_angle2 = first_defined([endcap_angle2, endcap_angle]); + endcap_angle1 = first_defined([endcap_angle1, endcap_angle, plot_angle]); + endcap_angle2 = first_defined([endcap_angle2, endcap_angle, plot_angle]); + joint_angle = first_defined([joint_angle, plot_angle]); assert(is_undef(endcap_angle1)||is_num(endcap_angle1)); assert(is_undef(endcap_angle2)||is_num(endcap_angle2)); + assert(is_undef(joint_angle)||is_num(joint_angle)); - endcap_shape1 = _endcap_shape(endcap1, select(width,0), endcap_width1, endcap_length1, endcap_extent1); - endcap_shape2 = _endcap_shape(endcap2, select(width,-1), endcap_width2, endcap_length2, endcap_extent2); + endcap_shape1 = _shape_path(endcap1, select(width,0), endcap_width1, endcap_length1, endcap_extent1); + endcap_shape2 = _shape_path(endcap2, select(width,-1), endcap_width2, endcap_length2, endcap_extent2); trim1 = select(width,0) * first_defined([ trim1, trim, @@ -201,17 +262,29 @@ module stroke( // Joints for (i = [1:1:len(path2)-2]) { $fn = quantup(segs(widths[i]/2),4); - if (hull) { - hull() { - translate(path2[i]) { + translate(path2[i]) { + if (joints != undef) { + joint_shape = _shape_path( + joints, width[i], + joint_width, + joint_length, + joint_extent + ); + v1 = unit(path2[i] - path2[i-1]); + v2 = unit(path2[i+1] - path2[i]); + vec = unit((v1+v2)/2); + mat = is_undef(joint_angle) + ? rot(from=BACK,to=v1) + : zrot(joint_angle); + multmatrix(mat) polygon(joint_shape); + } else if (hull) { + hull() { rot(from=BACK, to=path2[i]-path2[i-1]) circle(d=widths[i]); rot(from=BACK, to=path2[i+1]-path2[i]) circle(d=widths[i]); } - } - } else { - translate(path2[i]) { + } else { rot(from=BACK, to=path2[i]-path2[i-1]) circle(d=widths[i]); rot(from=BACK, to=path2[i+1]-path2[i]) @@ -222,17 +295,16 @@ module stroke( // Endcap1 translate(path[0]) { - start_vec = select(path,0) - select(path,1); - rot(from=BACK, to=start_vec) { - polygon(endcap_shape1); - } + mat = is_undef(endcap_angle1)? rot(from=BACK,to=start_vec) : + zrot(endcap_angle1); + multmatrix(mat) polygon(endcap_shape1); } // Endcap2 translate(select(path,-1)) { - rot(from=BACK, to=end_vec) { - polygon(endcap_shape2); - } + mat = is_undef(endcap_angle2)? rot(from=BACK,to=end_vec) : + zrot(endcap_angle2); + multmatrix(mat) polygon(endcap_shape2); } } else { quatsums = Q_Cumulative([ @@ -266,7 +338,30 @@ module stroke( for (i = [1:1:len(path2)-2]) { $fn = sides[i]; translate(path2[i]) { - if (hull) { + if (joints != undef) { + joint_shape = _shape_path( + joints, width[i], + joint_width, + joint_length, + joint_extent + ); + multmatrix(rotmats[i] * xrot(180)) { + $fn = sides[i]; + if (is_undef(joint_angle)) { + rotate_extrude(convexity=convexity) { + right_half(planar=true) { + polygon(joint_shape); + } + } + } else { + rotate([90,0,joint_angle]) { + linear_extrude(height=max(widths[i],0.001), center=true, convexity=convexity) { + polygon(joint_shape); + } + } + } + } + } else if (hull) { hull(){ multmatrix(rotmats[i]) { sphere(d=widths[i],style="aligned"); @@ -330,6 +425,60 @@ module stroke( } +// Function&Module: dashed_stroke() +// Usage: As a Module +// dashed_stroke(path, dashpat, ); +// Usage: As a Function +// dashes = dashed_stroke(path, dashpat, width=, ); +// Topics: Paths, Drawing Tools +// See Also: stroke(), path_cut_segs() +// Description: +// Given a path and a dash pattern, creates a dashed line that follows that +// path with the given dash pattern. +// - When called as a function, returns a list of dash sub-paths. +// - When called as a module, draws all those subpaths using `stroke()`. +// Arguments: +// path = The path to subdivide into dashes. +// dashpat = A list of alternating dash lengths and space lengths for the dash pattern. This will be scaled by the width of the line. +// --- +// width = The width of the dashed line to draw. Module only. Default: 1 +// closed = If true, treat path as a closed polygon. Default: false +// Example(2D): Open Path +// path = [for (a=[-180:10:180]) [a/3,20*sin(a)]]; +// dashed_stroke(path, [3,2], width=1); +// Example(2D): Closed Polygon +// path = circle(d=100,$fn=72); +// dashpat = [10,2,3,2,3,2]; +// dashed_stroke(path, dashpat, width=1, closed=true); +// Example(FlatSpin,VPD=250): 3D Dashed Path +// path = [for (a=[-180:5:180]) [a/3, 20*cos(3*a), 20*sin(3*a)]]; +// dashed_stroke(path, [3,2], width=1); +function dashed_stroke(path, dashpat=[3,3], closed=false) = + let( + path = closed? close_path(path) : path, + dashpat = len(dashpat)%2==0? dashpat : concat(dashpat,[0]), + plen = path_length(path), + dlen = sum(dashpat), + doff = cumsum(dashpat), + reps = floor(plen / dlen), + step = plen / reps, + cuts = [ + for (i=[0:1:reps-1], off=doff) + let (st=i*step, x=st+off) + if (x>0 && x Date: Sat, 6 Mar 2021 02:45:29 -0800 Subject: [PATCH 3/4] Fix regressions. --- tests/test_geometry.scad | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/test_geometry.scad b/tests/test_geometry.scad index fbcf4de..817323d 100644 --- a/tests/test_geometry.scad +++ b/tests/test_geometry.scad @@ -47,7 +47,6 @@ test_projection_on_plane(); test_plane_point_nearest_origin(); test_distance_from_plane(); -test_closest_point_on_plane(); test__general_plane_line_intersection(); test_plane_line_angle(); test_normalize_plane(); @@ -111,15 +110,6 @@ function info_str(list,i=0,string=chr(10)) = : info_str(list,i+1,str(string,str(list[i][0],_valstr(list[i][1]),chr(10)))); -module test_closest_point_on_plane(){ - plane = rands(-5,5,4,seed=175)+[10,0,0,0]; - point = rands(-1,1,3,seed=477); - point2 = closest_point_on_plane(plane,point); - assert_approx(norm(point-point2), abs(distance_from_plane(plane,point))); -} -*test_closest_point_on_plane(); - - module test_normalize_plane(){ plane = rands(-5,5,4,seed=333)+[10,0,0,0]; plane2 = normalize_plane(plane); From b2001ecadf9468d3c10dcce2d5fe44bcf97cfe28 Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Sat, 6 Mar 2021 02:54:03 -0800 Subject: [PATCH 4/4] Removed redundant path_cut_segs() --- paths.scad | 55 ++++++++++++------------------------------------------ 1 file changed, 12 insertions(+), 43 deletions(-) diff --git a/paths.scad b/paths.scad index 23c071d..e40d65a 100644 --- a/paths.scad +++ b/paths.scad @@ -1217,49 +1217,11 @@ module path_spread(path, n, spacing, sp=undef, rotate_children=true, closed=fals } } -// Function: path_cut_segs() -// Usage: -// segs = path_cut_segs(path, cutlens, ); -// Topics: Paths -// See Also: path_cut_points() -// Description: -// Given a path and a list of path lengths at which to cut, returns a list of -// sub-paths cut from the original path at those path lengths. -// Arguments: -// path = The original path to split. -// cutlens = The list of path lengths to cut at. -// closed = If true, treat the path as a closed polygon. -// Example(2D): -// path = circle(d=100); -// segs = path_cut_segs(path, [50, 200], closed=true); -// rainbow(segs) stroke($item); -function path_cut_segs(path,cutlens,closed=false) = - let( - path = closed? close_path(path) : path, - cutlist = path_cut_points(path, cutlens), - cuts = len(cutlist) - ) [ - concat( - select(path,0,cutlist[0][1]-1), - [cutlist[0][0]] - ), - for(i=[0:cuts-2]) concat( - [cutlist[i][0]], - slice(path, cutlist[i][1], cutlist[i+1][1]), - [cutlist[i+1][0]] - ), - concat( - [cutlist[cuts-1][0]], - select(path, cutlist[cuts-1][1], -1) - ) - ]; - - // Function: path_cut_points() // // Usage: -// path_cut_points(path, dists, [closed], [direction]) +// cuts = path_cut_points(path, dists, , ); // // Description: // Cuts a path at a list of distances from the first point in the path. Returns a list of the cut @@ -1280,6 +1242,7 @@ function path_cut_segs(path,cutlens,closed=false) = // Arguments: // path = path to cut // dists = distances where the path should be cut (a list) or a scalar single distance +// --- // closed = set to true if the curve is closed. Default: false // direction = set to true to return direction vectors. Default: false // @@ -1372,8 +1335,10 @@ function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) = // Function: path_cut_segs() +// Topics: Paths +// See Also: path_cut_points() // Usage: -// path_list = path_cut_segs(path, cutdist, ); +// path_list = path_cut_segs(path, cutdist, ); // Description: // Given a list of distances in `cutdist`, cut the path into // subpaths at those lengths, returning a list of paths. @@ -1382,9 +1347,13 @@ function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) = // in ascending order. If you repeat a distance you will get an // empty list in that position in the output. // Arguments: -// path = path to cut -// cutdist = distance or list of distances where path is cut -// closed = set to true for a closed path. Default: false +// path = The original path to split. +// cutdist = Distance or list of distances where path is cut +// closed = If true, treat the path as a closed polygon. +// Example(2D): +// path = circle(d=100); +// segs = path_cut_segs(path, [50, 200], closed=true); +// rainbow(segs) stroke($item); function path_cut_segs(path,cutdist,closed) = is_num(cutdist) ? path_cut_segs(path,[cutdist],closed) : assert(is_vector(cutdist))