2021-09-16 03:12:51 +00:00
//////////////////////////////////////////////////////////////////////
// LibFile: drawing.scad
// This file includes stroke(), which converts a path into a
// geometric object, like drawing with a pen. It even works on
// three-dimensional paths. You can make a dashed line or add arrow
// heads. The turtle() function provides a turtle graphics style
// approach for producing paths. The arc() function produces arc paths,
2022-04-21 04:26:20 +00:00
// and helix() produces helical paths.
2021-09-16 03:12:51 +00:00
// Includes:
// include <BOSL2/std.scad>
2021-12-13 23:48:30 +00:00
// FileGroup: Basic Modeling
2022-01-02 03:00:26 +00:00
// FileSummary: Create and draw 2D and 3D paths: arc, helix, turtle graphics
2021-12-13 23:48:30 +00:00
// FileFootnotes: STD=Included in std.scad
2021-09-16 03:12:51 +00:00
//////////////////////////////////////////////////////////////////////
// Section: Line Drawing
// Module: stroke()
2023-03-28 06:56:04 +00:00
// Synopsis: Draws a line along a path or region boundry.
2023-05-27 08:42:09 +00:00
// SynTags: Geom
2023-03-27 23:30:04 +00:00
// Topics: Paths (2D), Paths (3D), Drawing Tools
2024-01-26 23:09:39 +00:00
// See Also: dashed_stroke(), offset_stroke(), path_sweep()
2021-09-16 03:12:51 +00:00
// Usage:
// stroke(path, [width], [closed], [endcaps], [endcap_width], [endcap_length], [endcap_extent], [trim]);
// stroke(path, [width], [closed], [endcap1], [endcap2], [endcap_width1], [endcap_width2], [endcap_length1], [endcap_length2], [endcap_extent1], [endcap_extent2], [trim1], [trim2]);
// Description:
2021-10-05 02:11:22 +00:00
// Draws a 2D or 3D path with a given line width. Joints and each endcap can be replaced with
// various marker shapes, and can be assigned different colors. If passed a region instead of
// a path, draws each path in the region as a closed polygon by default. If `closed=false` is
2022-11-10 03:22:30 +00:00
// given with a region or list of paths, then each path is drawn without the closing line segment.
2023-04-24 01:38:27 +00:00
// When drawing a closed path or region, there are no endcaps, so you cannot give the endcap parameters.
2022-11-10 03:22:30 +00:00
// To facilitate debugging, stroke() accepts "paths" that have a single point. These are drawn with
// the style of endcap1, but have their own scale parameter, `singleton_scale`, which defaults to 2
2023-03-05 17:11:30 +00:00
// so that singleton dots with endcap "round" are clearly visible.
// .
// In 2d the stroke module works by creating a sequence of rectangles (or trapezoids if line width varies) and
// filling in the gaps with rounded wedges. This is fast and produces a good result. In 3d the modules
// creates a cylinders (or cones) and fills the gaps with rounded wedges made using rotate_extrude. This process will be slow for
// long paths due to the 3d unions, and the faces on sequential cylinders may not line up. In many cases, {{path_sweep()}} will be
// a better choice, both running faster and producing superior output, when working in three dimensions.
2021-09-16 03:12:51 +00:00
// Figure(Med,NoAxes,2D,VPR=[0,0,0],VPD=250): Endcap Types
// cap_pairs = [
// ["butt", "chisel" ],
// ["round", "square" ],
// ["line", "cross" ],
// ["x", "diamond"],
// ["dot", "block" ],
// ["tail", "arrow" ],
// ["tail2", "arrow2" ]
// ];
// 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 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.
2021-10-03 23:27:00 +00:00
// joints = Specifies the joint shape for each joint of the line. If a 2D polygon is given, use that to draw custom joints.
// endcaps = Specifies the endcap type for both ends of the line. If a 2D polygon is given, use that to draw custom endcaps.
// endcap1 = Specifies the endcap type for the start of the line. If a 2D polygon is given, use that to draw a custom endcap.
// endcap2 = Specifies the endcap type for the end of the line. If a 2D polygon is given, use that to draw a custom endcap.
// dots = Specifies both the endcap and joint types with one argument. If given `true`, sets both to "dot". If a 2D polygon is given, uses that to draw custom dots.
2021-09-16 03:12:51 +00:00
// 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.
2021-10-03 23:27:00 +00:00
// dots_width = This specifies the size of the joints and endcaps, in multiples of the line width.
2021-09-16 03:12:51 +00:00
// 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.
2021-10-03 23:27:00 +00:00
// dots_length = Length of both joints and endcaps, in multiples of the line width.
2021-09-16 03:12:51 +00:00
// 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.
2021-10-03 23:27:00 +00:00
// dots_extent = Extents length of both joints and endcaps, in multiples of the line width.
// joint_angle = Extra rotation given to joint shapes, in degrees. If not given, the shapes are fully spun (for 3D lines).
// endcap_angle = Extra rotation given to endcaps, in degrees. If not given, the endcaps are fully spun (for 3D lines).
// endcap_angle1 = Extra rotation given to a starting endcap, in degrees. If not given, the endcap is fully spun (for 3D lines).
// endcap_angle2 = Extra rotation given to a ending endcap, in degrees. If not given, the endcap is fully spun (for 3D lines).
// dots_angle = Extra rotation given to both joints and endcaps, in degrees. If not given, the endcap is fully spun (for 3D lines).
2021-09-16 03:12:51 +00:00
// 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.
2021-10-02 07:24:33 +00:00
// color = If given, sets the color of the line segments, joints and endcap.
2021-10-03 23:27:00 +00:00
// endcap_color = If given, sets the color of both endcaps. Overrides `color=` and `dots_color=`.
// endcap_color1 = If give, sets the color of the starting endcap. Overrides `color=`, `dots_color=`, and `endcap_color=`.
// endcap_color2 = If given, sets the color of the ending endcap. Overrides `color=`, `dots_color=`, and `endcap_color=`.
// joint_color = If given, sets the color of the joints. Overrides `color=` and `dots_color=`.
// dots_color = If given, sets the color of the endcaps and joints. Overrides `color=`.
2022-11-10 03:22:30 +00:00
// singleton_scale = Change the scale of the endcap shape drawn for singleton paths. Default: 2.
2021-09-16 03:12:51 +00:00
// convexity = Max number of times a line could intersect a wall of an endcap.
// Example(2D): Drawing a Path
// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
// stroke(path, width=20);
// Example(2D): Closing a Path
// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
2023-04-24 03:36:07 +00:00
// stroke(path, width=20, closed=true);
2021-09-16 03:12:51 +00:00
// Example(2D): Fancy Arrow Endcaps
// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
// stroke(path, width=10, endcaps="arrow2");
// Example(2D): Modified Fancy Arrow Endcaps
// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
// stroke(path, width=10, endcaps="arrow2", endcap_width=6, endcap_length=3, endcap_extent=2);
// Example(2D): Mixed Endcaps
// path = [[0,100], [100,100], [200,0], [100,-100], [100,0]];
// stroke(path, width=10, endcap1="tail2", endcap2="arrow2");
2022-11-10 03:22:30 +00:00
// Example(2D): Plotting Points. Setting endcap_angle to zero results in the weird arrow orientation.
2021-09-16 03:12:51 +00:00
// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]];
2021-10-02 07:24:33 +00:00
// stroke(path, width=3, joints="diamond", endcaps="arrow2", endcap_angle=0, endcap_width=5, joint_angle=0, joint_width=5);
2023-04-24 01:38:27 +00:00
// Example(2D): Default joint gives curves along outside corners of the path:
// stroke([square(40)], width=18);
// Example(2D): Setting `joints="square"` gives flat outside corners
// stroke([square(40)], width=18, joints="square");
// Example(2D): Setting `joints="butt"` does not draw any transitions, just rectangular strokes for each segment, meeting at their centers:
// stroke([square(40)], width=18, joints="butt");
2021-09-16 03:12:51 +00:00
// Example(2D): Joints and Endcaps
// path = [for (a=[0:30:360]) [a-180, 60*sin(a)]];
2021-10-08 01:31:58 +00:00
// stroke(path, width=8, joints="dot", endcaps="arrow2");
2021-09-16 03:12:51 +00:00
// 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]];
// stroke(path, width=10, trim=3.5, endcaps=arrow);
// Example(2D): Variable Line Width
// path = circle(d=50,$fn=18);
// widths = [for (i=idx(path)) 10*i/len(path)+2];
// stroke(path,width=widths,$fa=1,$fs=1);
// Example: 3D Path with Endcaps
// path = rot([15,30,0], p=path3d(pentagon(d=50)));
// stroke(path, width=2, endcaps="arrow2", $fn=18);
// Example: 3D Path with Flat Endcaps
// path = rot([15,30,0], p=path3d(pentagon(d=50)));
// stroke(path, width=2, endcaps="arrow2", endcap_angle=0, $fn=18);
// 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);
2021-10-02 07:24:33 +00:00
// Example: Coloring Lines, Joints, and Endcaps
// path = [for (i=[0:15:360]) [(i-180)/3,20*cos(2*i),20*sin(2*i)]];
// stroke(
// path, width=2, joints="dot", endcap1="dot", endcap2="arrow2",
// color="lightgreen", joint_color="red", endcap_color="blue",
// joint_width=2.0, endcap_width2=3, $fn=18
// );
2021-10-05 02:11:22 +00:00
// Example(2D): Simplified Plotting
2021-10-03 23:27:00 +00:00
// path = [for (i=[0:15:360]) [(i-180)/3,20*cos(2*i)]];
// stroke(path, width=2, dots=true, color="lightgreen", dots_color="red", $fn=18);
2021-10-05 02:11:22 +00:00
// Example(2D): Drawing a Region
// rgn = [square(100,center=true), circle(d=60,$fn=18)];
// stroke(rgn, width=2);
// Example(2D): Drawing a List of Lines
// paths = [
// for (y=[-60:60:60]) [
// for (a=[-180:15:180])
// [a, 2*y+60*sin(a+y)]
// ]
// ];
// stroke(paths, closed=false, width=5);
2022-11-10 03:22:30 +00:00
// Example(2D): Paths with a singleton. Note that the singleton is not a single point, but a list containing a single point.
// stroke([
// [[0,0],[1,1]],
// [[1.5,1.5]],
// [[2,2],[3,3]]
// ],width=0.2,closed=false,$fn=16);
2021-09-16 03:12:51 +00:00
function stroke (
2021-10-04 23:19:27 +00:00
path , width = 1 , closed ,
2021-10-03 23:27:00 +00:00
endcaps , endcap1 , endcap2 , joints , dots ,
endcap_width , endcap_width1 , endcap_width2 , joint_width , dots_width ,
endcap_length , endcap_length1 , endcap_length2 , joint_length , dots_length ,
endcap_extent , endcap_extent1 , endcap_extent2 , joint_extent , dots_extent ,
endcap_angle , endcap_angle1 , endcap_angle2 , joint_angle , dots_angle ,
endcap_color , endcap_color1 , endcap_color2 , joint_color , dots_color , color ,
2022-11-10 03:22:30 +00:00
trim , trim1 , trim2 , singleton_scale = 2 ,
2022-08-21 09:49:47 +00:00
convexity = 10
2021-09-16 03:12:51 +00:00
) = no_function ( "stroke" ) ;
2021-11-19 03:33:57 +00:00
2021-09-16 03:12:51 +00:00
module stroke (
2021-10-04 23:19:27 +00:00
path , width = 1 , closed ,
2021-10-03 23:27:00 +00:00
endcaps , endcap1 , endcap2 , joints , dots ,
endcap_width , endcap_width1 , endcap_width2 , joint_width , dots_width ,
endcap_length , endcap_length1 , endcap_length2 , joint_length , dots_length ,
endcap_extent , endcap_extent1 , endcap_extent2 , joint_extent , dots_extent ,
endcap_angle , endcap_angle1 , endcap_angle2 , joint_angle , dots_angle ,
endcap_color , endcap_color1 , endcap_color2 , joint_color , dots_color , color ,
2022-11-10 03:22:30 +00:00
trim , trim1 , trim2 , singleton_scale = 2 ,
2022-08-21 09:49:47 +00:00
convexity = 10
2021-09-16 03:12:51 +00:00
) {
2021-10-10 01:44:26 +00:00
no_children ( $children ) ;
2021-10-02 07:24:33 +00:00
module setcolor ( clr ) {
if ( clr = = undef ) {
children ( ) ;
} else {
color ( clr ) children ( ) ;
}
}
2021-09-16 03:12:51 +00:00
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 ] :
2021-10-03 23:27:00 +00:00
cap = = "block" ? [ 2.00 , 1.00 , 0.00 ] :
cap = = "diamond" ? [ 2.50 , 1.00 , 0.00 ] :
cap = = "dot" ? [ 2.00 , 1.00 , 0.00 ] :
cap = = "x" ? [ 2.50 , 0.40 , 0.00 ] :
cap = = "cross" ? [ 3.00 , 0.33 , 0.00 ] :
cap = = "line" ? [ 3.50 , 0.22 , 0.00 ] :
2021-09-16 03:12:51 +00: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 ) = (
2022-11-10 03:22:30 +00:00
cap = = "butt" || cap = = false || cap = = undef ? [ ] :
cap = = "round" || cap = = true ? scale ( [ w , l ] , p = circle ( d = 1 , $fn = max ( 8 , segs ( w / 2 ) ) ) ) :
2021-09-16 03:12:51 +00:00
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 :
assert ( false , str ( "Invalid endcap: " , cap ) )
) * linewidth ;
2021-10-04 23:19:27 +00:00
closed = default ( closed , is_region ( path ) ) ;
2023-04-24 01:38:27 +00:00
check1 = assert ( is_bool ( closed ) )
assert ( ! closed || num_defined ( [ endcaps , endcap1 , endcap2 ] ) = = 0 , "Cannot give endcap parameter(s) with closed path or region" ) ;
2021-09-16 03:12:51 +00:00
2021-10-03 23:27:00 +00:00
dots = dots = = true ? "dot" : dots ;
endcap1 = first_defined ( [ endcap1 , endcaps , dots , "round" ] ) ;
endcap2 = first_defined ( [ endcap2 , endcaps , if ( ! closed ) dots , "round" ] ) ;
joints = first_defined ( [ joints , dots , "round" ] ) ;
2023-04-24 01:38:27 +00:00
check2 =
2022-11-09 21:54:27 +00:00
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 ) ) ;
2021-09-16 03:12:51 +00:00
endcap1_dflts = _shape_defaults ( endcap1 ) ;
endcap2_dflts = _shape_defaults ( endcap2 ) ;
joint_dflts = _shape_defaults ( joints ) ;
2021-10-03 23:27:00 +00:00
endcap_width1 = first_defined ( [ endcap_width1 , endcap_width , dots_width , endcap1_dflts [ 0 ] ] ) ;
endcap_width2 = first_defined ( [ endcap_width2 , endcap_width , dots_width , endcap2_dflts [ 0 ] ] ) ;
joint_width = first_defined ( [ joint_width , dots_width , joint_dflts [ 0 ] ] ) ;
2021-09-16 03:12:51 +00:00
2021-10-03 23:27:00 +00:00
endcap_length1 = first_defined ( [ endcap_length1 , endcap_length , dots_length , endcap1_dflts [ 1 ] * endcap_width1 ] ) ;
endcap_length2 = first_defined ( [ endcap_length2 , endcap_length , dots_length , endcap2_dflts [ 1 ] * endcap_width2 ] ) ;
joint_length = first_defined ( [ joint_length , dots_length , joint_dflts [ 1 ] * joint_width ] ) ;
2021-09-16 03:12:51 +00:00
2021-10-03 23:27:00 +00:00
endcap_extent1 = first_defined ( [ endcap_extent1 , endcap_extent , dots_extent , endcap1_dflts [ 2 ] * endcap_width1 ] ) ;
endcap_extent2 = first_defined ( [ endcap_extent2 , endcap_extent , dots_extent , endcap2_dflts [ 2 ] * endcap_width2 ] ) ;
joint_extent = first_defined ( [ joint_extent , dots_extent , joint_dflts [ 2 ] * joint_width ] ) ;
2021-09-16 03:12:51 +00:00
2021-10-03 23:27:00 +00:00
endcap_angle1 = first_defined ( [ endcap_angle1 , endcap_angle , dots_angle ] ) ;
endcap_angle2 = first_defined ( [ endcap_angle2 , endcap_angle , dots_angle ] ) ;
2021-10-04 23:19:27 +00:00
joint_angle = first_defined ( [ joint_angle , dots_angle ] ) ;
2022-11-10 03:22:30 +00:00
check3 =
assert ( all_nonnegative ( [ endcap_length1 ] ) )
assert ( all_nonnegative ( [ endcap_length2 ] ) )
assert ( all_nonnegative ( [ joint_length ] ) ) ;
assert ( all_nonnegative ( [ endcap_extent1 ] ) )
assert ( all_nonnegative ( [ endcap_extent2 ] ) )
assert ( all_nonnegative ( [ joint_extent ] ) ) ;
assert ( is_undef ( endcap_angle1 ) || is_finite ( endcap_angle1 ) )
assert ( is_undef ( endcap_angle2 ) || is_finite ( endcap_angle2 ) )
assert ( is_undef ( joint_angle ) || is_finite ( joint_angle ) )
assert ( all_positive ( [ singleton_scale ] ) )
assert ( all_positive ( width ) ) ;
2021-10-03 23:27:00 +00:00
endcap_color1 = first_defined ( [ endcap_color1 , endcap_color , dots_color , color ] ) ;
endcap_color2 = first_defined ( [ endcap_color2 , endcap_color , dots_color , color ] ) ;
joint_color = first_defined ( [ joint_color , dots_color , color ] ) ;
2022-11-10 03:22:30 +00:00
// We want to allow "paths" with length 1, so we can't use the normal path/region checks
paths = is_matrix ( path ) ? [ path ] : path ;
assert ( is_list ( paths ) , "The path argument must be a list of 2D or 3D points, or a region." ) ;
2023-04-24 01:38:27 +00:00
attachable ( ) {
for ( path = paths ) {
pathvalid = is_path ( path , [ 2 , 3 ] ) || same_shape ( path , [ [ 0 , 0 ] ] ) || same_shape ( path , [ [ 0 , 0 , 0 ] ] ) ;
assert ( pathvalid , "The path argument must be a list of 2D or 3D points, or a region." ) ;
2021-10-04 23:19:27 +00:00
2023-04-24 01:38:27 +00:00
check4 = assert ( is_num ( width ) || len ( width ) = = len ( path ) ,
"width must be a number or a vector the same length as the path (or all components of a region)" ) ;
path = deduplicate ( closed ? list_wrap ( path ) : path ) ;
width = is_num ( width ) ? [ for ( x = path ) width ]
: closed ? list_wrap ( width )
: width ;
check4a = assert ( len ( width ) = = len ( path ) , "path had duplicated points and width was given as a list: this is not allowd" ) ;
2021-09-16 03:12:51 +00:00
2023-04-24 01:38:27 +00:00
endcap_shape1 = _shape_path ( endcap1 , width [ 0 ] , endcap_width1 , endcap_length1 , endcap_extent1 ) ;
endcap_shape2 = _shape_path ( endcap2 , last ( width ) , endcap_width2 , endcap_length2 , endcap_extent2 ) ;
2021-09-16 03:12:51 +00:00
2023-04-24 01:38:27 +00:00
trim1 = width [ 0 ] * first_defined ( [
trim1 , trim ,
( endcap1 = = "arrow" ) ? endcap_length1 - 0.01 :
( endcap1 = = "arrow2" ) ? endcap_length1 * 3 / 4 :
0
] ) ;
2021-09-19 03:54:27 +00:00
2023-04-24 01:38:27 +00:00
trim2 = last ( width ) * first_defined ( [
trim2 , trim ,
( endcap2 = = "arrow" ) ? endcap_length2 - 0.01 :
( endcap2 = = "arrow2" ) ? endcap_length2 * 3 / 4 :
0
] ) ;
check10 = assert ( is_finite ( trim1 ) )
assert ( is_finite ( trim2 ) ) ;
2021-10-04 23:19:27 +00:00
2023-04-24 01:38:27 +00:00
if ( len ( path ) = = 1 ) {
if ( len ( path [ 0 ] ) = = 2 ) {
// Endcap1
setcolor ( endcap_color1 ) {
translate ( path [ 0 ] ) {
mat = is_undef ( endcap_angle1 ) ? ident ( 3 ) : zrot ( endcap_angle1 ) ;
multmatrix ( mat ) polygon ( scale ( singleton_scale , endcap_shape1 ) ) ;
}
}
} else {
// Endcap1
setcolor ( endcap_color1 ) {
translate ( path [ 0 ] ) {
$fn = segs ( width [ 0 ] / 2 ) ;
if ( is_undef ( endcap_angle1 ) ) {
rotate_extrude ( convexity = convexity ) {
right_half ( planar = true ) {
polygon ( endcap_shape1 ) ;
}
}
} else {
rotate ( [ 90 , 0 , endcap_angle1 ] ) {
linear_extrude ( height = max ( widths [ 0 ] , 0.001 ) , center = true , convexity = convexity ) {
polygon ( endcap_shape1 ) ;
}
}
}
}
}
}
} else {
dummy = assert ( trim1 < path_length ( path ) - trim2 , "Path is too short for endcap(s). Try a smaller width, or set endcap_length to a smaller value." ) ;
// This section shortens the path to allow room for the specified endcaps. Note that if
// the path is closed, there are not endcaps, so we don't shorten the path, but in that case we
// duplicate entry 1 so that the path wraps around a little more and we can correctly create all the joints.
// (Why entry 1? Because entry 0 was already duplicated by a list_wrap() call.)
pathcut = path_cut_points ( path , [ trim1 , path_length ( path ) - trim2 ] , closed = false ) ;
pathcut_su = _cut_to_seg_u_form ( pathcut , path ) ;
path2 = closed ? [ each path , path [ 1 ] ]
: _path_cut_getpaths ( path , pathcut , closed = false ) [ 1 ] ;
widths = closed ? [ each width , width [ 1 ] ]
: _path_select ( width , pathcut_su [ 0 ] [ 0 ] , pathcut_su [ 0 ] [ 1 ] , pathcut_su [ 1 ] [ 0 ] , pathcut_su [ 1 ] [ 1 ] ) ;
start_vec = path [ 0 ] - path [ 1 ] ;
end_vec = last ( path ) - select ( path , - 2 ) ;
2021-09-16 03:12:51 +00:00
2023-04-24 01:38:27 +00:00
if ( len ( path [ 0 ] ) = = 2 ) { // Two dimensional case
// Straight segments
setcolor ( color ) {
for ( i = idx ( path2 , e = - 2 ) ) {
seg = select ( path2 , i , i + 1 ) ;
delt = seg [ 1 ] - seg [ 0 ] ;
translate ( seg [ 0 ] ) {
rot ( from = BACK , to = delt ) {
trapezoid ( w1 = widths [ i ] , w2 = widths [ i + 1 ] , h = norm ( delt ) , anchor = FRONT ) ;
}
}
}
}
2021-09-16 03:12:51 +00:00
2023-04-24 01:38:27 +00:00
// Joints
setcolor ( joint_color ) {
for ( i = [ 1 : 1 : len ( path2 ) - 2 ] ) {
$fn = quantup ( segs ( widths [ i ] / 2 ) , 4 ) ;
translate ( path2 [ i ] ) {
if ( joints ! = undef && joints ! = "round" && joints ! = "square" ) {
joint_shape = _shape_path (
joints , widths [ i ] ,
joint_width ,
joint_length ,
joint_extent
) ;
v1 = unit ( path2 [ i ] - path2 [ i - 1 ] ) ;
v2 = unit ( path2 [ i + 1 ] - path2 [ i ] ) ;
mat = is_undef ( joint_angle )
? rot ( from = BACK , to = v1 )
: zrot ( joint_angle ) ;
multmatrix ( mat ) polygon ( joint_shape ) ;
} else {
// These are parallel to the path
v1 = path2 [ i ] - path2 [ i - 1 ] ;
v2 = path2 [ i + 1 ] - path2 [ i ] ;
ang = modang ( v_theta ( v2 ) - v_theta ( v1 ) ) ;
// Need 90 deg offset to make wedge perpendicular to path, and the wedge
// position depends on whether we turn left (ang<0) or right (ang>0)
theta = v_theta ( v1 ) - sign ( ang ) * 90 ;
if ( ! approx ( ang , 0 ) ) {
// This section creates a rounded wedge to fill in gaps. The wedge needs to be oversized for overlap
// in all directions, including its apex, but not big enough to create artifacts.
// The core of the wedge is the proper arc we need to create. We then add side points based
// on firstang and secondang, where we try 1 degree, but if that appears too big we based it
// on the segment length. We pick the radius based on the smaller of the width at this point
// and the adjacent width, which could be much smaller---meaning that we need a much smaller radius.
// The apex offset we pick to be simply based on the width at this point.
firstang = sign ( ang ) * min ( 1 , 0.5 * norm ( v1 ) / PI / widths [ i ] * 360 ) ;
secondang = sign ( ang ) * min ( 1 , 0.5 * norm ( v2 ) / PI / widths [ i ] * 360 ) ;
firstR = 0.5 * min ( widths [ i ] , lerp ( widths [ i ] , widths [ i - 1 ] , abs ( firstang ) * PI * widths [ i ] / 360 / norm ( v1 ) ) ) ;
secondR = 0.5 * min ( widths [ i ] , lerp ( widths [ i ] , widths [ i + 1 ] , abs ( secondang ) * PI * widths [ i ] / 360 / norm ( v2 ) ) ) ;
apex_offset = widths [ i ] / 10 ;
arcpath = [
firstR * [ cos ( theta - firstang ) , sin ( theta - firstang ) ] ,
each arc ( d = widths [ i ] , angle = [ theta , theta + ang ] , n = joints = = "square" ? 2 : undef ) ,
secondR * [ cos ( theta + ang + secondang ) , sin ( theta + ang + secondang ) ] ,
- apex_offset * [ cos ( theta + ang / 2 ) , sin ( theta + ang / 2 ) ]
] ;
polygon ( arcpath ) ;
}
}
}
}
}
if ( ! closed ) {
// Endcap1
setcolor ( endcap_color1 ) {
translate ( path [ 0 ] ) {
mat = is_undef ( endcap_angle1 ) ? rot ( from = BACK , to = start_vec ) :
zrot ( endcap_angle1 ) ;
multmatrix ( mat ) polygon ( endcap_shape1 ) ;
2021-09-16 03:12:51 +00:00
}
}
2023-04-24 01:38:27 +00:00
// Endcap2
setcolor ( endcap_color2 ) {
translate ( last ( path ) ) {
mat = is_undef ( endcap_angle2 ) ? rot ( from = BACK , to = end_vec ) :
zrot ( endcap_angle2 ) ;
multmatrix ( mat ) polygon ( endcap_shape2 ) ;
}
2021-10-04 23:19:27 +00:00
}
2023-04-24 01:38:27 +00:00
}
} else { // Three dimensional case
rotmats = cumprod ( [
for ( i = idx ( path2 , e = - 2 ) ) let (
vec1 = i = = 0 ? UP : unit ( path2 [ i ] - path2 [ i - 1 ] , UP ) ,
vec2 = unit ( path2 [ i + 1 ] - path2 [ i ] , UP )
) rot ( from = vec1 , to = vec2 )
] ) ;
2021-09-16 03:12:51 +00:00
2023-04-24 01:38:27 +00:00
sides = [
for ( i = idx ( path2 , e = - 2 ) )
quantup ( segs ( max ( widths [ i ] , widths [ i + 1 ] ) / 2 ) , 4 )
] ;
2022-02-26 02:22:37 +00:00
2023-04-24 01:38:27 +00:00
// Straight segments
setcolor ( color ) {
for ( i = idx ( path2 , e = - 2 ) ) {
dist = norm ( path2 [ i + 1 ] - path2 [ i ] ) ;
w1 = widths [ i ] / 2 ;
w2 = widths [ i + 1 ] / 2 ;
$fn = sides [ i ] ;
translate ( path2 [ i ] ) {
multmatrix ( rotmats [ i ] ) {
cylinder ( r1 = w1 , r2 = w2 , h = dist , center = false ) ;
}
}
}
}
2021-09-16 03:12:51 +00:00
2023-04-24 01:38:27 +00:00
// Joints
setcolor ( joint_color ) {
for ( i = [ 1 : 1 : len ( path2 ) - 2 ] ) {
$fn = sides [ i ] ;
translate ( path2 [ i ] ) {
if ( joints ! = undef && joints ! = "round" ) {
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 {
corner = select ( path2 , i - 1 , i + 1 ) ;
axis = vector_axis ( corner ) ;
ang = vector_angle ( corner ) ;
if ( ! approx ( ang , 0 ) ) {
frame_map ( x = path2 [ i - 1 ] - path2 [ i ] , z = - axis ) {
zrot ( 90 - 0.5 ) {
rotate_extrude ( angle = 180 - ang + 1 ) {
arc ( d = widths [ i ] , start = - 90 , angle = 180 ) ;
}
}
}
}
}
}
}
}
if ( ! closed ) {
// Endcap1
setcolor ( endcap_color1 ) {
translate ( path [ 0 ] ) {
multmatrix ( rotmats [ 0 ] * xrot ( 180 ) ) {
$fn = sides [ 0 ] ;
if ( is_undef ( endcap_angle1 ) ) {
rotate_extrude ( convexity = convexity ) {
right_half ( planar = true ) {
polygon ( endcap_shape1 ) ;
2021-10-02 07:24:33 +00:00
}
2021-09-16 03:12:51 +00:00
}
2023-04-24 01:38:27 +00:00
} else {
rotate ( [ 90 , 0 , endcap_angle1 ] ) {
linear_extrude ( height = max ( widths [ 0 ] , 0.001 ) , center = true , convexity = convexity ) {
polygon ( endcap_shape1 ) ;
2022-08-21 09:49:47 +00:00
}
}
2021-10-02 07:24:33 +00:00
}
}
2021-09-16 03:12:51 +00:00
}
}
2023-04-24 01:38:27 +00:00
// Endcap2
setcolor ( endcap_color2 ) {
translate ( last ( path ) ) {
multmatrix ( last ( rotmats ) ) {
$fn = last ( sides ) ;
if ( is_undef ( endcap_angle2 ) ) {
rotate_extrude ( convexity = convexity ) {
right_half ( planar = true ) {
polygon ( endcap_shape2 ) ;
}
2021-10-04 23:19:27 +00:00
}
2023-04-24 01:38:27 +00:00
} else {
rotate ( [ 90 , 0 , endcap_angle2 ] ) {
linear_extrude ( height = max ( last ( widths ) , 0.001 ) , center = true , convexity = convexity ) {
polygon ( endcap_shape2 ) ;
}
2021-10-04 23:19:27 +00:00
}
2021-10-02 07:24:33 +00:00
}
2021-09-16 03:12:51 +00:00
}
}
}
2023-04-24 01:38:27 +00:00
}
}
}
}
union ( ) ;
2021-09-16 03:12:51 +00:00
}
}
// Function&Module: dashed_stroke()
2023-03-27 23:30:04 +00:00
// Synopsis: Draws a dashed line along a path or region boundry.
2023-05-27 08:42:09 +00:00
// SynTags: Geom, PathList
2023-03-27 23:30:04 +00:00
// Topics: Paths, Drawing Tools
// See Also: stroke(), path_cut()
2021-09-16 03:12:51 +00:00
// Usage: As a Module
2022-03-31 22:12:23 +00:00
// dashed_stroke(path, dashpat, [width=], [closed=]);
2021-09-16 03:12:51 +00:00
// Usage: As a Function
2022-03-31 22:12:23 +00:00
// dashes = dashed_stroke(path, dashpat, [closed=]);
2021-09-16 03:12:51 +00:00
// Description:
2021-10-01 03:11:01 +00:00
// Given a path (or region) and a dash pattern, creates a dashed line that follows that
// path or region boundary with the given dash pattern.
2021-09-16 03:12:51 +00:00
// - When called as a function, returns a list of dash sub-paths.
// - When called as a module, draws all those subpaths using `stroke()`.
2023-11-05 15:05:54 +00:00
// .
2021-10-01 03:11:01 +00:00
// When called as a module the dash pattern is multiplied by the line width. When called as
// a function the dash pattern applies as you specify it.
2021-09-16 03:12:51 +00:00
// Arguments:
2021-10-01 03:11:01 +00:00
// path = The path or region to subdivide into dashes.
2021-09-16 03:12:51 +00:00
// 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
2022-07-29 01:40:21 +00:00
// fit = If true, shrink or stretch the dash pattern so that the path ends ofter a logical dash. Default: true
// roundcaps = (Module only) If true, draws dashes with rounded caps. This often looks better. Default: true
// mindash = (Function only) Specifies the minimal dash length to return at the end of a path when fit is false. Default: 0.5
2021-09-16 03:12:51 +00:00
// 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);
2022-07-29 01:40:21 +00:00
// dashpat = [10,2, 3,2, 3,2];
2021-09-16 03:12:51 +00:00
// 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);
2022-07-29 01:40:21 +00:00
function dashed_stroke ( path , dashpat = [ 3 , 3 ] , closed = false , fit = true , mindash = 0.5 ) =
is_region ( path ) ? [
for ( p = path )
each dashed_stroke ( p , dashpat , closed = true , fit = fit )
] :
2021-09-16 03:12:51 +00:00
let (
2023-03-03 00:40:12 +00:00
path = closed ? list_wrap ( path ) : path ,
2021-09-16 03:12:51 +00:00
dashpat = len ( dashpat ) % 2 = = 0 ? dashpat : concat ( dashpat , [ 0 ] ) ,
plen = path_length ( path ) ,
dlen = sum ( dashpat ) ,
doff = cumsum ( dashpat ) ,
2022-07-29 01:40:21 +00:00
freps = plen / dlen ,
reps = max ( 1 , fit ? round ( freps ) : floor ( freps ) ) ,
tlen = ! fit ? plen :
reps * dlen + ( closed ? 0 : dashpat [ 0 ] ) ,
sc = plen / tlen ,
2021-09-16 03:12:51 +00:00
cuts = [
2022-07-29 01:40:21 +00:00
for ( i = [ 0 : 1 : reps ] , off = doff * sc )
2024-01-26 23:09:39 +00:00
let ( x = i * dlen * sc + off )
if ( x > 0 && x < plen - EPSILON ) x
2021-09-16 03:12:51 +00:00
] ,
dashes = path_cut ( path , cuts , closed = false ) ,
2022-07-29 01:40:21 +00:00
dcnt = len ( dashes ) ,
evens = [
for ( i = idx ( dashes ) )
if ( i % 2 = = 0 )
let ( dash = dashes [ i ] )
if ( i < dcnt - 1 || path_length ( dash ) > mindash )
dashes [ i ]
]
2021-09-16 03:12:51 +00:00
) evens ;
2022-07-29 01:40:21 +00:00
module dashed_stroke ( path , dashpat = [ 3 , 3 ] , width = 1 , closed = false , fit = true , roundcaps = false ) {
2021-11-04 02:30:01 +00:00
no_children ( $children ) ;
2022-07-29 01:40:21 +00:00
segs = dashed_stroke ( path , dashpat = dashpat * width , closed = closed , fit = fit , mindash = 0.5 * width ) ;
2021-09-16 03:12:51 +00:00
for ( seg = segs )
2022-07-29 01:40:21 +00:00
stroke ( seg , width = width , endcaps = roundcaps ? "round" : false ) ;
2021-09-16 03:12:51 +00:00
}
2021-10-01 03:11:01 +00:00
2021-09-16 03:12:51 +00:00
// Section: Computing paths
// Function&Module: arc()
2023-05-24 08:50:29 +00:00
// Synopsis: Draws a 2D pie-slice or returns 2D or 3D path forming an arc.
2023-05-27 08:42:09 +00:00
// SynTags: Geom, Path
2023-05-24 08:50:29 +00:00
// Topics: Paths (2D), Paths (3D), Shapes (2D), Path Generators
2024-01-26 23:09:39 +00:00
// See Also: pie_slice(), stroke(), ring()
2023-03-27 23:30:04 +00:00
//
2021-09-16 03:12:51 +00:00
// Usage: 2D arc from 0º to `angle` degrees.
2022-03-31 22:12:23 +00:00
// path=arc(n, r|d=, angle);
2021-09-16 03:12:51 +00:00
// Usage: 2D arc from START to END degrees.
2022-03-31 22:12:23 +00:00
// path=arc(n, r|d=, angle=[START,END]);
2021-09-16 03:12:51 +00:00
// Usage: 2D arc from `start` to `start+angle` degrees.
2022-03-31 22:12:23 +00:00
// path=arc(n, r|d=, start=, angle=);
2021-09-16 03:12:51 +00:00
// Usage: 2D circle segment by `width` and `thickness`, starting and ending on the X axis.
2022-03-31 22:12:23 +00:00
// path=arc(n, width=, thickness=);
2021-09-16 03:12:51 +00:00
// Usage: Shortest 2D or 3D arc around centerpoint `cp`, starting at P0 and ending on the vector pointing from `cp` to `P1`.
2022-03-31 22:12:23 +00:00
// path=arc(n, cp=, points=[P0,P1], [long=], [cw=], [ccw=]);
2021-09-16 03:12:51 +00:00
// Usage: 2D or 3D arc, starting at `P0`, passing through `P1` and ending at `P2`.
2022-03-31 22:12:23 +00:00
// path=arc(n, points=[P0,P1,P2]);
2022-04-06 07:21:57 +00:00
// Usage: 2D or 3D arc, fron tangent point on segment `[P0,P1]` to the tangent point on segment `[P1,P2]`.
// path=arc(n, corner=[P0,P1,P2], r=);
2024-01-26 23:09:39 +00:00
// Usage: Create a wedge using any other arc parameters
// path=arc(wedge=true,...)
2022-03-31 22:12:23 +00:00
// Usage: as module
// arc(...) [ATTACHMENTS];
2021-09-16 03:12:51 +00:00
// Description:
2024-01-26 23:09:39 +00:00
// If called as a function, returns a 2D or 3D path forming an arc. If `wedge` is true, the centerpoint of the arc appears as the first point in the result.
2021-09-16 03:12:51 +00:00
// If called as a module, creates a 2D arc polygon or pie slice shape.
// Arguments:
2022-03-14 01:40:55 +00:00
// n = Number of vertices to form the arc curve from.
2021-09-16 03:12:51 +00:00
// r = Radius of the arc.
// angle = If a scalar, specifies the end angle in degrees (relative to start parameter). If a vector of two scalars, specifies start and end angles.
// ---
// d = Diameter of the arc.
// cp = Centerpoint of arc.
// points = Points on the arc.
2022-04-06 07:21:57 +00:00
// corner = A path of two segments to fit an arc tangent to.
2021-09-16 03:12:51 +00:00
// long = if given with cp and points takes the long arc instead of the default short arc. Default: false
// cw = if given with cp and 2 points takes the arc in the clockwise direction. Default: false
// ccw = if given with cp and 2 points takes the arc in the counter-clockwise direction. Default: false
// width = If given with `thickness`, arc starts and ends on X axis, to make a circle segment.
// thickness = If given with `width`, arc starts and ends on X axis, to make a circle segment.
2024-01-26 23:09:39 +00:00
// start = Start angle of arc. Default: 0
2023-03-27 23:30:04 +00:00
// wedge = If true, include centerpoint `cp` in output to form pie slice shape. Default: false
2021-09-16 03:12:51 +00:00
// endpoint = If false exclude the last point (function only). Default: true
2021-11-20 03:33:16 +00:00
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). (Module only) Default: `CENTER`
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). (Module only) Default: `0`
2021-09-16 03:12:51 +00:00
// Examples(2D):
2022-03-14 01:40:55 +00:00
// arc(n=4, r=30, angle=30, wedge=true);
2021-09-16 03:12:51 +00:00
// arc(r=30, angle=30, wedge=true);
// arc(d=60, angle=30, wedge=true);
// arc(d=60, angle=120);
// arc(d=60, angle=120, wedge=true);
// arc(r=30, angle=[75,135], wedge=true);
// arc(r=30, start=45, angle=75, wedge=true);
// arc(width=60, thickness=20);
// arc(cp=[-10,5], points=[[20,10],[0,35]], wedge=true);
// arc(points=[[30,-5],[20,10],[-10,20]], wedge=true);
2022-04-06 07:21:57 +00:00
// Example(2D): Fit to three points.
2021-09-16 03:12:51 +00:00
// arc(points=[[5,30],[-10,-10],[30,5]], wedge=true);
// Example(2D):
// path = arc(points=[[5,30],[-10,-10],[30,5]], wedge=true);
// stroke(closed=true, path);
// Example(FlatSpin,VPD=175):
// path = arc(points=[[0,30,0],[0,0,30],[30,0,0]]);
2021-10-07 01:16:39 +00:00
// stroke(path, dots=true, dots_color="blue");
2022-04-06 07:21:57 +00:00
// Example(2D): Fit to a corner.
// pts = [[0,40], [-40,-10], [30,0]];
// path = arc(corner=pts, r=20);
// stroke(pts, endcaps="arrow2");
// stroke(path, endcap2="arrow2", color="blue");
function arc ( n , r , angle , d , cp , points , corner , width , thickness , start , wedge = false , long = false , cw = false , ccw = false , endpoint = true ) =
2021-09-16 03:12:51 +00:00
assert ( is_bool ( endpoint ) )
2022-04-06 07:21:57 +00:00
! endpoint ?
assert ( ! wedge , "endpoint cannot be false if wedge is true" )
2024-01-26 23:09:39 +00:00
list_head ( arc ( u_add ( n , 1 ) , r , angle , d , cp , points , corner , width , thickness , start , wedge , long , cw , ccw , true ) )
:
assert ( is_undef ( start ) || is_def ( angle ) , "start requires angle" )
assert ( is_undef ( angle ) || ! any_defined ( [ thickness , width , points , corner ] ) , "Cannot give angle with points, corner, width or thickness" )
2022-03-14 01:40:55 +00:00
assert ( is_undef ( n ) || ( is_integer ( n ) && n >= 2 ) , "Number of points must be an integer 2 or larger" )
2024-01-26 23:09:39 +00:00
assert ( is_undef ( points ) || is_path ( points , [ 2 , 3 ] ) , "Points must be a list of 2d or 3d points" )
assert ( ( is_def ( points ) && len ( points ) = = 2 ) || ! any ( [ cw , ccw , long ] ) , "cw, ccw, and long are only allowed when points is a list of length 2" )
2021-09-16 03:12:51 +00:00
// First try for 2D arc specified by width and thickness
2024-01-26 23:09:39 +00:00
is_def ( width ) && is_def ( thickness ) ?
assert ( ! any_defined ( [ r , cp , points , angle , start ] ) , "Conflicting or invalid parameters to arc" )
2022-04-06 07:21:57 +00:00
assert ( width > 0 , "Width must be postive" )
assert ( thickness > 0 , "Thickness must be positive" )
2022-03-14 01:40:55 +00:00
arc ( n , points = [ [ width / 2 , 0 ] , [ 0 , thickness ] , [ - width / 2 , 0 ] ] , wedge = wedge )
2024-01-26 23:09:39 +00:00
: is_def ( angle ) ?
2021-09-16 03:12:51 +00:00
let (
parmok = ! any_defined ( [ points , width , thickness ] ) &&
2022-11-10 03:22:30 +00:00
( ( is_vector ( angle , 2 ) && is_undef ( start ) ) || is_finite ( angle ) )
2021-09-16 03:12:51 +00:00
)
assert ( parmok , "Invalid parameters in arc" )
let (
cp = first_defined ( [ cp , [ 0 , 0 ] ] ) ,
start = is_def ( start ) ? start : is_vector ( angle ) ? angle [ 0 ] : 0 ,
angle = is_vector ( angle ) ? angle [ 1 ] - angle [ 0 ] : angle ,
r = get_radius ( r = r , d = d )
)
assert ( is_vector ( cp , 2 ) , "Centerpoint must be a 2d vector" )
assert ( angle ! = 0 , "Arc has zero length" )
assert ( is_def ( r ) && r > 0 , "Arc radius invalid" )
let (
2022-03-14 01:40:55 +00:00
n = is_def ( n ) ? n : max ( 3 , ceil ( segs ( r ) * abs ( angle ) / 360 ) ) ,
2024-05-23 00:26:52 +00:00
arcpoints = [ for ( i = [ 0 : n - 1 ] ) let ( theta = start + i * angle / ( n - 1 ) ) r * [ cos ( theta ) , sin ( theta ) ] + cp ]
2021-09-16 03:12:51 +00:00
)
2024-05-23 00:26:52 +00:00
[
if ( wedge ) cp ,
each arcpoints
]
2024-01-26 23:09:39 +00:00
: is_def ( corner ) ?
assert ( is_path ( corner , [ 2 , 3 ] ) && len ( corner ) = = 3 , str ( "Point list is invalid" ) )
assert ( is_undef ( cp ) && ! any ( [ long , cw , ccw ] ) , "Cannot use cp, long, cw, or ccw with corner" )
2022-04-06 07:21:57 +00:00
// Arc is 3D, so transform corner to 2D and make a recursive call, then remap back to 3D
len ( corner [ 0 ] ) = = 3 ? (
let (
2024-01-26 23:09:39 +00:00
plane = [ corner [ 2 ] , corner [ 0 ] , corner [ 1 ] ] ,
2022-04-06 07:21:57 +00:00
points2d = project_plane ( plane , corner )
)
2024-09-28 12:44:14 +00:00
lift_plane ( plane , arc ( n , corner = points2d , wedge = wedge , r = r , d = d ) )
2022-04-06 07:21:57 +00:00
) :
assert ( is_path ( corner ) && len ( corner ) = = 3 )
let ( col = is_collinear ( corner [ 0 ] , corner [ 1 ] , corner [ 2 ] ) )
assert ( ! col , "Collinear inputs do not define an arc" )
let ( r = get_radius ( r = r , d = d ) )
assert ( is_finite ( r ) && r > 0 , "Must specify r= or d= when corner= is given." )
let (
2022-04-07 02:37:00 +00:00
ci = circle_2tangents ( r , corner [ 0 ] , corner [ 1 ] , corner [ 2 ] , tangents = true ) ,
2022-04-06 07:21:57 +00:00
cp = ci [ 0 ] , nrm = ci [ 1 ] , tp1 = ci [ 2 ] , tp2 = ci [ 3 ] ,
dir = det2 ( [ corner [ 1 ] - corner [ 0 ] , corner [ 2 ] - corner [ 1 ] ] ) > 0 ,
corner = dir ? [ tp1 , tp2 ] : [ tp2 , tp1 ] ,
theta_start = atan2 ( corner [ 0 ] . y - cp . y , corner [ 0 ] . x - cp . x ) ,
theta_end = atan2 ( corner [ 1 ] . y - cp . y , corner [ 1 ] . x - cp . x ) ,
angle = posmod ( theta_end - theta_start , 360 ) ,
arcpts = arc ( n , cp = cp , r = r , start = theta_start , angle = angle , wedge = wedge )
)
2024-01-26 23:09:39 +00:00
dir ? arcpts : wedge ? reverse_polygon ( arcpts ) : reverse ( arcpts )
: assert ( is_def ( points ) , "Arc not specified: must give points, angle, or width and thickness" )
2022-04-06 07:21:57 +00:00
assert ( is_path ( points , [ 2 , 3 ] ) , "Point list is invalid" )
2024-01-26 23:09:39 +00:00
// If arc is 3D, transform points to 2D and make a recursive call, then remap back to 3D
len ( points [ 0 ] ) = = 3 ?
2022-04-06 07:21:57 +00:00
assert ( ! ( cw || ccw ) , "(Counter)clockwise isn't meaningful in 3d, so `cw` and `ccw` must be false" )
assert ( is_undef ( cp ) || is_vector ( cp , 3 ) , "points are 3d so cp must be 3d" )
2021-09-16 03:12:51 +00:00
let (
plane = [ is_def ( cp ) ? cp : points [ 2 ] , points [ 0 ] , points [ 1 ] ] ,
center2d = is_def ( cp ) ? project_plane ( plane , cp ) : undef ,
points2d = project_plane ( plane , points )
)
2022-03-14 01:40:55 +00:00
lift_plane ( plane , arc ( n , cp = center2d , points = points2d , wedge = wedge , long = long ) )
2024-01-26 23:09:39 +00:00
: len ( points ) = = 2 ?
2021-09-16 03:12:51 +00:00
// Arc defined by center plus two points, will have radius defined by center and points[0]
// and extent defined by direction of point[1] from the center
2024-01-26 23:09:39 +00:00
assert ( is_vector ( cp , 2 ) , "Centerpoint is required when points has length 2 and it must be a 2d vector" )
2022-04-06 07:21:57 +00:00
assert ( len ( points ) = = 2 , "When pointlist has length 3 centerpoint is not allowed" )
assert ( points [ 0 ] ! = points [ 1 ] , "Arc endpoints are equal" )
assert ( cp ! = points [ 0 ] && cp ! = points [ 1 ] , "Centerpoint equals an arc endpoint" )
2022-05-27 14:21:33 +00:00
assert ( num_true ( [ long , cw , ccw ] ) < = 1 , str ( "Only one of `long`, `cw` and `ccw` can be true" , cw , ccw , long ) )
2021-09-16 03:12:51 +00:00
let (
angle = vector_angle ( points [ 0 ] , cp , points [ 1 ] ) ,
v1 = points [ 0 ] - cp ,
v2 = points [ 1 ] - cp ,
2022-04-06 07:21:57 +00:00
prelim_dir = sign ( det2 ( [ v1 , v2 ] ) ) , // z component of cross product
dir = prelim_dir ! = 0 ? prelim_dir :
assert ( cw || ccw , "Collinear inputs don't define a unique arc" )
1 ,
r = norm ( v1 ) ,
final_angle = long || ( ccw && dir < 0 ) || ( cw && dir > 0 ) ?
- dir * ( 360 - angle ) :
dir * angle ,
sa = atan2 ( v1 . y , v1 . x )
2021-09-16 03:12:51 +00:00
)
2022-04-06 07:21:57 +00:00
arc ( n , cp = cp , r = r , start = sa , angle = final_angle , wedge = wedge )
2024-01-26 23:09:39 +00:00
: // Final case is arc passing through three points, starting at point[0] and ending at point[3]
2021-09-16 03:12:51 +00:00
let ( col = is_collinear ( points [ 0 ] , points [ 1 ] , points [ 2 ] ) )
assert ( ! col , "Collinear inputs do not define an arc" )
let (
cp = line_intersection ( _normal_segment ( points [ 0 ] , points [ 1 ] ) , _normal_segment ( points [ 1 ] , points [ 2 ] ) ) ,
// select order to be counterclockwise
dir = det2 ( [ points [ 1 ] - points [ 0 ] , points [ 2 ] - points [ 1 ] ] ) > 0 ,
points = dir ? select ( points , [ 0 , 2 ] ) : select ( points , [ 2 , 0 ] ) ,
r = norm ( points [ 0 ] - cp ) ,
theta_start = atan2 ( points [ 0 ] . y - cp . y , points [ 0 ] . x - cp . x ) ,
theta_end = atan2 ( points [ 1 ] . y - cp . y , points [ 1 ] . x - cp . x ) ,
angle = posmod ( theta_end - theta_start , 360 ) ,
2024-05-23 00:26:52 +00:00
// Specify endpoints exactly; skip those endpoints when producing arc points
2024-05-23 01:51:01 +00:00
// Generating the whole arc and clipping ends is the easiest way to ensure that we
// generate the proper number of points.
2024-05-23 00:26:52 +00:00
arcpts = [ if ( wedge ) cp ,
points [ 0 ] ,
2024-05-23 01:51:01 +00:00
each select ( arc ( n , cp = cp , r = r , start = theta_start , angle = angle ) , 1 , - 2 ) ,
2024-05-23 00:26:52 +00:00
points [ 1 ]
]
2021-09-16 03:12:51 +00:00
)
2024-05-23 00:26:52 +00:00
dir ? arcpts
2024-05-23 01:51:01 +00:00
: wedge ? reverse_polygon ( arcpts ) // Keep the centerpoint at position 0 in the list
2024-05-23 00:26:52 +00:00
: reverse ( arcpts ) ;
2021-09-16 03:12:51 +00:00
2022-04-06 07:21:57 +00:00
module arc ( n , r , angle , d , cp , points , corner , width , thickness , start , wedge = false , anchor = CENTER , spin = 0 )
2021-09-16 03:12:51 +00:00
{
2022-04-06 07:21:57 +00:00
path = arc ( n = n , r = r , angle = angle , d = d , cp = cp , points = points , corner = corner , width = width , thickness = thickness , start = start , wedge = wedge ) ;
2021-11-17 01:46:37 +00:00
attachable ( anchor , spin , two_d = true , path = path , extent = false ) {
2021-11-10 03:27:55 +00:00
polygon ( path ) ;
children ( ) ;
}
2021-09-16 03:12:51 +00:00
}
2023-11-03 02:39:22 +00:00
// Function: catenary()
2023-11-03 02:05:45 +00:00
// Synopsis: Returns a 2D Catenary chain or arch path.
// SynTags: Path
// Topics: Paths
// See Also: circle(), stroke()
// Usage:
2023-11-03 02:39:22 +00:00
// path = catenary(width, droop=|angle=, n=);
2023-11-03 02:05:45 +00:00
// Description:
// Returns a 2D Catenary path, which is the path a chain held at both ends will take.
// The path will have the endpoints at `[±width/2, 0]`, and the middle of the path will droop
// towards Y- if the given droop= or angle= is positive. It will droop towards Y+ if the
// droop= or angle= is negative. You *must* specify one of droop= or angle=.
// Arguments:
// width = The straight-line distance between the endpoints of the path.
// droop = If given, specifies the height difference between the endpoints and the hanging middle of the path. If given a negative value, returns an arch *above* the Y axis.
// n = The number of points to return in the path. Default: 100
// ---
// angle = If given, specifies the angle that the path will droop by at the endpoints. If given a negative value, returns an arch *above* the Y axis.
2023-11-05 05:08:27 +00:00
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). (Module only) Default: `CENTER`
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). (Module only) Default: `0`
2023-11-03 02:05:45 +00:00
// Example(2D): By Droop
2023-11-03 02:39:22 +00:00
// stroke(catenary(100, droop=30));
2023-11-03 02:05:45 +00:00
// Example(2D): By Angle
2023-11-03 02:39:22 +00:00
// stroke(catenary(100, angle=30));
2023-11-03 02:05:45 +00:00
// Example(2D): Upwards Arch by Angle
2023-11-03 02:39:22 +00:00
// stroke(catenary(100, angle=30));
2023-11-03 02:05:45 +00:00
// Example(2D): Upwards Arch by Height Delta
2023-11-03 02:39:22 +00:00
// stroke(catenary(100, droop=-30));
2023-11-03 02:05:45 +00:00
// Example(2D): Specifying Vertex Count
2023-11-03 02:39:22 +00:00
// stroke(catenary(100, angle=-85, n=11), dots="dot");
2024-04-22 21:37:55 +00:00
// Example(3D): Sweeping a Catenary Path
2023-11-03 02:39:22 +00:00
// path = xrot(90, p=path3d(catenary(100, droop=20, n=41)));
2023-11-03 02:05:45 +00:00
// path_sweep(circle(r=1.5, $fn=24), path);
2023-11-03 02:39:22 +00:00
function catenary ( width , droop , n = 100 , angle ) =
2023-11-03 02:05:45 +00:00
assert ( one_defined ( [ droop , angle ] , "droop,angle" ) )
let (
sgn = is_undef ( droop ) ? sign ( angle ) : sign ( droop ) ,
droop = droop = = undef ? undef : abs ( droop ) ,
angle = angle = = undef ? undef : abs ( angle )
)
assert ( is_finite ( width ) && width > 0 , "Bad width= value." )
2023-11-05 05:08:27 +00:00
assert ( is_integer ( n ) && n > 0 , "Bad n= value. Must be a positive integer." )
2023-11-03 02:05:45 +00:00
assert ( is_undef ( droop ) || is_finite ( droop ) , "Bad droop= value." )
assert ( is_undef ( angle ) || ( is_finite ( angle ) && angle ! = 0 && abs ( angle ) < 90 ) , "Bad angle= value." )
let (
2023-11-05 05:08:27 +00:00
catlup_fn = is_undef ( droop )
? function ( x ) let (
p1 = [ x - 0.001 , cosh ( x - 0.001 ) - 1 ] ,
p2 = [ x + 0.001 , cosh ( x + 0.001 ) - 1 ] ,
delta = p2 - p1 ,
ang = atan2 ( delta . y , delta . x )
) ang
: function ( x ) ( cosh ( x ) - 1 ) / x ,
binsearch_fn = function ( targ , x = 0 , inc = 4 )
inc < 1e-9 ? lookup ( targ , [ [ catlup_fn ( x ) , x ] , [ catlup_fn ( x + inc ) , x + inc ] ] ) :
catlup_fn ( x + inc ) > targ ? binsearch_fn ( targ , x , inc / 2 ) :
binsearch_fn ( targ , x + inc , inc ) ,
scx = is_undef ( droop ) ? binsearch_fn ( angle ) :
binsearch_fn ( droop / ( width / 2 ) ) ,
sc = width / 2 / scx ,
droop = ! is_undef ( droop ) ? droop : ( cosh ( scx ) - 1 ) * sc ,
2023-11-03 02:05:45 +00:00
path = [
for ( x = lerpn ( - scx , scx , n ) )
2023-11-05 05:08:27 +00:00
let (
xval = x * sc ,
yval = approx ( abs ( x ) , scx ) ? 0 :
( cosh ( x ) - 1 ) * sc - droop
)
[ xval , yval ]
2023-11-03 02:05:45 +00:00
] ,
out = sgn > 0 ? path : yflip ( p = path )
) out ;
2023-11-05 05:08:27 +00:00
module catenary ( width , droop , n = 100 , angle , anchor = CTR , spin = 0 ) {
path = catenary ( width = width , droop = droop , n = n , angle = angle ) ;
attachable ( anchor , spin , two_d = true , path = path , extent = true ) {
polygon ( path ) ;
children ( ) ;
}
}
2021-09-16 03:12:51 +00:00
// Function: helix()
2023-03-27 23:30:04 +00:00
// Synopsis: Creates a 2d spiral or 3d helical path.
2023-05-27 08:42:09 +00:00
// SynTags: Path
2023-03-27 23:30:04 +00:00
// Topics: Path Generators, Paths, Drawing Tools
2023-05-27 08:42:09 +00:00
// See Also: pie_slice(), stroke(), thread_helix(), path_sweep()
2023-03-27 23:30:04 +00:00
//
2021-09-16 03:12:51 +00:00
// Usage:
2022-03-31 22:12:23 +00:00
// path = helix(l|h, [turns=], [angle=], r=|r1=|r2=, d=|d1=|d2=);
2021-10-02 01:27:52 +00:00
// Description:
// Returns a 3D helical path on a cone, including the degerate case of flat spirals.
// You can specify start and end radii. You can give the length, the helix angle, or the number of turns: two
// of these three parameters define the helix. For a flat helix you must give length 0 and a turn count.
// Helix will be right handed if turns is positive and left handed if it is negative.
// The angle is calculateld based on the radius at the base of the helix.
2021-09-16 03:12:51 +00:00
// Arguments:
2022-03-08 00:43:12 +00:00
// h/l = Height/length of helix, zero for a flat spiral
2021-10-02 01:27:52 +00:00
// ---
// turns = Number of turns in helix, positive for right handed
// angle = helix angle
// r = Radius of helix
// r1 = Radius of bottom of helix
// r2 = Radius of top of helix
// d = Diameter of helix
// d1 = Diameter of bottom of helix
// d2 = Diameter of top of helix
2021-09-16 03:12:51 +00:00
// Example(3D):
2021-10-07 01:16:39 +00:00
// stroke(helix(turns=2.5, h=100, r=50), dots=true, dots_color="blue");
2021-10-02 01:27:52 +00:00
// Example(3D): Helix that turns the other way
2021-10-07 01:16:39 +00:00
// stroke(helix(turns=-2.5, h=100, r=50), dots=true, dots_color="blue");
2021-10-02 01:27:52 +00:00
// Example(3D): Flat helix (note points are still 3d)
// stroke(helix(h=0,r1=50,r2=25,l=0, turns=4));
2021-11-04 02:30:01 +00:00
module helix ( l , h , turns , angle , r , r1 , r2 , d , d1 , d2 ) { no_module ( ) ; }
2021-10-02 01:27:52 +00:00
function helix ( l , h , turns , angle , r , r1 , r2 , d , d1 , d2 ) =
let (
r1 = get_radius ( r = r , r1 = r1 , d = d , d1 = d1 , dflt = 1 ) ,
r2 = get_radius ( r = r , r1 = r2 , d = d , d1 = d2 , dflt = 1 ) ,
length = first_defined ( [ l , h ] )
)
assert ( num_defined ( [ length , turns , angle ] ) = = 2 , "Must define exactly two of l/h, turns, and angle" )
assert ( is_undef ( angle ) || length ! = 0 , "Cannot give length 0 with an angle" )
let (
// length advances dz for each turn
dz = is_def ( angle ) && length ! = 0 ? 2 * PI * r1 * tan ( angle ) : length / abs ( turns ) ,
maxtheta = is_def ( turns ) ? 360 * turns : 360 * length / dz ,
N = segs ( max ( r1 , r2 ) )
)
[ for ( theta = lerpn ( 0 , maxtheta , max ( 3 , ceil ( abs ( maxtheta ) * N / 360 ) ) ) )
let ( R = lerp ( r1 , r2 , theta / maxtheta ) )
[ R * cos ( theta ) , R * sin ( theta ) , abs ( theta ) / 360 * dz ] ] ;
2021-09-16 03:12:51 +00:00
function _normal_segment ( p1 , p2 ) =
let ( center = ( p1 + p2 ) / 2 )
[ center , center + norm ( p1 - p2 ) / 2 * line_normal ( p1 , p2 ) ] ;
// Function: turtle()
2023-03-27 23:30:04 +00:00
// Synopsis: Uses [turtle graphics](https://en.wikipedia.org/wiki/Turtle_graphics) to generate a 2D path.
2023-05-27 08:42:09 +00:00
// SynTags: Path
2021-09-16 03:12:51 +00:00
// Topics: Shapes (2D), Path Generators (2D), Mini-Language
2023-05-27 08:42:09 +00:00
// See Also: turtle3d(), stroke(), path_sweep()
2023-03-27 23:30:04 +00:00
// Usage:
2023-05-27 08:42:09 +00:00
// path = turtle(commands, [state], [full_state=], [repeat=])
2021-09-16 03:12:51 +00:00
// Description:
2024-08-10 01:52:51 +00:00
// Use a sequence of [turtle graphics](https://en.wikipedia.org/wiki/Turtle_graphics) commands to generate a path. The parameter `commands` is a list of
2021-09-16 03:12:51 +00:00
// turtle commands and optional parameters for each command. The turtle state has a position, movement direction,
// movement distance, and default turn angle. If you do not give `state` as input then the turtle starts at the
// origin, pointed along the positive x axis with a movement distance of 1. By default, `turtle` returns just
// the computed turtle path. If you set `full_state` to true then it instead returns the full turtle state.
// You can invoke `turtle` again with this full state to continue the turtle path where you left off.
// .
// The turtle state is a list with three entries: the path constructed so far, the current step as a 2-vector, the current default angle,
// and the current arcsteps setting.
// .
// Commands | Arguments | What it does
// ------------ | ------------------ | -------------------------------
// "move" | [dist] | Move turtle scale*dist units in the turtle direction. Default dist=1.
// "xmove" | [dist] | Move turtle scale*dist units in the x direction. Default dist=1. Does not change turtle direction.
// "ymove" | [dist] | Move turtle scale*dist units in the y direction. Default dist=1. Does not change turtle direction.
// "xymove" | vector | Move turtle by the specified vector. Does not change turtle direction.
// "untilx" | xtarget | Move turtle in turtle direction until x==xtarget. Produces an error if xtarget is not reachable.
2022-05-04 04:31:55 +00:00
// "untily" | ytarget | Move turtle in turtle direction until y==ytarget. Produces an error if ytarget is not reachable.
2021-09-16 03:12:51 +00:00
// "jump" | point | Move the turtle to the specified point
// "xjump" | x | Move the turtle's x position to the specified value
// "yjump | y | Move the turtle's y position to the specified value
// "turn" | [angle] | Turn turtle direction by specified angle, or the turtle's default turn angle. The default angle starts at 90.
// "left" | [angle] | Same as "turn"
// "right" | [angle] | Same as "turn", -angle
// "angle" | angle | Set the default turn angle.
// "setdir" | dir | Set turtle direction. The parameter `dir` can be an angle or a vector.
// "length" | length | Change the turtle move distance to `length`
// "scale" | factor | Multiply turtle move distance by `factor`
// "addlength" | length | Add `length` to the turtle move distance
// "repeat" | count, commands | Repeats a list of commands `count` times.
// "arcleft" | radius, [angle] | Draw an arc from the current position toward the left at the specified radius and angle. The turtle turns by `angle`. A negative angle draws the arc to the right instead of the left, and leaves the turtle facing right. A negative radius draws the arc to the right but leaves the turtle facing left.
// "arcright" | radius, [angle] | Draw an arc from the current position toward the right at the specified radius and angle
// "arcleftto" | radius, angle | Draw an arc at the given radius turning toward the left until reaching the specified absolute angle.
// "arcrightto" | radius, angle | Draw an arc at the given radius turning toward the right until reaching the specified absolute angle.
// "arcsteps" | count | Specifies the number of segments to use for drawing arcs. If you set it to zero then the standard `$fn`, `$fa` and `$fs` variables define the number of segments.
//
// Arguments:
// commands = List of turtle commands
// state = Starting turtle state (from previous call) or starting point. Default: start at the origin, pointing right.
// ---
// full_state = If true return the full turtle state for continuing the path in subsequent turtle calls. Default: false
// repeat = Number of times to repeat the command list. Default: 1
//
// Example(2D): Simple rectangle
// path = turtle(["xmove",3, "ymove", "xmove",-3, "ymove",-1]);
// stroke(path,width=.1);
// Example(2D): Pentagon
// path=turtle(["angle",360/5,"move","turn","move","turn","move","turn","move"]);
// stroke(path,width=.1,closed=true);
// Example(2D): Pentagon using the repeat argument
// path=turtle(["move","turn",360/5],repeat=5);
// stroke(path,width=.1,closed=true);
// Example(2D): Pentagon using the repeat turtle command, setting the turn angle
// path=turtle(["angle",360/5,"repeat",5,["move","turn"]]);
// stroke(path,width=.1,closed=true);
// Example(2D): Pentagram
// path = turtle(["move","left",144], repeat=4);
// stroke(path,width=.05,closed=true);
// Example(2D): Sawtooth path
// path = turtle([
// "turn", 55,
// "untily", 2,
// "turn", -55-90,
// "untily", 0,
// "turn", 55+90,
// "untily", 2.5,
// "turn", -55-90,
// "untily", 0,
// "turn", 55+90,
// "untily", 3,
// "turn", -55-90,
// "untily", 0
// ]);
// stroke(path, width=.1);
// Example(2D): Simpler way to draw the sawtooth. The direction of the turtle is preserved when executing "yjump".
// path = turtle([
// "turn", 55,
// "untily", 2,
// "yjump", 0,
// "untily", 2.5,
// "yjump", 0,
// "untily", 3,
// "yjump", 0,
// ]);
// stroke(path, width=.1);
// Example(2DMed): square spiral
// path = turtle(["move","left","addlength",1],repeat=50);
// stroke(path,width=.2);
// Example(2DMed): pentagonal spiral
// path = turtle(["move","left",360/5,"addlength",1],repeat=50);
2021-10-08 01:31:58 +00:00
// stroke(path,width=.7);
2021-09-16 03:12:51 +00:00
// Example(2DMed): yet another spiral, without using `repeat`
// path = turtle(concat(["angle",71],flatten(repeat(["move","left","addlength",1],50))));
2021-10-08 01:31:58 +00:00
// stroke(path,width=.7);
2021-09-16 03:12:51 +00:00
// Example(2DMed): The previous spiral grows linearly and eventually intersects itself. This one grows geometrically and does not.
// path = turtle(["move","left",71,"scale",1.05],repeat=50);
2021-10-08 01:31:58 +00:00
// stroke(path,width=.15);
2021-09-16 03:12:51 +00:00
// Example(2D): Koch Snowflake
// function koch_unit(depth) =
// depth==0 ? ["move"] :
// concat(
// koch_unit(depth-1),
// ["right"],
// koch_unit(depth-1),
// ["left","left"],
// koch_unit(depth-1),
// ["right"],
// koch_unit(depth-1)
// );
// koch=concat(["angle",60,"repeat",3],[concat(koch_unit(3),["left","left"])]);
// polygon(turtle(koch));
module turtle ( commands , state = [ [ [ 0 , 0 ] ] , [ 1 , 0 ] , 90 , 0 ] , full_state = false , repeat = 1 ) { no_module ( ) ; }
function turtle ( commands , state = [ [ [ 0 , 0 ] ] , [ 1 , 0 ] , 90 , 0 ] , full_state = false , repeat = 1 ) =
let ( state = is_vector ( state ) ? [ [ state ] , [ 1 , 0 ] , 90 , 0 ] : state )
repeat = = 1 ?
_turtle ( commands , state , full_state ) :
_turtle_repeat ( commands , state , full_state , repeat ) ;
function _turtle_repeat ( commands , state , full_state , repeat ) =
repeat = = 1 ?
_turtle ( commands , state , full_state ) :
_turtle_repeat ( commands , _turtle ( commands , state , true ) , full_state , repeat - 1 ) ;
function _turtle_command_len ( commands , index ) =
let ( one_or_two_arg = [ "arcleft" , "arcright" , "arcleftto" , "arcrightto" ] )
commands [ index ] = = "repeat" ? 3 : // Repeat command requires 2 args
// For these, the first arg is required, second arg is present if it is not a string
in_list ( commands [ index ] , one_or_two_arg ) && len ( commands ) > index + 2 && ! is_string ( commands [ index + 2 ] ) ? 3 :
is_string ( commands [ index + 1 ] ) ? 1 : // If 2nd item is a string it's must be a new command
2 ; // Otherwise we have command and arg
function _turtle ( commands , state , full_state , index = 0 ) =
index < len ( commands ) ?
_turtle ( commands ,
_turtle_command ( commands [ index ] , commands [ index + 1 ] , commands [ index + 2 ] , state , index ) ,
full_state ,
index + _turtle_command_len ( commands , index )
) :
( full_state ? state : state [ 0 ] ) ;
// Turtle state: state = [path, step_vector, default angle, default arcsteps]
function _turtle_command ( command , parm , parm2 , state , index ) =
command = = "repeat" ?
assert ( is_num ( parm ) , str ( "\"repeat\" command requires a numeric repeat count at index " , index ) )
assert ( is_list ( parm2 ) , str ( "\"repeat\" command requires a command list parameter at index " , index ) )
_turtle_repeat ( parm2 , state , true , parm ) :
let (
path = 0 ,
step = 1 ,
angle = 2 ,
arcsteps = 3 ,
parm = ! is_string ( parm ) ? parm : undef ,
parm2 = ! is_string ( parm2 ) ? parm2 : undef ,
needvec = [ "jump" , "xymove" ] ,
neednum = [ "untilx" , "untily" , "xjump" , "yjump" , "angle" , "length" , "scale" , "addlength" ] ,
needeither = [ "setdir" ] ,
chvec = ! in_list ( command , needvec ) || is_vector ( parm , 2 ) ,
chnum = ! in_list ( command , neednum ) || is_num ( parm ) ,
vec_or_num = ! in_list ( command , needeither ) || ( is_num ( parm ) || is_vector ( parm , 2 ) ) ,
lastpt = last ( state [ path ] )
)
assert ( chvec , str ( "\"" , command , "\" requires a vector parameter at index " , index ) )
assert ( chnum , str ( "\"" , command , "\" requires a numeric parameter at index " , index ) )
assert ( vec_or_num , str ( "\"" , command , "\" requires a vector or numeric parameter at index " , index ) )
command = = "move" ? list_set ( state , path , concat ( state [ path ] , [ default ( parm , 1 ) * state [ step ] + lastpt ] ) ) :
command = = "untilx" ? (
let (
int = line_intersection ( [ lastpt , lastpt + state [ step ] ] , [ [ parm , 0 ] , [ parm , 1 ] ] ) ,
xgood = sign ( state [ step ] . x ) = = sign ( int . x - lastpt . x )
)
assert ( xgood , str ( "\"untilx\" never reaches desired goal at index " , index ) )
list_set ( state , path , concat ( state [ path ] , [ int ] ) )
) :
command = = "untily" ? (
let (
int = line_intersection ( [ lastpt , lastpt + state [ step ] ] , [ [ 0 , parm ] , [ 1 , parm ] ] ) ,
ygood = is_def ( int ) && sign ( state [ step ] . y ) = = sign ( int . y - lastpt . y )
)
assert ( ygood , str ( "\"untily\" never reaches desired goal at index " , index ) )
list_set ( state , path , concat ( state [ path ] , [ int ] ) )
) :
command = = "xmove" ? list_set ( state , path , concat ( state [ path ] , [ default ( parm , 1 ) * norm ( state [ step ] ) * [ 1 , 0 ] + lastpt ] ) ) :
command = = "ymove" ? list_set ( state , path , concat ( state [ path ] , [ default ( parm , 1 ) * norm ( state [ step ] ) * [ 0 , 1 ] + lastpt ] ) ) :
command = = "xymove" ? list_set ( state , path , concat ( state [ path ] , [ lastpt + parm ] ) ) :
command = = "jump" ? list_set ( state , path , concat ( state [ path ] , [ parm ] ) ) :
command = = "xjump" ? list_set ( state , path , concat ( state [ path ] , [ [ parm , lastpt . y ] ] ) ) :
command = = "yjump" ? list_set ( state , path , concat ( state [ path ] , [ [ lastpt . x , parm ] ] ) ) :
2021-12-28 22:05:37 +00:00
command = = "turn" || command = = "left" ? list_set ( state , step , rot ( default ( parm , state [ angle ] ) , p = state [ step ] ) ) :
command = = "right" ? list_set ( state , step , rot ( - default ( parm , state [ angle ] ) , p = state [ step ] ) ) :
2021-09-16 03:12:51 +00:00
command = = "angle" ? list_set ( state , angle , parm ) :
command = = "setdir" ? (
is_vector ( parm ) ?
list_set ( state , step , norm ( state [ step ] ) * unit ( parm ) ) :
list_set ( state , step , norm ( state [ step ] ) * [ cos ( parm ) , sin ( parm ) ] )
) :
command = = "length" ? list_set ( state , step , parm * unit ( state [ step ] ) ) :
command = = "scale" ? list_set ( state , step , parm * state [ step ] ) :
command = = "addlength" ? list_set ( state , step , state [ step ] + unit ( state [ step ] ) * parm ) :
command = = "arcsteps" ? list_set ( state , arcsteps , parm ) :
command = = "arcleft" || command = = "arcright" ?
assert ( is_num ( parm ) , str ( "\"" , command , "\" command requires a numeric radius value at index " , index ) )
let (
myangle = default ( parm2 , state [ angle ] ) ,
lrsign = command = = "arcleft" ? 1 : - 1 ,
radius = parm * sign ( myangle ) ,
center = lastpt + lrsign * radius * line_normal ( [ 0 , 0 ] , state [ step ] ) ,
steps = state [ arcsteps ] = = 0 ? segs ( abs ( radius ) ) : state [ arcsteps ] ,
arcpath = myangle = = 0 || radius = = 0 ? [ ] : arc (
steps ,
points = [
lastpt ,
rot ( cp = center , p = lastpt , a = sign ( parm ) * lrsign * myangle / 2 ) ,
rot ( cp = center , p = lastpt , a = sign ( parm ) * lrsign * myangle )
]
)
)
list_set (
state , [ path , step ] , [
concat ( state [ path ] , list_tail ( arcpath ) ) ,
2021-12-28 22:05:37 +00:00
rot ( lrsign * myangle , p = state [ step ] )
2021-09-16 03:12:51 +00:00
]
) :
command = = "arcleftto" || command = = "arcrightto" ?
assert ( is_num ( parm ) , str ( "\"" , command , "\" command requires a numeric radius value at index " , index ) )
assert ( is_num ( parm2 ) , str ( "\"" , command , "\" command requires a numeric angle value at index " , index ) )
let (
radius = parm ,
lrsign = command = = "arcleftto" ? 1 : - 1 ,
center = lastpt + lrsign * radius * line_normal ( [ 0 , 0 ] , state [ step ] ) ,
steps = state [ arcsteps ] = = 0 ? segs ( abs ( radius ) ) : state [ arcsteps ] ,
start_angle = posmod ( atan2 ( state [ step ] . y , state [ step ] . x ) , 360 ) ,
end_angle = posmod ( parm2 , 360 ) ,
delta_angle = - start_angle + ( lrsign * end_angle < lrsign * start_angle ? end_angle + lrsign * 360 : end_angle ) ,
arcpath = delta_angle = = 0 || radius = = 0 ? [ ] : arc (
steps ,
points = [
lastpt ,
rot ( cp = center , p = lastpt , a = sign ( radius ) * delta_angle / 2 ) ,
rot ( cp = center , p = lastpt , a = sign ( radius ) * delta_angle )
]
)
)
list_set (
state , [ path , step ] , [
concat ( state [ path ] , list_tail ( arcpath ) ) ,
2021-12-28 22:05:37 +00:00
rot ( delta_angle , p = state [ step ] )
2021-09-16 03:12:51 +00:00
]
) :
assert ( false , str ( "Unknown turtle command \"" , command , "\" at index" , index ) )
[ ] ;
2021-10-02 07:24:33 +00:00
2021-11-10 03:27:55 +00:00
// Section: Debugging polygons
// Module: debug_polygon()
2023-03-27 23:30:04 +00:00
// Synopsis: Draws an annotated polygon.
2023-05-27 08:42:09 +00:00
// SynTags: Geom
2023-03-27 23:30:04 +00:00
// Topics: Shapes (2D)
2024-03-13 00:11:16 +00:00
// See Also: debug_region(), debug_vnf(), debug_bezier()
2023-03-27 23:30:04 +00:00
//
2021-11-10 03:27:55 +00:00
// Usage:
// debug_polygon(points, paths, [vertices=], [edges=], [convexity=], [size=]);
// Description:
// A drop-in replacement for `polygon()` that renders and labels the path points and
// edges. The start of each path is marked with a blue circle and the end with a pink diamond.
// You can suppress the display of vertex or edge labeling using the `vertices` and `edges` arguments.
// Arguments:
// points = The array of 2D polygon vertices.
// paths = The path connections between the vertices.
// ---
// vertices = if true display vertex labels and start/end markers. Default: true
// edges = if true display edge labels. Default: true
// convexity = The max number of walls a ray can pass through the given polygon paths.
// size = The base size of the line and labels.
// Example(Big2D):
// debug_polygon(
// points=concat(
// regular_ngon(or=10, n=8),
// regular_ngon(or=8, n=8)
// ),
// paths=[
// [for (i=[0:7]) i],
// [for (i=[15:-1:8]) i]
// ]
// );
module debug_polygon ( points , paths , vertices = true , edges = true , convexity = 2 , size = 1 )
{
2022-04-12 02:18:52 +00:00
no_children ( $children ) ;
2024-03-13 00:11:16 +00:00
print_paths = is_def ( paths ) ;
echo ( points = points ) ;
if ( print_paths )
echo ( paths = paths ) ;
2021-11-10 03:27:55 +00:00
paths = is_undef ( paths ) ? [ count ( points ) ] :
is_num ( paths [ 0 ] ) ? [ paths ] :
paths ;
linear_extrude ( height = 0.01 , convexity = convexity , center = true ) {
polygon ( points = points , paths = paths , convexity = convexity ) ;
}
2024-03-13 00:11:16 +00:00
if ( vertices )
_debug_poly_verts ( points , size ) ;
if ( edges )
for ( j = [ 0 : 1 : len ( paths ) - 1 ] ) _debug_poly_edges ( j , points , paths [ j ] , vertices , size ) ;
}
module _debug_poly_verts ( points , size )
{
labels = is_vector ( points [ 0 ] ) ? [ for ( i = idx ( points ) ) str ( i ) ]
: [ for ( j = idx ( points ) , i = idx ( points [ j ] ) ) str ( chr ( 97 + j ) , i ) ] ;
points = is_vector ( points [ 0 ] ) ? points : flatten ( points ) ;
dups = vector_search ( points , EPSILON , points ) ;
color ( "red" ) {
2021-11-10 03:27:55 +00:00
for ( ind = dups ) {
2024-03-13 00:11:16 +00:00
numstr = str_join ( select ( labels , ind ) , "," ) ;
2021-11-10 03:27:55 +00:00
up ( 0.2 ) {
translate ( points [ ind [ 0 ] ] ) {
linear_extrude ( height = 0.1 , convexity = 10 , center = true ) {
text ( text = numstr , size = size , halign = "center" , valign = "center" ) ;
}
}
}
}
}
2024-03-13 00:11:16 +00:00
}
module _debug_poly_edges ( j , points , path , vertices , size )
{
path = default ( path , count ( len ( points ) ) ) ;
if ( vertices ) {
2021-11-10 03:27:55 +00:00
translate ( points [ path [ 0 ] ] ) {
color ( "cyan" ) up ( 0.1 ) cylinder ( d = size * 1.5 , h = 0.01 , center = false , $fn = 12 ) ;
}
translate ( points [ path [ len ( path ) - 1 ] ] ) {
color ( "pink" ) up ( 0.11 ) cylinder ( d = size * 1.5 , h = 0.01 , center = false , $fn = 4 ) ;
}
}
for ( i = [ 0 : 1 : len ( path ) - 1 ] ) {
midpt = ( points [ path [ i ] ] + points [ path [ ( i + 1 ) % len ( path ) ] ] ) / 2 ;
color ( "blue" ) {
up ( 0.2 ) {
translate ( midpt ) {
linear_extrude ( height = 0.1 , convexity = 10 , center = true ) {
text ( text = str ( chr ( 65 + j ) , i ) , size = size / 2 , halign = "center" , valign = "center" ) ;
}
}
}
}
}
2024-03-13 00:11:16 +00:00
}
2021-11-10 03:27:55 +00:00
2021-10-02 07:24:33 +00:00
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap