From 81c5303c94b823ff0b42e751bfc18cbbd1ed2474 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Thu, 17 Apr 2025 06:35:17 -0400 Subject: [PATCH 1/6] add plot3d --- math.scad | 2 +- shapes3d.scad | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/math.scad b/math.scad index 405a6fcf..31c69e60 100644 --- a/math.scad +++ b/math.scad @@ -144,7 +144,7 @@ function lerpn(a,b,n,endpoint=true) = // Topics: Interpolation, Math // See Also: lerpn() // Usage: -// x = lerp(pts, x, y); +// x = bilerp(pts, x, y); // Description: // Compute bilinear interpolation between four values using two // coordinates that are meant to lie in [0,1]. (If they are outside diff --git a/shapes3d.scad b/shapes3d.scad index d430635f..3397a395 100644 --- a/shapes3d.scad +++ b/shapes3d.scad @@ -4212,6 +4212,98 @@ module fillet(l, r, ang, r1, r2, excess=0.01, d1, d2,d,length, h, height, anchor } + + +// Function&Module: plot3d() +// Synopsis: Generates a surface by evaluating a function on a 2D grid +// SynTags: Geom, VNF +// Topics: Function Plotting +// See Also: cylindrical_heightfield() +// Usage: As Module +// plot3d(f, xrange, yrange, [zclip=], [zspan=], [base=], [convexity=], [style=]) [ATTACHMENTS]; +// Usage: As Function +// vnf = plot3d(f, xrange, yrange, [zclip=], [zspan=], [base=], [style=]); +// Arguments: +// f = function literal accepting two arguments (x and y) that defines the function to compute +// xrange = A list or range of values for x +// yrange = A list or range of values for y +// --- +// zclip = Constrain the function to these bounds. +// zspan = Rescale and shift the function values so the minimum value of f appears at zspan[0] and the maximum at zspan[1]. +// base = Amount of extra thickness to add at the bottom of the model. If set to zero, produce a non-manifold zero-thickness VNF. Default: 1 +// style = {{vnf_vertex_array()}} style used to triangulate heightfield textures. Default: "default" +// convexity = Max number of times a line could intersect a wall of the surface being formed. Module only. Default: 10 +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#subsection-spin). Default: `0` +// orient = Vector to rotate top towards. See [orient](attachments.scad#subsection-orient). Default: `UP` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` +// orient = Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP` +// atype = Select "hull" or "intersect" anchor type. Default: "hull" +// cp = Centerpoint for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" +// Anchor Types: +// "hull" = Anchors to the virtual convex hull of the shape. +// "intersect" = Anchors to the surface of the shape. +// Named Anchors: +// "origin" = Anchor at the origin, oriented UP. +// Example(NoScales): A basic function calculation +// func = function (x,y) 35*cos(3*norm([x,y])); +// plot3d(func, [-180:4:180], [-180:4:180]); +// Example(NoScales): Here we give the function inline and since it blows up we add clipping +// plot3d(function (x,y) 1/norm([x,y]), [-2:.1:2], [-2:.1:2], zclip=[0,2],style="default"); +// Example(NoScales): Clipped edges often don't look very good and may be improved somewhat with more points. Here we give lists with varying point spacing to improve the point density around the clipped top of the shape. +// range = concat( +// lerpn(-2,-1,10,endpoint=false), +// lerpn(-1,1,75,endpoint=false), +// lerpn(1,2,10) +// ); +// plot3d(function (x,y) 1/norm([x,y]), range, range, zclip=[0,2],style="default"); +// Example(3D,NoAxes,VPR=[76.70,0.00,18.70],VPD=325.23,VPT=[-8.47,27.30,50.84]): Making a zero thickness VNF +// fn = function (x,y) (x^2+y^2)/50; +// plot3d(fn, [-50:5:50], [-50:5:50], base=0); +// Example(3D,NoScales): Use `zspan` to fit the plot vertically to a range and use anchoring to center it on the origin. +// f = function(x,y) 10*(sin(20*x)^2+cos(20*y)^2)/norm([2*x,y]); +// plot3d(f, [10:.3:40], [4:.3:37],zspan=[0,25],anchor=BOT); + +module plot3d(f,xrange,yrange,zclip, zspan, base=1, anchor="origin", orient=UP, spin=0, atype="hull", cp="box", convexity=4, style="default") + vnf_polyhedron(plot3d(f,xrange,yrange,zclip, zspan,base, style=style), atype=atype, orient=orient, anchor=anchor, cp=cp, convexity=convexity) children(); + +function plot3d(f,xrange,yrange,zclip, zspan, base=1, anchor="origin", orient=UP, spin=0, atype="hull", cp="box", style="default") = + assert(is_finite(base) && base>=0, "base must be a nonnegative number") + assert(is_vector(xrange) || valid_range(xrange), "xrange must be a vector or nonempty range") + assert(is_vector(yrange) || valid_range(yrange), "yrange must be a vector or nonempty range") + assert(is_range(xrange) || is_increasing(xrange, strict=true), "xrange must be strictly increasing") + assert(is_range(yrange) || is_increasing(yrange, strict=true), "yrange must be strictly increasing") + assert(num_defined([zclip,zspan])<2, "Cannot give both zclip and zspan") + assert(is_undef(zclip) || (is_list(zclip) && len(zclip)==2 && is_num(zclip[0]) && is_num(zclip[1])), "zclip must be a list of two values (which may be infinite)") + assert(is_undef(zspan) || (is_vector(zspan,2) && zspan[0]1 && len(data)>1, "xrange and yrange must both provide at least 2 points"), + minval = min(column(flatten(data),2)), + maxval = max(column(flatten(data),2)), + sdata = is_undef(zspan) ? data + : let( + scale = (zspan[1]-zspan[0])/(maxval-minval) + ) + [for(row=data) [for (entry=row) [entry.x,entry.y,scale*(entry.z-minval)+zspan[0]]]] + ) + base==0 ? vnf_vertex_array(sdata,style=style) + : + let( + minval = min(column(flatten(sdata),2)), + maxval = max(column(flatten(sdata),2)), + bottom = is_def(zspan) ? zspan[0]-base : minval-base, + data = [ [for(p=sdata[0]) [p.x,p.y,bottom]], + each sdata, + [for(p=last(sdata)) [p.x,p.y,bottom]] + ], + vnf = vnf_vertex_array(transpose(data), col_wrap=true, caps=true, style=style) + ) + reorient(anchor,spin,orient, vnf=vnf, p=vnf); + + + // Function&Module: heightfield() // Synopsis: Generates a 3D surface from a 2D grid of values. // SynTags: Geom, VNF From 99d2d7f8f64cf1ce176edd53dae864c342e48616 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Thu, 17 Apr 2025 18:50:47 -0400 Subject: [PATCH 2/6] plot3d updates --- shapes3d.scad | 138 +++++++++++++++++++++++++++++--------------------- utility.scad | 15 ++++++ 2 files changed, 94 insertions(+), 59 deletions(-) diff --git a/shapes3d.scad b/shapes3d.scad index 3397a395..4df49f3e 100644 --- a/shapes3d.scad +++ b/shapes3d.scad @@ -1315,9 +1315,9 @@ function textured_tile( tex_skip=0, anchor=CENTER, spin=0, orient=UP, _return_anchor=false -) = - assert(tex_reps==undef || is_int(tex_reps) || (all_integer(tex_reps) && len(tex_reps)==2), "tex_reps must be an integer or list of two integers") - assert(tex_size==undef || is_vector(tex_size,2) || is_finite(tex_size)) +) = let(f=echo(size=tex_size)) + assert(is_undef(tex_reps) || is_int(tex_reps) || (all_integer(tex_reps) && len(tex_reps)==2), "tex_reps must be an integer or list of two integers") + assert(is_undef(tex_size) || is_vector(tex_size,2) || is_finite(tex_size)) assert(num_defined([tex_size, tex_reps])<2, "Cannot give both tex_size and tex_reps") assert(is_undef(size) || is_num(size) || is_vector(size,2) || is_vector(size,3), "size must be a 2-vector or 3-vector") assert(is_undef(size) || num_defined([ysize,h, height, thickness, w1,w2,ang])==0, "Cannot combine size with any other dimensional specifications") @@ -4223,6 +4223,22 @@ module fillet(l, r, ang, r1, r2, excess=0.01, d1, d2,d,length, h, height, anchor // plot3d(f, xrange, yrange, [zclip=], [zspan=], [base=], [convexity=], [style=]) [ATTACHMENTS]; // Usage: As Function // vnf = plot3d(f, xrange, yrange, [zclip=], [zspan=], [base=], [style=]); +// Description: +// Given a function literal taking 2 parameters and a 2d grid, generate a surface where the height at any point is +// the value of the function. You can specify the grid using a range or using a list of points that +// need not be uniformly spaced. To create a valid polyhedron, the graph is closed at the sides and +// a base is added below the smallest value. By default this base has unit thickness, but you can +// adjust it by setting the `base` parameter. If you set `base=0` then you will get a a zero thickness +// sheet that is not a manifold without sides or a bottom. +// . +// Your function may have have excessively large values at some points, or you may not know exactly +// what its extreme values are. To manage these situations you can use either the `zclip` or `zspan` +// parameter (but not both). The `zclip` parameter is a 2-vector giving a minimum and maximum +// value, either of which can be infinite. If the function falls below the minimum it is set +// equal to the minimum, and if it rises above the maximum it is set equal to the maximum. The +// `zspan` parameter is a 2-vector giving a minum and maximum value which must both be finite. +// The function's values will be scaled and shifted to exactly cover the range you specifiy +// in `zspan`. // Arguments: // f = function literal accepting two arguments (x and y) that defines the function to compute // xrange = A list or range of values for x @@ -4291,7 +4307,7 @@ function plot3d(f,xrange,yrange,zclip, zspan, base=1, anchor="origin", orient=UP base==0 ? vnf_vertex_array(sdata,style=style) : let( - minval = min(column(flatten(sdata),2)), + minval = min(column(flatten(sdata),2)), maxval = max(column(flatten(sdata),2)), bottom = is_def(zspan) ? zspan[0]-base : minval-base, data = [ [for(p=sdata[0]) [p.x,p.y,bottom]], @@ -4304,61 +4320,61 @@ function plot3d(f,xrange,yrange,zclip, zspan, base=1, anchor="origin", orient=UP -// Function&Module: heightfield() -// Synopsis: Generates a 3D surface from a 2D grid of values. -// SynTags: Geom, VNF -// Topics: Textures, Heightfield -// See Also: cylindrical_heightfield() -// Usage: As Module -// heightfield(data, [size], [bottom], [maxz], [xrange], [yrange], [style], [convexity], ...) [ATTACHMENTS]; -// Usage: As Function -// vnf = heightfield(data, [size], [bottom], [maxz], [xrange], [yrange], [style], ...); -// Description: -// Given a regular rectangular 2D grid of scalar values, or a function literal, generates a 3D -// surface where the height at any given point is the scalar value for that position. -// One script to convert a grayscale image to a heightfield array in a .scad file can be found at: -// https://raw.githubusercontent.com/BelfrySCAD/BOSL2/master/scripts/img2scad.py -// The bottom value defines a planar base for the resulting shape and it must be strictly less than -// the model data to produce valid geometry, so data which is too small is set to 0.1 units above the bottom value. -// Arguments: -// data = This is either the 2D rectangular array of heights, or a function literal that takes X and Y arguments. -// size = The [X,Y] size of the surface to create. If given as a scalar, use it for both X and Y sizes. Default: `[100,100]` -// bottom = The Z coordinate for the bottom of the heightfield object to create. Any heights lower than this will be truncated to very slightly (0.1) above this height. Default: -20 -// maxz = The maximum height to model. Truncates anything taller to this height. Set to INF for no truncation. Default: 100 -// xrange = A range of values to iterate X over when calculating a surface from a function literal. Default: [-1 : 0.01 : 1] -// yrange = A range of values to iterate Y over when calculating a surface from a function literal. Default: [-1 : 0.01 : 1] -// style = The style of subdividing the quads into faces. Valid options are "default", "alt", and "quincunx". Default: "default" -// --- -// convexity = Max number of times a line could intersect a wall of the surface being formed. Module only. Default: 10 -// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER` -// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#subsection-spin). Default: `0` -// orient = Vector to rotate top towards. See [orient](attachments.scad#subsection-orient). Default: `UP` -// Example: -// heightfield(size=[100,100], bottom=-20, data=[ -// for (y=[-180:4:180]) [ -// for(x=[-180:4:180]) -// 10*cos(3*norm([x,y])) -// ] -// ]); -// Example: -// intersection() { -// heightfield(size=[100,100], data=[ -// for (y=[-180:5:180]) [ -// for(x=[-180:5:180]) -// 10+5*cos(3*x)*sin(3*y) -// ] -// ]); -// cylinder(h=50,d=100); -// } -// Example: Heightfield by Function -// fn = function (x,y) 10*sin(x*360)*cos(y*360); -// heightfield(size=[100,100], data=fn); -// Example: Heightfield by Function, with Specific Ranges -// fn = function (x,y) 2*cos(5*norm([x,y])); -// heightfield( -// size=[100,100], bottom=-20, data=fn, -// xrange=[-180:2:180], yrange=[-180:2:180] -// ); +/// Function&Module: heightfield() +/// Synopsis: Generates a 3D surface from a 2D grid of values. +/// SynTags: Geom, VNF +/// Topics: Textures, Heightfield +/// See Also: cylindrical_heightfield() +/// Usage: As Module +/// heightfield(data, [size], [bottom], [maxz], [xrange], [yrange], [style], [convexity], ...) [ATTACHMENTS]; +/// Usage: As Function +/// vnf = heightfield(data, [size], [bottom], [maxz], [xrange], [yrange], [style], ...); +/// Description: +/// Given a regular rectangular 2D grid of scalar values, or a function literal, generates a 3D +/// surface where the height at any given point is the scalar value for that position. +/// One script to convert a grayscale image to a heightfield array in a .scad file can be found at: +/// https://raw.githubusercontent.com/BelfrySCAD/BOSL2/master/scripts/img2scad.py +/// The bottom value defines a planar base for the resulting shape and it must be strictly less than +/// the model data to produce valid geometry, so data which is too small is set to 0.1 units above the bottom value. +/// Arguments: +/// data = This is either the 2D rectangular array of heights, or a function literal that takes X and Y arguments. +/// size = The [X,Y] size of the surface to create. If given as a scalar, use it for both X and Y sizes. Default: `[100,100]` +/// bottom = The Z coordinate for the bottom of the heightfield object to create. Any heights lower than this will be truncated to very slightly (0.1) above this height. Default: -20 +/// maxz = The maximum height to model. Truncates anything taller to this height. Set to INF for no truncation. Default: 100 +/// xrange = A range of values to iterate X over when calculating a surface from a function literal. Default: [-1 : 0.01 : 1] +/// yrange = A range of values to iterate Y over when calculating a surface from a function literal. Default: [-1 : 0.01 : 1] +/// style = The style of subdividing the quads into faces. Valid options are "default", "alt", and "quincunx". Default: "default" +/// --- +/// convexity = Max number of times a line could intersect a wall of the surface being formed. Module only. Default: 10 +/// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER` +/// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#subsection-spin). Default: `0` +/// orient = Vector to rotate top towards. See [orient](attachments.scad#subsection-orient). Default: `UP` +/// Example: +/// heightfield(size=[100,100], bottom=-20, data=[ +/// for (y=[-180:4:180]) [ +/// for(x=[-180:4:180]) +/// 10*cos(3*norm([x,y])) +/// ] +/// ]); +/// Example: +/// intersection() { +/// heightfield(size=[100,100], data=[ +/// for (y=[-180:5:180]) [ +/// for(x=[-180:5:180]) +/// 10+5*cos(3*x)*sin(3*y) +/// ] +/// ]); +/// cylinder(h=50,d=100); +/// } +/// Example: Heightfield by Function +/// fn = function (x,y) 10*sin(x*360)*cos(y*360); +/// heightfield(size=[100,100], data=fn); +/// Example: Heightfield by Function, with Specific Ranges +/// fn = function (x,y) 2*cos(5*norm([x,y])); +/// heightfield( +/// size=[100,100], bottom=-20, data=fn, +/// xrange=[-180:2:180], yrange=[-180:2:180] +/// ); module heightfield(data, size=[100,100], bottom=-20, maxz=100, xrange=[-1:0.04:1], yrange=[-1:0.04:1], style="default", convexity=10, anchor=CENTER, spin=0, orient=UP) { @@ -4372,6 +4388,10 @@ module heightfield(data, size=[100,100], bottom=-20, maxz=100, xrange=[-1:0.04:1 function heightfield(data, size=[100,100], bottom=-20, maxz=100, xrange=[-1:0.04:1], yrange=[-1:0.04:1], style="default", anchor=CENTER, spin=0, orient=UP) = + let( + dummy=is_function(data) ? echo("***** heightfield() is deprecated and will be removed in a future version. For displaying functions use plot3d(). *****") + : echo("***** heightfield() is deprecated and will be removed in a future version. For displaying arrays use textured_tile() *****") + ) assert(is_list(data) || is_function(data)) let( size = is_num(size)? [size,size] : point2d(size), diff --git a/utility.scad b/utility.scad index 97c4f1b8..6bb643fe 100644 --- a/utility.scad +++ b/utility.scad @@ -877,6 +877,21 @@ module deprecate(new_name) } + +// Module: echo_viewport() +// Synopsis: Display the current viewport parameters. +// Usage: +// echo_viewport(); +// Description: +// Display the current viewport parameters so that they can be pasted into examples for the wiki. + +module echo_viewport() +{ + echo(format("VPR=[{:.2f},{:.2f},{:.2f}],VPD={:.2f},VPT=[{:.2f},{:.2f},{:.2f}]", [each $vpr, $vpd, each $vpt])); +} + + + // Section: Testing Helpers From 615a088488817347e8a6a96d6210014bf6053693 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Thu, 17 Apr 2025 22:43:49 -0400 Subject: [PATCH 3/6] add plot_revolution --- math.scad | 2 +- rounding.scad | 322 +++++++++++++++++++++++++++----------------------- skin.scad | 2 +- utility.scad | 3 +- 4 files changed, 177 insertions(+), 152 deletions(-) diff --git a/math.scad b/math.scad index 31c69e60..7cc20145 100644 --- a/math.scad +++ b/math.scad @@ -142,7 +142,7 @@ function lerpn(a,b,n,endpoint=true) = // Function: bilerp() // Synopsis: Bi-linear interpolation between four values // Topics: Interpolation, Math -// See Also: lerpn() +// See Also: lerp() // Usage: // x = bilerp(pts, x, y); // Description: diff --git a/rounding.scad b/rounding.scad index 44a023a9..50bb712a 100644 --- a/rounding.scad +++ b/rounding.scad @@ -2579,7 +2579,9 @@ function _circle_mask(r) = // same dimensions that is has on the plane, with y axis mapping to the z axis and the x axis bending // around the curve of the cylinder. The angular span of the path on the cylinder must be somewhat // less than 180 degrees, and the path shouldn't have closely spaced points at concave points of high curvature because -// this causes self-intersection in the mask polyhedron, resulting in CGAL failures. +// this causes self-intersection in the mask polyhedron, resulting in CGAL failures. The path also cannot include +// sharp corners, because it internally uses {{offset()}} which will expand those sharp corners into very long single +// segments that produce incorrect result. // Arguments: // r / radius = center radius of the cylindrical shell to cut a hole in // thickness = thickness of cylindrical shell (may need to be slighly oversized) @@ -4535,6 +4537,10 @@ function _find_center_anchor(desc1, desc2, anchor2, flip) = +function _is_anchor(a) = is_string(a) || is_vector(a,2) || is_vector(a,3); + +function _is_anchor_list(list) = is_list(list) && [for(a=list) if (!_is_anchor(a)) a]==[]; + module prism_connector(profile, desc1, anchor1, desc2, anchor2, shift1=0, shift2=0, spin_align=1, scale=1, @@ -4545,155 +4551,173 @@ module prism_connector(profile, desc1, anchor1, desc2, anchor2, shift1=0, shift2 smooth_normals, smooth_normals1, smooth_normals2, debug=false, debug_pos=false) { - base_fillet = first_defined([fillet1,fillet,0]); - aux_fillet = first_defined([fillet2,fillet,0]); - - base_overlap = first_defined([overlap1,overlap,1]); - aux_overlap = first_defined([overlap2,overlap,1]); - - base_n = first_defined([n1,n,15]); - aux_n = first_defined([n2,n,15]); - - base_uniform = first_defined([uniform1, uniform, true]); - aux_uniform = first_defined([uniform2, uniform, true]); - - base_k = first_defined([k1,k,0.7]); - aux_k = first_defined([k2,k,0.7]); - - profile = force_path(profile,"profile"); - dummy0 = assert(is_path(profile,2), "profile must be a 2d path"); - - corrected_base_anchor = is_vector(anchor1) && norm(anchor1)==0 ? _find_center_anchor(desc1,desc2,anchor2,true) : undef; - corrected_aux_anchor = is_vector(anchor2) && norm(anchor2)==0 ? _find_center_anchor(desc2,desc1,anchor1,false) : undef; - - - base_anchor=is_string(anchor1) ? anchor1 - : is_def(corrected_base_anchor) ? corrected_base_anchor[0] - : point3d(anchor1); - aux_anchor=is_string(anchor2) ? anchor2 - : is_def(corrected_aux_anchor) ? corrected_aux_anchor[0] - : point3d(anchor2); - - base=desc1; - aux=desc2; - - current = parent(); - tobase = current==base ? ident(4) : linear_solve(current[0],base[0]); // Maps from current frame into the base frame - auxmap = linear_solve(base[0], aux[0]); // Map from the base (desc1) coordinate frame into the aux (desc2) coordinate frame - - dummy = assert(is_vector(base_anchor) || is_string(base_anchor), "anchor1 must be a string or a 3-vector") - assert(is_vector(aux_anchor) || is_string(aux_anchor), "anchor2 must be a string or a 3-vector") - assert(is_rotation(auxmap), "desc1 and desc2 are not related to each other by a rotation (and translation)"); - - base_type = _get_obj_type(1,base[1],base_anchor,profile); - base_axis = base_type=="cyl" ? base[1][5] : RIGHT; - base_edge = _is_geom_an_edge(base[1],base_anchor); - base_r = in_list(base_type,["cyl","sphere"]) ? base[1][1] : 1; - base_anch = _find_anchor(base_anchor, base[1]); - base_spin = base_anch[3]; - base_anch_pos = base_anch[1]; - base_anch_dir = base_anch[2]; - - prelim_shift1 = _check_join_shift(1,base_type,shift1,true); - shift1 = corrected_base_anchor ? corrected_base_anchor[1] + prelim_shift1 : prelim_shift1; - aux_type = _get_obj_type(2,aux[1],aux_anchor,profile); - aux_anch = _find_anchor(aux_anchor, aux[1]); - aux_edge = _is_geom_an_edge(aux[1],aux_anchor); - aux_r = in_list(aux_type,["cyl","sphere"]) ? aux[1][1] : 1; - aux_anch_pos = aux_anch[1]; - aux_anch_dir = aux_anch[2]; - aux_spin = aux_anch[3]; - aux_spin_dir = apply(rot(from=UP,to=aux_anch[2])*zrot(aux_spin),BACK); - aux_axis = aux_type=="cyl" ? aux[1][5] : RIGHT; - prelim_shift2 = _check_join_shift(2,aux_type,shift2,false); - shift2 = corrected_aux_anchor ? corrected_aux_anchor[1] + prelim_shift2 : prelim_shift2; - - - base_smooth_normals = first_defined([smooth_normals1, smooth_normals,!base_edge]); - aux_smooth_normals = first_defined([smooth_normals2, smooth_normals,!aux_edge]); - - backdir = base_type=="cyl" ? base_axis - : apply(rot(from=UP,to=base_anch_dir)*zrot(base_spin),BACK); - anch_shift = base_type=="plane" || is_list(base_type) ? base_anch_pos : CENTER; - - - // Map from the starting position for a join_prism object to - // the standard position for the object. - // If the aux object is an edge (type is a list) then the starting position - // of the prism is with the edge lying on the X axis and the object below. - aux_to_canonical = aux_type=="sphere" ? IDENT - : aux_type=="cyl" ? frame_map(x=aux_axis, z=aux_anch[2]) - : aux_type=="plane" ? move(aux_anch_pos) * rot(from=UP, to=aux_anch[2])*zrot(aux_spin) - : /* list */ move(aux_anch_pos) * frame_map(z=aux_anch[2],x=aux_spin_dir) ; - - // aux_T is the map that maps the auxiliary object from its initial position to - // the position for the prism. The initial position is centered for a sphere, - // with the axis X aligned for a cylinder, and with the face of a polyhedron - // laying in the XY plane for polyhedra. - aux_T = move(-shift1) - * frame_map(x=backdir, z=base_anch_dir, reverse=true) - * move(-anch_shift) - * auxmap - * aux_to_canonical - * move(shift2); - aux_root = aux_type=="plane" || is_list(aux_type) || aux_anchor==CTR ? apply(aux_T,CTR) - : apply(aux_T * matrix_inverse(aux_to_canonical), aux_anch_pos); - - base_root = base_type=="plane" || is_list(base_type) || base_anchor==CTR ? CENTER : base_r*UP; - - prism_axis = aux_root-base_root; - - base_inside = prism_axis.z<0 ? -1 : 1; - - aux_normal = aux_type=="cyl" || aux_type=="sphere" ? apply(aux_T*matrix_inverse(aux_to_canonical), aux_anch[2]) - apply(aux_T*matrix_inverse(aux_to_canonical), CTR) - : apply(aux_T, UP) - apply(aux_T,CTR); // Is this right? Added second term - aux_inside = aux_normal*(base_root-aux_root) < 0 ? -1 : 1; - - shaft = rot(from=UP,to=prism_axis, p=zrot(base_spin,BACK)); - - obj1_back = apply(frame_map(x=backdir,z=base_anch_dir,reverse=true)*rot(from=UP,to=base_anch_dir)*zrot(base_spin),BACK); - obj2_back = aux_type=="plane" ? apply(aux_T,BACK)-apply(aux_T,CTR) - : is_list(aux_type) ? apply(aux_T*matrix_inverse(frame_map(z=aux_anch[2],x=aux_spin_dir)),aux_spin_dir)-apply(aux_T,CTR) - : aux_type=="sphere"? apply(aux_T,aux_spin_dir)-apply(aux_T,CTR) - /*cyl*/ : apply(aux_T*matrix_inverse(aux_to_canonical),aux_spin_dir)-apply(aux_T,CTR); - - v1 = vector_perp(prism_axis, shaft); - v2 = vector_perp(prism_axis, obj1_back); - v3 = vector_perp(prism_axis, obj2_back); - sign1 = cross(v1,v2)*prism_axis < 0 ? 1 : -1; - sign2 = cross(v3,v1)*prism_axis < 0 ? 1 : -1; - - spin1 = sign1 * vector_angle(v1,v2); - spin2 = -sign2 * vector_angle(v3,v1); - spin = spin_align==1 ? spin1 - : spin_align==2 ? spin2 - : spin_align==12 ? mean_angle(spin1,spin2) - : spin_align==21 ? mean_angle(spin2,spin1) - : assert(false, str("spin_align must be one of 1, 2, 12, or 21 but was ",spin_align)); - multmatrix(tobase) - move(anch_shift) - frame_map(x=backdir, z=base_anch_dir) - move(shift1){ - // For debugging spin, shows line in the spin direction - //stroke([aux_root,aux_root+unit(obj2_back)*15], width=1,color="lightblue"); - //stroke([base_root,base_root+unit(obj1_back)*15], width=1,color="lightgreen"); - - if (debug_pos) - move(base_root)rot(from=UP,to=prism_axis) - linear_extrude(height=norm(base_root-aux_root))zrot(base_spin-spin)polygon(profile); - else{ - join_prism(zrot(base_spin-spin,profile), - base=base_type, base_r=u_mul(base_r,base_inside), - aux=aux_type, aux_T=aux_T, aux_r=u_mul(aux_r,aux_inside), - scale=scale, - start=base_root, end=aux_root, - base_k=base_k, aux_k=aux_k, base_overlap=base_overlap, aux_overlap=aux_overlap, - base_n=base_n, aux_n=aux_n, base_fillet=base_fillet, aux_fillet=aux_fillet, - base_smooth_normals = base_smooth_normals, aux_smooth_normals=aux_smooth_normals, - debug=debug, - _name1="desc1", _name2="desc2") children(); + dummy1 = assert(_is_anchor(anchor1) || _is_anchor_list(anchor1) , "anchor1 must be an anchor (string or a 3-vector) or a list of anchors") + assert(_is_anchor(anchor2) || _is_anchor_list(anchor2) , "anchor2 must be an anchor (string or a 3-vector) or a list of anchors"); + if (_is_anchor_list(anchor1) || _is_anchor_list(anchor2)) { + anchor1_list=_is_anchor(anchor1) ? [anchor1] : anchor1; + anchor2_list=_is_anchor(anchor2) ? [anchor2] : anchor2; + for(i = idx(anchor1_list)) + for(j= idx(anchor2_list)) + { + $anchor1 = anchor1_list[i]; + $anchor2 = anchor2_list[j]; + $idx = [i,j]; + prism_connector(profile=profile, desc1=desc1, anchor1=$anchor1, desc2=desc2, anchor2=$anchor2, shift1=shift1, shift2=shift2, spin_align=spin_align, + scale=scale, fillet=fillet, fillet1=fillet1, fillet2=fillet2, overlap=overlap, overlap1=overlap1, overlap2=overlap2, + k=k, k1=k1, k2=k2, n=n, n1=n1, n2=n2, uniform=uniform, uniform1=uniform1, uniform2=uniform2, + smooth_normals=smooth_normals, smooth_normals1=smooth_normals1, smooth_normals2=smooth_normals2, + debug=debug, debug_pos=debug_pos) children(); } - } + } + else { + base_fillet = first_defined([fillet1,fillet,0]); + aux_fillet = first_defined([fillet2,fillet,0]); + + base_overlap = first_defined([overlap1,overlap,1]); + aux_overlap = first_defined([overlap2,overlap,1]); + + base_n = first_defined([n1,n,15]); + aux_n = first_defined([n2,n,15]); + + base_uniform = first_defined([uniform1, uniform, true]); + aux_uniform = first_defined([uniform2, uniform, true]); + + base_k = first_defined([k1,k,0.7]); + aux_k = first_defined([k2,k,0.7]); + + profile = force_path(profile,"profile"); + dummy0 = assert(is_path(profile,2), "profile must be a 2d path"); + + corrected_base_anchor = is_vector(anchor1) && norm(anchor1)==0 ? _find_center_anchor(desc1,desc2,anchor2,true) : undef; + corrected_aux_anchor = is_vector(anchor2) && norm(anchor2)==0 ? _find_center_anchor(desc2,desc1,anchor1,false) : undef; + + + base_anchor=is_string(anchor1) ? anchor1 + : is_def(corrected_base_anchor) ? corrected_base_anchor[0] + : point3d(anchor1); + aux_anchor=is_string(anchor2) ? anchor2 + : is_def(corrected_aux_anchor) ? corrected_aux_anchor[0] + : point3d(anchor2); + + base=desc1; + aux=desc2; + + current = parent(); + tobase = current==base ? ident(4) : linear_solve(current[0],base[0]); // Maps from current frame into the base frame + auxmap = linear_solve(base[0], aux[0]); // Map from the base (desc1) coordinate frame into the aux (desc2) coordinate frame + + dummy = + assert(is_rotation(auxmap), "desc1 and desc2 are not related to each other by a rotation (and translation)"); + + base_type = _get_obj_type(1,base[1],base_anchor,profile); + base_axis = base_type=="cyl" ? base[1][5] : RIGHT; + base_edge = _is_geom_an_edge(base[1],base_anchor); + base_r = in_list(base_type,["cyl","sphere"]) ? base[1][1] : 1; + base_anch = _find_anchor(base_anchor, base[1]); + base_spin = base_anch[3]; + base_anch_pos = base_anch[1]; + base_anch_dir = base_anch[2]; + + prelim_shift1 = _check_join_shift(1,base_type,shift1,true); + shift1 = corrected_base_anchor ? corrected_base_anchor[1] + prelim_shift1 : prelim_shift1; + aux_type = _get_obj_type(2,aux[1],aux_anchor,profile); + aux_anch = _find_anchor(aux_anchor, aux[1]); + aux_edge = _is_geom_an_edge(aux[1],aux_anchor); + aux_r = in_list(aux_type,["cyl","sphere"]) ? aux[1][1] : 1; + aux_anch_pos = aux_anch[1]; + aux_anch_dir = aux_anch[2]; + aux_spin = aux_anch[3]; + aux_spin_dir = apply(rot(from=UP,to=aux_anch[2])*zrot(aux_spin),BACK); + aux_axis = aux_type=="cyl" ? aux[1][5] : RIGHT; + prelim_shift2 = _check_join_shift(2,aux_type,shift2,false); + shift2 = corrected_aux_anchor ? corrected_aux_anchor[1] + prelim_shift2 : prelim_shift2; + + + base_smooth_normals = first_defined([smooth_normals1, smooth_normals,!base_edge]); + aux_smooth_normals = first_defined([smooth_normals2, smooth_normals,!aux_edge]); + + backdir = base_type=="cyl" ? base_axis + : apply(rot(from=UP,to=base_anch_dir)*zrot(base_spin),BACK); + anch_shift = base_type=="plane" || is_list(base_type) ? base_anch_pos : CENTER; + + + // Map from the starting position for a join_prism object to + // the standard position for the object. + // If the aux object is an edge (type is a list) then the starting position + // of the prism is with the edge lying on the X axis and the object below. + aux_to_canonical = aux_type=="sphere" ? IDENT + : aux_type=="cyl" ? frame_map(x=aux_axis, z=aux_anch[2]) + : aux_type=="plane" ? move(aux_anch_pos) * rot(from=UP, to=aux_anch[2])*zrot(aux_spin) + : /* list */ move(aux_anch_pos) * frame_map(z=aux_anch[2],x=aux_spin_dir) ; + + // aux_T is the map that maps the auxiliary object from its initial position to + // the position for the prism. The initial position is centered for a sphere, + // with the axis X aligned for a cylinder, and with the face of a polyhedron + // laying in the XY plane for polyhedra. + aux_T = move(-shift1) + * frame_map(x=backdir, z=base_anch_dir, reverse=true) + * move(-anch_shift) + * auxmap + * aux_to_canonical + * move(shift2); + aux_root = aux_type=="plane" || is_list(aux_type) || aux_anchor==CTR ? apply(aux_T,CTR) + : apply(aux_T * matrix_inverse(aux_to_canonical), aux_anch_pos); + + base_root = base_type=="plane" || is_list(base_type) || base_anchor==CTR ? CENTER : base_r*UP; + + prism_axis = aux_root-base_root; + + base_inside = prism_axis.z<0 ? -1 : 1; + + aux_normal = aux_type=="cyl" || aux_type=="sphere" ? apply(aux_T*matrix_inverse(aux_to_canonical), aux_anch[2]) - apply(aux_T*matrix_inverse(aux_to_canonical), CTR) + : apply(aux_T, UP) - apply(aux_T,CTR); // Is this right? Added second term + aux_inside = aux_normal*(base_root-aux_root) < 0 ? -1 : 1; + + shaft = rot(from=UP,to=prism_axis, p=zrot(base_spin,BACK)); + + obj1_back = apply(frame_map(x=backdir,z=base_anch_dir,reverse=true)*rot(from=UP,to=base_anch_dir)*zrot(base_spin),BACK); + obj2_back = aux_type=="plane" ? apply(aux_T,BACK)-apply(aux_T,CTR) + : is_list(aux_type) ? apply(aux_T*matrix_inverse(frame_map(z=aux_anch[2],x=aux_spin_dir)),aux_spin_dir)-apply(aux_T,CTR) + : aux_type=="sphere"? apply(aux_T,aux_spin_dir)-apply(aux_T,CTR) + /*cyl*/ : apply(aux_T*matrix_inverse(aux_to_canonical),aux_spin_dir)-apply(aux_T,CTR); + + v1 = vector_perp(prism_axis, shaft); + v2 = vector_perp(prism_axis, obj1_back); + v3 = vector_perp(prism_axis, obj2_back); + sign1 = cross(v1,v2)*prism_axis < 0 ? 1 : -1; + sign2 = cross(v3,v1)*prism_axis < 0 ? 1 : -1; + + spin1 = sign1 * vector_angle(v1,v2); + spin2 = -sign2 * vector_angle(v3,v1); + spin = spin_align==1 ? spin1 + : spin_align==2 ? spin2 + : spin_align==12 ? mean_angle(spin1,spin2) + : spin_align==21 ? mean_angle(spin2,spin1) + : assert(false, str("spin_align must be one of 1, 2, 12, or 21 but was ",spin_align)); + multmatrix(tobase) + move(anch_shift) + frame_map(x=backdir, z=base_anch_dir) + move(shift1){ + // For debugging spin, shows line in the spin direction + //stroke([aux_root,aux_root+unit(obj2_back)*15], width=1,color="lightblue"); + //stroke([base_root,base_root+unit(obj1_back)*15], width=1,color="lightgreen"); + + if (debug_pos) + move(base_root)rot(from=UP,to=prism_axis) + linear_extrude(height=norm(base_root-aux_root))zrot(base_spin-spin)polygon(profile); + else{ + join_prism(zrot(base_spin-spin,profile), + base=base_type, base_r=u_mul(base_r,base_inside), + aux=aux_type, aux_T=aux_T, aux_r=u_mul(aux_r,aux_inside), + scale=scale, + start=base_root, end=aux_root, + base_k=base_k, aux_k=aux_k, base_overlap=base_overlap, aux_overlap=aux_overlap, + base_n=base_n, aux_n=aux_n, base_fillet=base_fillet, aux_fillet=aux_fillet, + base_smooth_normals = base_smooth_normals, aux_smooth_normals=aux_smooth_normals, + debug=debug, + _name1="desc1", _name2="desc2") children(); + } + } } diff --git a/skin.scad b/skin.scad index a4464aea..9df6d233 100644 --- a/skin.scad +++ b/skin.scad @@ -3417,7 +3417,7 @@ function associate_vertices(polygons, split, curpoly=0) = // Topics: Textures, Knurling // Synopsis: Produce a standard texture. // Topics: Extrusion, Textures -// See Also: linear_sweep(), rotate_sweep(), heightfield(), cylindrical_heightfield() +// See Also: linear_sweep(), rotate_sweep(), cyl(), vnf_vertex_array(), sweep(), path_sweep(), textured_tile() // Usage: // tx = texture(tex, [n=], [inset=], [gap=], [roughness=]); // Description: diff --git a/utility.scad b/utility.scad index 6bb643fe..28e1c7a6 100644 --- a/utility.scad +++ b/utility.scad @@ -883,7 +883,8 @@ module deprecate(new_name) // Usage: // echo_viewport(); // Description: -// Display the current viewport parameters so that they can be pasted into examples for the wiki. +// Display the current viewport parameters so that they can be pasted into examples for the wiki. +// The viewport should have a 4x3 aspect ratio to ensure proper framing of the object. module echo_viewport() { From f3f7e222c9a18e279be52ed589f93a56025e8bbd Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Thu, 17 Apr 2025 23:19:53 -0400 Subject: [PATCH 4/6] back out rounding.scad changes that were in error and include advertised changes in shapes3d for plot_revolution --- rounding.scad | 318 +++++++++++++++++++++++--------------------------- shapes3d.scad | 167 +++++++++++++++++++++++--- 2 files changed, 298 insertions(+), 187 deletions(-) diff --git a/rounding.scad b/rounding.scad index 50bb712a..948ef18f 100644 --- a/rounding.scad +++ b/rounding.scad @@ -4537,10 +4537,6 @@ function _find_center_anchor(desc1, desc2, anchor2, flip) = -function _is_anchor(a) = is_string(a) || is_vector(a,2) || is_vector(a,3); - -function _is_anchor_list(list) = is_list(list) && [for(a=list) if (!_is_anchor(a)) a]==[]; - module prism_connector(profile, desc1, anchor1, desc2, anchor2, shift1=0, shift2=0, spin_align=1, scale=1, @@ -4551,173 +4547,155 @@ module prism_connector(profile, desc1, anchor1, desc2, anchor2, shift1=0, shift2 smooth_normals, smooth_normals1, smooth_normals2, debug=false, debug_pos=false) { - dummy1 = assert(_is_anchor(anchor1) || _is_anchor_list(anchor1) , "anchor1 must be an anchor (string or a 3-vector) or a list of anchors") - assert(_is_anchor(anchor2) || _is_anchor_list(anchor2) , "anchor2 must be an anchor (string or a 3-vector) or a list of anchors"); - if (_is_anchor_list(anchor1) || _is_anchor_list(anchor2)) { - anchor1_list=_is_anchor(anchor1) ? [anchor1] : anchor1; - anchor2_list=_is_anchor(anchor2) ? [anchor2] : anchor2; - for(i = idx(anchor1_list)) - for(j= idx(anchor2_list)) - { - $anchor1 = anchor1_list[i]; - $anchor2 = anchor2_list[j]; - $idx = [i,j]; - prism_connector(profile=profile, desc1=desc1, anchor1=$anchor1, desc2=desc2, anchor2=$anchor2, shift1=shift1, shift2=shift2, spin_align=spin_align, - scale=scale, fillet=fillet, fillet1=fillet1, fillet2=fillet2, overlap=overlap, overlap1=overlap1, overlap2=overlap2, - k=k, k1=k1, k2=k2, n=n, n1=n1, n2=n2, uniform=uniform, uniform1=uniform1, uniform2=uniform2, - smooth_normals=smooth_normals, smooth_normals1=smooth_normals1, smooth_normals2=smooth_normals2, - debug=debug, debug_pos=debug_pos) children(); + base_fillet = first_defined([fillet1,fillet,0]); + aux_fillet = first_defined([fillet2,fillet,0]); + + base_overlap = first_defined([overlap1,overlap,1]); + aux_overlap = first_defined([overlap2,overlap,1]); + + base_n = first_defined([n1,n,15]); + aux_n = first_defined([n2,n,15]); + + base_uniform = first_defined([uniform1, uniform, true]); + aux_uniform = first_defined([uniform2, uniform, true]); + + base_k = first_defined([k1,k,0.7]); + aux_k = first_defined([k2,k,0.7]); + + profile = force_path(profile,"profile"); + dummy0 = assert(is_path(profile,2), "profile must be a 2d path"); + + corrected_base_anchor = is_vector(anchor1) && norm(anchor1)==0 ? _find_center_anchor(desc1,desc2,anchor2,true) : undef; + corrected_aux_anchor = is_vector(anchor2) && norm(anchor2)==0 ? _find_center_anchor(desc2,desc1,anchor1,false) : undef; + + + base_anchor=is_string(anchor1) ? anchor1 + : is_def(corrected_base_anchor) ? corrected_base_anchor[0] + : point3d(anchor1); + aux_anchor=is_string(anchor2) ? anchor2 + : is_def(corrected_aux_anchor) ? corrected_aux_anchor[0] + : point3d(anchor2); + + base=desc1; + aux=desc2; + + current = parent(); + tobase = current==base ? ident(4) : linear_solve(current[0],base[0]); // Maps from current frame into the base frame + auxmap = linear_solve(base[0], aux[0]); // Map from the base (desc1) coordinate frame into the aux (desc2) coordinate frame + + dummy = assert(is_vector(base_anchor) || is_string(base_anchor), "anchor1 must be a string or a 3-vector") + assert(is_vector(aux_anchor) || is_string(aux_anchor), "anchor2 must be a string or a 3-vector") + assert(is_rotation(auxmap), "desc1 and desc2 are not related to each other by a rotation (and translation)"); + + base_type = _get_obj_type(1,base[1],base_anchor,profile); + base_axis = base_type=="cyl" ? base[1][5] : RIGHT; + base_edge = _is_geom_an_edge(base[1],base_anchor); + base_r = in_list(base_type,["cyl","sphere"]) ? base[1][1] : 1; + base_anch = _find_anchor(base_anchor, base[1]); + base_spin = base_anch[3]; + base_anch_pos = base_anch[1]; + base_anch_dir = base_anch[2]; + + prelim_shift1 = _check_join_shift(1,base_type,shift1,true); + shift1 = corrected_base_anchor ? corrected_base_anchor[1] + prelim_shift1 : prelim_shift1; + aux_type = _get_obj_type(2,aux[1],aux_anchor,profile); + aux_anch = _find_anchor(aux_anchor, aux[1]); + aux_edge = _is_geom_an_edge(aux[1],aux_anchor); + aux_r = in_list(aux_type,["cyl","sphere"]) ? aux[1][1] : 1; + aux_anch_pos = aux_anch[1]; + aux_anch_dir = aux_anch[2]; + aux_spin = aux_anch[3]; + aux_spin_dir = apply(rot(from=UP,to=aux_anch[2])*zrot(aux_spin),BACK); + aux_axis = aux_type=="cyl" ? aux[1][5] : RIGHT; + prelim_shift2 = _check_join_shift(2,aux_type,shift2,false); + shift2 = corrected_aux_anchor ? corrected_aux_anchor[1] + prelim_shift2 : prelim_shift2; + + + base_smooth_normals = first_defined([smooth_normals1, smooth_normals,!base_edge]); + aux_smooth_normals = first_defined([smooth_normals2, smooth_normals,!aux_edge]); + + backdir = base_type=="cyl" ? base_axis + : apply(rot(from=UP,to=base_anch_dir)*zrot(base_spin),BACK); + anch_shift = base_type=="plane" || is_list(base_type) ? base_anch_pos : CENTER; + + + // Map from the starting position for a join_prism object to + // the standard position for the object. + // If the aux object is an edge (type is a list) then the starting position + // of the prism is with the edge lying on the X axis and the object below. + aux_to_canonical = aux_type=="sphere" ? IDENT + : aux_type=="cyl" ? frame_map(x=aux_axis, z=aux_anch[2]) + : aux_type=="plane" ? move(aux_anch_pos) * rot(from=UP, to=aux_anch[2])*zrot(aux_spin) + : /* list */ move(aux_anch_pos) * frame_map(z=aux_anch[2],x=aux_spin_dir) ; + + // aux_T is the map that maps the auxiliary object from its initial position to + // the position for the prism. The initial position is centered for a sphere, + // with the axis X aligned for a cylinder, and with the face of a polyhedron + // laying in the XY plane for polyhedra. + aux_T = move(-shift1) + * frame_map(x=backdir, z=base_anch_dir, reverse=true) + * move(-anch_shift) + * auxmap + * aux_to_canonical + * move(shift2); + aux_root = aux_type=="plane" || is_list(aux_type) || aux_anchor==CTR ? apply(aux_T,CTR) + : apply(aux_T * matrix_inverse(aux_to_canonical), aux_anch_pos); + + base_root = base_type=="plane" || is_list(base_type) || base_anchor==CTR ? CENTER : base_r*UP; + + prism_axis = aux_root-base_root; + + base_inside = prism_axis.z<0 ? -1 : 1; + + aux_normal = aux_type=="cyl" || aux_type=="sphere" ? apply(aux_T*matrix_inverse(aux_to_canonical), aux_anch[2]) - apply(aux_T*matrix_inverse(aux_to_canonical), CTR) + : apply(aux_T, UP) - apply(aux_T,CTR); // Is this right? Added second term + aux_inside = aux_normal*(base_root-aux_root) < 0 ? -1 : 1; + + shaft = rot(from=UP,to=prism_axis, p=zrot(base_spin,BACK)); + + obj1_back = apply(frame_map(x=backdir,z=base_anch_dir,reverse=true)*rot(from=UP,to=base_anch_dir)*zrot(base_spin),BACK); + obj2_back = aux_type=="plane" ? apply(aux_T,BACK)-apply(aux_T,CTR) + : is_list(aux_type) ? apply(aux_T*matrix_inverse(frame_map(z=aux_anch[2],x=aux_spin_dir)),aux_spin_dir)-apply(aux_T,CTR) + : aux_type=="sphere"? apply(aux_T,aux_spin_dir)-apply(aux_T,CTR) + /*cyl*/ : apply(aux_T*matrix_inverse(aux_to_canonical),aux_spin_dir)-apply(aux_T,CTR); + + v1 = vector_perp(prism_axis, shaft); + v2 = vector_perp(prism_axis, obj1_back); + v3 = vector_perp(prism_axis, obj2_back); + sign1 = cross(v1,v2)*prism_axis < 0 ? 1 : -1; + sign2 = cross(v3,v1)*prism_axis < 0 ? 1 : -1; + + spin1 = sign1 * vector_angle(v1,v2); + spin2 = -sign2 * vector_angle(v3,v1); + spin = spin_align==1 ? spin1 + : spin_align==2 ? spin2 + : spin_align==12 ? mean_angle(spin1,spin2) + : spin_align==21 ? mean_angle(spin2,spin1) + : assert(false, str("spin_align must be one of 1, 2, 12, or 21 but was ",spin_align)); + multmatrix(tobase) + move(anch_shift) + frame_map(x=backdir, z=base_anch_dir) + move(shift1){ + // For debugging spin, shows line in the spin direction + //stroke([aux_root,aux_root+unit(obj2_back)*15], width=1,color="lightblue"); + //stroke([base_root,base_root+unit(obj1_back)*15], width=1,color="lightgreen"); + + if (debug_pos) + move(base_root)rot(from=UP,to=prism_axis) + linear_extrude(height=norm(base_root-aux_root))zrot(base_spin-spin)polygon(profile); + else{ + join_prism(zrot(base_spin-spin,profile), + base=base_type, base_r=u_mul(base_r,base_inside), + aux=aux_type, aux_T=aux_T, aux_r=u_mul(aux_r,aux_inside), + scale=scale, + start=base_root, end=aux_root, + base_k=base_k, aux_k=aux_k, base_overlap=base_overlap, aux_overlap=aux_overlap, + base_n=base_n, aux_n=aux_n, base_fillet=base_fillet, aux_fillet=aux_fillet, + base_smooth_normals = base_smooth_normals, aux_smooth_normals=aux_smooth_normals, + debug=debug, + _name1="desc1", _name2="desc2") children(); } - } - else { - base_fillet = first_defined([fillet1,fillet,0]); - aux_fillet = first_defined([fillet2,fillet,0]); - - base_overlap = first_defined([overlap1,overlap,1]); - aux_overlap = first_defined([overlap2,overlap,1]); - - base_n = first_defined([n1,n,15]); - aux_n = first_defined([n2,n,15]); - - base_uniform = first_defined([uniform1, uniform, true]); - aux_uniform = first_defined([uniform2, uniform, true]); - - base_k = first_defined([k1,k,0.7]); - aux_k = first_defined([k2,k,0.7]); - - profile = force_path(profile,"profile"); - dummy0 = assert(is_path(profile,2), "profile must be a 2d path"); - - corrected_base_anchor = is_vector(anchor1) && norm(anchor1)==0 ? _find_center_anchor(desc1,desc2,anchor2,true) : undef; - corrected_aux_anchor = is_vector(anchor2) && norm(anchor2)==0 ? _find_center_anchor(desc2,desc1,anchor1,false) : undef; - - - base_anchor=is_string(anchor1) ? anchor1 - : is_def(corrected_base_anchor) ? corrected_base_anchor[0] - : point3d(anchor1); - aux_anchor=is_string(anchor2) ? anchor2 - : is_def(corrected_aux_anchor) ? corrected_aux_anchor[0] - : point3d(anchor2); - - base=desc1; - aux=desc2; - - current = parent(); - tobase = current==base ? ident(4) : linear_solve(current[0],base[0]); // Maps from current frame into the base frame - auxmap = linear_solve(base[0], aux[0]); // Map from the base (desc1) coordinate frame into the aux (desc2) coordinate frame - - dummy = - assert(is_rotation(auxmap), "desc1 and desc2 are not related to each other by a rotation (and translation)"); - - base_type = _get_obj_type(1,base[1],base_anchor,profile); - base_axis = base_type=="cyl" ? base[1][5] : RIGHT; - base_edge = _is_geom_an_edge(base[1],base_anchor); - base_r = in_list(base_type,["cyl","sphere"]) ? base[1][1] : 1; - base_anch = _find_anchor(base_anchor, base[1]); - base_spin = base_anch[3]; - base_anch_pos = base_anch[1]; - base_anch_dir = base_anch[2]; - - prelim_shift1 = _check_join_shift(1,base_type,shift1,true); - shift1 = corrected_base_anchor ? corrected_base_anchor[1] + prelim_shift1 : prelim_shift1; - aux_type = _get_obj_type(2,aux[1],aux_anchor,profile); - aux_anch = _find_anchor(aux_anchor, aux[1]); - aux_edge = _is_geom_an_edge(aux[1],aux_anchor); - aux_r = in_list(aux_type,["cyl","sphere"]) ? aux[1][1] : 1; - aux_anch_pos = aux_anch[1]; - aux_anch_dir = aux_anch[2]; - aux_spin = aux_anch[3]; - aux_spin_dir = apply(rot(from=UP,to=aux_anch[2])*zrot(aux_spin),BACK); - aux_axis = aux_type=="cyl" ? aux[1][5] : RIGHT; - prelim_shift2 = _check_join_shift(2,aux_type,shift2,false); - shift2 = corrected_aux_anchor ? corrected_aux_anchor[1] + prelim_shift2 : prelim_shift2; - - - base_smooth_normals = first_defined([smooth_normals1, smooth_normals,!base_edge]); - aux_smooth_normals = first_defined([smooth_normals2, smooth_normals,!aux_edge]); - - backdir = base_type=="cyl" ? base_axis - : apply(rot(from=UP,to=base_anch_dir)*zrot(base_spin),BACK); - anch_shift = base_type=="plane" || is_list(base_type) ? base_anch_pos : CENTER; - - - // Map from the starting position for a join_prism object to - // the standard position for the object. - // If the aux object is an edge (type is a list) then the starting position - // of the prism is with the edge lying on the X axis and the object below. - aux_to_canonical = aux_type=="sphere" ? IDENT - : aux_type=="cyl" ? frame_map(x=aux_axis, z=aux_anch[2]) - : aux_type=="plane" ? move(aux_anch_pos) * rot(from=UP, to=aux_anch[2])*zrot(aux_spin) - : /* list */ move(aux_anch_pos) * frame_map(z=aux_anch[2],x=aux_spin_dir) ; - - // aux_T is the map that maps the auxiliary object from its initial position to - // the position for the prism. The initial position is centered for a sphere, - // with the axis X aligned for a cylinder, and with the face of a polyhedron - // laying in the XY plane for polyhedra. - aux_T = move(-shift1) - * frame_map(x=backdir, z=base_anch_dir, reverse=true) - * move(-anch_shift) - * auxmap - * aux_to_canonical - * move(shift2); - aux_root = aux_type=="plane" || is_list(aux_type) || aux_anchor==CTR ? apply(aux_T,CTR) - : apply(aux_T * matrix_inverse(aux_to_canonical), aux_anch_pos); - - base_root = base_type=="plane" || is_list(base_type) || base_anchor==CTR ? CENTER : base_r*UP; - - prism_axis = aux_root-base_root; - - base_inside = prism_axis.z<0 ? -1 : 1; - - aux_normal = aux_type=="cyl" || aux_type=="sphere" ? apply(aux_T*matrix_inverse(aux_to_canonical), aux_anch[2]) - apply(aux_T*matrix_inverse(aux_to_canonical), CTR) - : apply(aux_T, UP) - apply(aux_T,CTR); // Is this right? Added second term - aux_inside = aux_normal*(base_root-aux_root) < 0 ? -1 : 1; - - shaft = rot(from=UP,to=prism_axis, p=zrot(base_spin,BACK)); - - obj1_back = apply(frame_map(x=backdir,z=base_anch_dir,reverse=true)*rot(from=UP,to=base_anch_dir)*zrot(base_spin),BACK); - obj2_back = aux_type=="plane" ? apply(aux_T,BACK)-apply(aux_T,CTR) - : is_list(aux_type) ? apply(aux_T*matrix_inverse(frame_map(z=aux_anch[2],x=aux_spin_dir)),aux_spin_dir)-apply(aux_T,CTR) - : aux_type=="sphere"? apply(aux_T,aux_spin_dir)-apply(aux_T,CTR) - /*cyl*/ : apply(aux_T*matrix_inverse(aux_to_canonical),aux_spin_dir)-apply(aux_T,CTR); - - v1 = vector_perp(prism_axis, shaft); - v2 = vector_perp(prism_axis, obj1_back); - v3 = vector_perp(prism_axis, obj2_back); - sign1 = cross(v1,v2)*prism_axis < 0 ? 1 : -1; - sign2 = cross(v3,v1)*prism_axis < 0 ? 1 : -1; - - spin1 = sign1 * vector_angle(v1,v2); - spin2 = -sign2 * vector_angle(v3,v1); - spin = spin_align==1 ? spin1 - : spin_align==2 ? spin2 - : spin_align==12 ? mean_angle(spin1,spin2) - : spin_align==21 ? mean_angle(spin2,spin1) - : assert(false, str("spin_align must be one of 1, 2, 12, or 21 but was ",spin_align)); - multmatrix(tobase) - move(anch_shift) - frame_map(x=backdir, z=base_anch_dir) - move(shift1){ - // For debugging spin, shows line in the spin direction - //stroke([aux_root,aux_root+unit(obj2_back)*15], width=1,color="lightblue"); - //stroke([base_root,base_root+unit(obj1_back)*15], width=1,color="lightgreen"); - - if (debug_pos) - move(base_root)rot(from=UP,to=prism_axis) - linear_extrude(height=norm(base_root-aux_root))zrot(base_spin-spin)polygon(profile); - else{ - join_prism(zrot(base_spin-spin,profile), - base=base_type, base_r=u_mul(base_r,base_inside), - aux=aux_type, aux_T=aux_T, aux_r=u_mul(aux_r,aux_inside), - scale=scale, - start=base_root, end=aux_root, - base_k=base_k, aux_k=aux_k, base_overlap=base_overlap, aux_overlap=aux_overlap, - base_n=base_n, aux_n=aux_n, base_fillet=base_fillet, aux_fillet=aux_fillet, - base_smooth_normals = base_smooth_normals, aux_smooth_normals=aux_smooth_normals, - debug=debug, - _name1="desc1", _name2="desc2") children(); - } - } + } } diff --git a/shapes3d.scad b/shapes3d.scad index 4df49f3e..ef4f2fe3 100644 --- a/shapes3d.scad +++ b/shapes3d.scad @@ -4218,11 +4218,11 @@ module fillet(l, r, ang, r1, r2, excess=0.01, d1, d2,d,length, h, height, anchor // Synopsis: Generates a surface by evaluating a function on a 2D grid // SynTags: Geom, VNF // Topics: Function Plotting -// See Also: cylindrical_heightfield() +// See Also: plot_revolution() // Usage: As Module -// plot3d(f, xrange, yrange, [zclip=], [zspan=], [base=], [convexity=], [style=]) [ATTACHMENTS]; +// plot3d(f, x, y, [zclip=], [zspan=], [base=], [convexity=], [style=]) [ATTACHMENTS]; // Usage: As Function -// vnf = plot3d(f, xrange, yrange, [zclip=], [zspan=], [base=], [style=]); +// vnf = plot3d(f, x, y, [zclip=], [zspan=], [base=], [style=]); // Description: // Given a function literal taking 2 parameters and a 2d grid, generate a surface where the height at any point is // the value of the function. You can specify the grid using a range or using a list of points that @@ -4241,11 +4241,11 @@ module fillet(l, r, ang, r1, r2, excess=0.01, d1, d2,d,length, h, height, anchor // in `zspan`. // Arguments: // f = function literal accepting two arguments (x and y) that defines the function to compute -// xrange = A list or range of values for x -// yrange = A list or range of values for y +// x = A list or range of values for x +// y = A list or range of values for y // --- -// zclip = Constrain the function to these bounds. -// zspan = Rescale and shift the function values so the minimum value of f appears at zspan[0] and the maximum at zspan[1]. +// zclip = A vector `[zmin,zmax]' that constrains the output of function to these bounds. Cannot be used with `zspan`. +// zspan = Rescale and shift the function values so the minimum value of f appears at zspan[0] and the maximum at zspan[1]. Cannot be used with `zclip`. // base = Amount of extra thickness to add at the bottom of the model. If set to zero, produce a non-manifold zero-thickness VNF. Default: 1 // style = {{vnf_vertex_array()}} style used to triangulate heightfield textures. Default: "default" // convexity = Max number of times a line could intersect a wall of the surface being formed. Module only. Default: 10 @@ -4280,22 +4280,22 @@ module fillet(l, r, ang, r1, r2, excess=0.01, d1, d2,d,length, h, height, anchor // f = function(x,y) 10*(sin(20*x)^2+cos(20*y)^2)/norm([2*x,y]); // plot3d(f, [10:.3:40], [4:.3:37],zspan=[0,25],anchor=BOT); -module plot3d(f,xrange,yrange,zclip, zspan, base=1, anchor="origin", orient=UP, spin=0, atype="hull", cp="box", convexity=4, style="default") - vnf_polyhedron(plot3d(f,xrange,yrange,zclip, zspan,base, style=style), atype=atype, orient=orient, anchor=anchor, cp=cp, convexity=convexity) children(); +module plot3d(f,x,y,zclip, zspan, base=1, anchor="origin", orient=UP, spin=0, atype="hull", cp="box", convexity=4, style="default") + vnf_polyhedron(plot3d(f,x,y,zclip, zspan,base, style=style), atype=atype, orient=orient, anchor=anchor, cp=cp, convexity=convexity) children(); -function plot3d(f,xrange,yrange,zclip, zspan, base=1, anchor="origin", orient=UP, spin=0, atype="hull", cp="box", style="default") = +function plot3d(f,x,y,zclip, zspan, base=1, anchor="origin", orient=UP, spin=0, atype="hull", cp="box", style="default") = assert(is_finite(base) && base>=0, "base must be a nonnegative number") - assert(is_vector(xrange) || valid_range(xrange), "xrange must be a vector or nonempty range") - assert(is_vector(yrange) || valid_range(yrange), "yrange must be a vector or nonempty range") - assert(is_range(xrange) || is_increasing(xrange, strict=true), "xrange must be strictly increasing") - assert(is_range(yrange) || is_increasing(yrange, strict=true), "yrange must be strictly increasing") + assert(is_vector(x) || valid_range(x), "x must be a vector or nonempty range") + assert(is_vector(y) || valid_range(y), "y must be a vector or nonempty range") + assert(is_range(x) || is_increasing(x, strict=true), "x must be strictly increasing") + assert(is_range(y) || is_increasing(y, strict=true), "y must be strictly increasing") assert(num_defined([zclip,zspan])<2, "Cannot give both zclip and zspan") assert(is_undef(zclip) || (is_list(zclip) && len(zclip)==2 && is_num(zclip[0]) && is_num(zclip[1])), "zclip must be a list of two values (which may be infinite)") assert(is_undef(zspan) || (is_vector(zspan,2) && zspan[0]1 && len(data)>1, "xrange and yrange must both provide at least 2 points"), + data = [for(x=x) [for(y=y) [x,y,min(max(f(x,y),zclip[0]),zclip[1])]]], + dummy=assert(len(data[0])>1 && len(data)>1, "x and y must both provide at least 2 points"), minval = min(column(flatten(data),2)), maxval = max(column(flatten(data),2)), sdata = is_undef(zspan) ? data @@ -4320,6 +4320,140 @@ function plot3d(f,xrange,yrange,zclip, zspan, base=1, anchor="origin", orient=UP +// Function&Module: plot_revolution() +// Synopsis: Generates a surface by evaluating a of z and theta and putting the result on a surface of revolution +// SynTags: Geom, VNF +// Topics: Function Plotting +// See Also: plot3d() +// Usage: To create a cylinder or cone (by angle) +// plot_revolution(f, angle, z, [r=/d=] [r1=/d1], [r2=/d2=], [rclip=], [rspan=], [horiz=], [style=], [convexity=], ...) [ATTACHMENTS]; +// Usage: To create a cylinder or cone (by arclength) +// plot_revolution(f, arclength=, z=, [r=/d=] [r1=/d1], [r2=/d2=], [rclip=], [rspan=], [horiz=], [style=], [convexity=], ...) [ATTACHMENTS]; +// Usage: To create a surface of revolution +// plot_revolution(f, [angle], [arclength=], path=, [rclip=], [rspan=], [horiz=], [style=], [convexity=], ...) [ATTACHMENTS]; +// Usage: As Function +// vnf = plot_revolution(...); +// Description: +// Given a function literal, `f`, sets `r=f(theta,z)` over a range of theta and z values, and uses the +// computed r values to define the offset from a cylinder or surface of revolution. You can specify +// the theta range as an angle or based on arc length. If the computed value produces a radius smaller +// than zero it will be rounded up to 0.01. You can specify the cylinder using the usual length and +// radius or diameter parameters, or you can give `path`, a path which whose x values are strictly positive +// to define the textured surface of revolution. +// . +// Your function may have have excessively large values at some points, or you may not know exactly +// what its extreme values are. To manage these situations you can use either the `rclip` or `rspan` +// parameter (but not both). The `rclip` parameter is a 2-vector giving a minimum and maximum +// value, either of which can be infinite. If the function falls below the minimum it is set +// equal to the minimum, and if it rises above the maximum it is set equal to the maximum. The +// `rspan` parameter is a 2-vector giving a minum and maximum value which must both be finite. +// The function's values will be scaled and shifted to exactly cover the range you specifiy +// in `rspan`. +// . +// The default is to erect the function normal to the surface. You can also set `horiz=true` to +// erect the function perpendicular to the rotation axis. In the former case, the caps of the +// model are likely to be irregularly shaped and not exactly the requested size, unless the function +// evaluates to zero at the top and bottom of the path. When `horiz=true` the top and bottom will +// be flat. +// Arguments: +// f = function literal accepting two arguments (x and y) that defines the function to compute +// --- +// r / d = radius or diameter of cylinder +// r1 / d1 = radius or diameter of bottom end +// r2 / d2 = radius or diameter of top end +// path = path to revolve to produce the shape +// rclip = A vector `[rmin,rmax]' that constrains the output of function to these bounds. Cannot be used with `rspan`. +// rspan = Rescale and shift the function values so the minimum value of f appears at rspan[0] and the maximum at rspan[1]. Cannot be used with `rclip`. +// style = {{vnf_vertex_array()}} style used to triangulate heightfield textures. Default: "default" +// convexity = Max number of times a line could intersect a wall of the surface being formed. Module only. Default: 10 +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#subsection-spin). Default: `0` +// orient = Vector to rotate top towards. See [orient](attachments.scad#subsection-orient). Default: `UP` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` +// orient = Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP` +// atype = Select "hull" or "intersect" anchor type. Default: "hull" +// cp = Centerpoint for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" +// Anchor Types: +// "hull" = Anchors to the virtual convex hull of the shape. +// "intersect" = Anchors to the surface of the shape. +// Named Anchors: +// "origin" = Anchor at the origin, oriented UP. +// Example(3D,NoScale): +// f = function (x,y) 5*cos(5*norm([x*180/50,y*180/50]))+5; +// plot_revolution(f, arclength=[-50:1:50], z=[-50:1:50], r=30); +// Example(3D,NoScale): When specifying angle, the pattern shrinks at the top of the cone. +// g = function (x,y) 5*sin(4*x)*cos(6*y)+5; +// plot_revolution(g, z=[-60:2:60], angle=[-180:4:180], r1=30, r2=16); +// Example(3D,NoScale): When specifying arc length, the shape wraps around more cone at the top +// g = function (x,y) 5*sin(8*x)*cos(6*y)+5; +// plot_revolution(g, z=[-60:.5:60], arclength=[-45:.5:45], r1=30,r2=16); + +module plot_revolution(f,angle,z,arclength, path, rclip, rspan, horiz=false,r1,r2,r,d1,d2,d,convexity=4, + anchor="origin", orient=UP, spin=0, atype="hull", cp="centroid", style="min_edge") + vnf_polyhedron(plot_revolution(f=f,angle=angle,z=z,arclength=arclength,path=path, rclip=rclip, rspan=rspan, horiz=horiz, style=style, + r=r,d=d,r1=r1,d1=d1,r2=r2,d2=d2), anchor=anchor, orient=orient, spin=spin, atype=atype, cp=cp); + + +function plot_revolution(f,angle,z,arclength, path, rclip, rspan, horiz=false,r1,r2,r,d1,d2,d, + anchor="origin", orient=UP, spin=0, atype="hull", cp="centroid", style="min_edge") = + assert(num_defined([angle,arclength])==1, "must define exactly one of angle and arclength") + assert(is_vector(z) || valid_range(z), "z must be a vector or nonempty range") + assert(is_range(z) || is_increasing(z, strict=true), "z must be strictly increasing") + assert(is_undef(path) || num_defined([r1,r2,d1,d2,r,d,z])==0, "Cannot define the z parameter or any radius or diameter parameters in combination with path") + assert(num_defined([rclip,rspan])<2, "Cannot give both rclip and rspan") + assert(is_undef(rclip) || (is_list(rclip) && len(rclip)==2 && is_finite(rclip[0]) && rclip[0]>0 && is_num(rclip[1])), + "rclip must be a list of two values (r[1] may be infinite)") + assert(is_undef(rspan) || (is_vector(rspan,2) && rspan[0]>0 && rspan[0]1 && is_increasing(thetarange,strict=true), + "angle/arclength must be a strictly increasing array or range with at least 2 elements") + assert(is_def(arclength) || (last(thetarange)-thetarange[0])<=360, "angle span exceeds 360 degrees"), + path = is_def(path) ? path + : let( + rvals = add_scalar(add_scalar(z,-z[0]) / (last(z)-z[0]) * (r2-r1) ,r1) + ) + hstack([rvals,z]), + normals = horiz ? repeat([1,0], len(path)) + : path_normals(path), + rclip = default(rclip, [0.01,INF]), + rdata = [for(pt=path) + let( + angscale = is_def(angle) ? 1 : 360/2/PI/pt.x + ) + [for(theta=thetarange) min(max(f(theta /*angscale*/,pt.y),rclip[0]),rclip[1])]], + dummy2=assert(len(rdata[0])>1 && len(rdata)>1, "xrange and yrange must both provide at least 2 points"), + minval = min(flatten(rdata)), + maxval = max(flatten(rdata)), + sdata = is_undef(rspan) && minval>0 ? rdata + : is_undef(rspan) ? [for(row=rdata) [for (entry=row) max(rmin,entry)]] + : let( + scale = (rspan[1]-rspan[0])/(maxval-minval) + ) + [for(row=rdata) [for (entry=row) scale*(entry.z-minval)+rspan[0]]], + closed = is_def(angle) && last(thetarange)-thetarange[0]==360, + final = [for(i=idx(path)) + let( + angscale = is_def(angle) ? 1 + : 360/2/PI/path[i].x + + ) + assert(angscale*(last(thetarange)-thetarange[0])<=360, str("arclength span is more than 360 degrees at profile index ",i," with radius ",path[i].x)) + [ + if (!closed) [0,0,path[i].y], + for(j=idx(sdata[0])) + cylindrical_to_xyz(path[i].x+sdata[i][j]*normals[i].x, angscale*thetarange[j], path[i].y+sdata[i][j]*normals[i].y) + ] + ] + ) + vnf_vertex_array(final, col_wrap=true, caps=true); + + + /// Function&Module: heightfield() /// Synopsis: Generates a 3D surface from a 2D grid of values. /// SynTags: Geom, VNF @@ -4461,7 +4595,6 @@ function heightfield(data, size=[100,100], bottom=-20, maxz=100, xrange=[-1:0.04 // Synopsis: Generates a cylindrical 3d surface from a 2D grid of values. // SynTags: Geom, VNF // Topics: Extrusion, Textures, Knurling, Heightfield -// See Also: heightfield() // Usage: As Function // vnf = cylindrical_heightfield(data, l|length=|h=|height=, r|d=, [base=], [transpose=], [aspect=]); // Usage: As Module From 7aadbcff13cf4f3d105718d132a0c1764e52c369 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Fri, 18 Apr 2025 06:15:59 -0400 Subject: [PATCH 5/6] doc fixes --- miscellaneous.scad | 2 +- rounding.scad | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/miscellaneous.scad b/miscellaneous.scad index 8a59e77a..fbcc1178 100644 --- a/miscellaneous.scad +++ b/miscellaneous.scad @@ -246,7 +246,7 @@ module path_extrude(path, convexity=10, clipsize=100) { // Synopsis: Extrudes 2D children outwards around a cylinder. // SynTags: Geom // Topics: Miscellaneous, Extrusion, Rotation -// See Also: heightfield(), cylindrical_heightfield(), cyl() +// See Also: cyl(), plot_revolution() // Usage: // cylindrical_extrude(ir|id=, or|od=, [size=], [convexity=], [spin=], [orient=]) 2D-CHILDREN; // Description: diff --git a/rounding.scad b/rounding.scad index 948ef18f..33046d22 100644 --- a/rounding.scad +++ b/rounding.scad @@ -2580,8 +2580,8 @@ function _circle_mask(r) = // around the curve of the cylinder. The angular span of the path on the cylinder must be somewhat // less than 180 degrees, and the path shouldn't have closely spaced points at concave points of high curvature because // this causes self-intersection in the mask polyhedron, resulting in CGAL failures. The path also cannot include -// sharp corners, because it internally uses {{offset()}} which will expand those sharp corners into very long single -// segments that produce incorrect result. +// sharp corners, because it internally uses {{offset()}} which expands those sharp corners into long single +// segments that produce incorrect results. // Arguments: // r / radius = center radius of the cylindrical shell to cut a hole in // thickness = thickness of cylindrical shell (may need to be slighly oversized) From 03a47d2b6d1e8b92086765d94ecaa50421884e64 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Fri, 18 Apr 2025 16:55:11 -0400 Subject: [PATCH 6/6] bugfixes and a few new examples --- rounding.scad | 4 +- shapes3d.scad | 203 ++++++++++++++++++++++++++++---------------------- skin.scad | 2 - utility.scad | 2 +- 4 files changed, 119 insertions(+), 92 deletions(-) diff --git a/rounding.scad b/rounding.scad index 33046d22..9f90f578 100644 --- a/rounding.scad +++ b/rounding.scad @@ -2580,8 +2580,8 @@ function _circle_mask(r) = // around the curve of the cylinder. The angular span of the path on the cylinder must be somewhat // less than 180 degrees, and the path shouldn't have closely spaced points at concave points of high curvature because // this causes self-intersection in the mask polyhedron, resulting in CGAL failures. The path also cannot include -// sharp corners, because it internally uses {{offset()}} which expands those sharp corners into long single -// segments that produce incorrect results. +// sharp corners: construction of the mask requires the use of {{offset()}}, which expands sharp corners into long single +// segments leading to incorrect results. // Arguments: // r / radius = center radius of the cylindrical shell to cut a hole in // thickness = thickness of cylindrical shell (may need to be slighly oversized) diff --git a/shapes3d.scad b/shapes3d.scad index ef4f2fe3..745d5dda 100644 --- a/shapes3d.scad +++ b/shapes3d.scad @@ -4336,8 +4336,15 @@ function plot3d(f,x,y,zclip, zspan, base=1, anchor="origin", orient=UP, spin=0, // Description: // Given a function literal, `f`, sets `r=f(theta,z)` over a range of theta and z values, and uses the // computed r values to define the offset from a cylinder or surface of revolution. You can specify -// the theta range as an angle or based on arc length. If the computed value produces a radius smaller -// than zero it will be rounded up to 0.01. You can specify the cylinder using the usual length and +// the theta range as a `angle` to give an angle range in degrees or with `arclength` to give an arc length +// range in distance units. Your function will receive its parameters in +// the form you specify, as angle or as arclength. If you use `angle` then as the radius decreases, the +// function shrinks in the horizontal direction to fit. If you use `arclength` distance is preserved for +// the function and as you move toward the top of a cone, the function will occupy a larger amount +// of total angle so that the arc length stays the same. +// . +// If the computed value produces a radius smaller than zero it will be rounded up to 0.01. You can +// specify a cylinder using the usual length and // radius or diameter parameters, or you can give `path`, a path which whose x values are strictly positive // to define the textured surface of revolution. // . @@ -4356,13 +4363,16 @@ function plot3d(f,x,y,zclip, zspan, base=1, anchor="origin", orient=UP, spin=0, // evaluates to zero at the top and bottom of the path. When `horiz=true` the top and bottom will // be flat. // Arguments: -// f = function literal accepting two arguments (x and y) that defines the function to compute +// f = function literal accepting two arguments (angle and z) that defines the function to compute +// angle = a list or range of angle values where the function is calculated +// z = a list or range of z values to where the function is calculated, used only with cylinders and cones, not allowed with `path`. // --- -// r / d = radius or diameter of cylinder -// r1 / d1 = radius or diameter of bottom end -// r2 / d2 = radius or diameter of top end -// path = path to revolve to produce the shape -// rclip = A vector `[rmin,rmax]' that constrains the output of function to these bounds. Cannot be used with `rspan`. +// r / d = radius or diameter of cylinder (not allowed with `path`) +// r1 / d1 = radius or diameter of bottom end (not allowed with `path`) +// r2 / d2 = radius or diameter of top end (not allowed with `path`) +// arclength = list or range of arc length values where the function is calculated +// path = path to revolve to produce the shape. (If omitted you must supply cylinder parameters.) +// rclip = A vector `[rmin,rmax]' that constrains the output of function to these bounds, which may be infinite. Cannot be used with `rspan`. // rspan = Rescale and shift the function values so the minimum value of f appears at rspan[0] and the maximum at rspan[1]. Cannot be used with `rclip`. // style = {{vnf_vertex_array()}} style used to triangulate heightfield textures. Default: "default" // convexity = Max number of times a line could intersect a wall of the surface being formed. Module only. Default: 10 @@ -4386,19 +4396,36 @@ function plot3d(f,x,y,zclip, zspan, base=1, anchor="origin", orient=UP, spin=0, // plot_revolution(g, z=[-60:2:60], angle=[-180:4:180], r1=30, r2=16); // Example(3D,NoScale): When specifying arc length, the shape wraps around more cone at the top // g = function (x,y) 5*sin(8*x)*cos(6*y)+5; -// plot_revolution(g, z=[-60:.5:60], arclength=[-45:.5:45], r1=30,r2=16); +// plot_revolution(g, z=[-60:.5:60], arclength=[-45:.5:45],r1=30,r2=16); +// Example(3D,VPR=[60.60,0.00,100.60],VPD=100.87,VPT=[-1.84,-1.70,5.63]): Here we place a simple ridge function onto a cone using angle. Note how the ribs narrow with the radius. +// f = function(x,y) cos(20*x)+1; +// plot_revolution(f,z=[0:.1:20], angle=[-45:.1:45], r1=20,r2=10, horiz=true); +// cyl(h=20, r1=20,r2=10,anchor=BOT,$fn=64); +// Example(3D,VPR=[60.60,0.00,100.60],VPD=100.87,VPT=[-1.84,-1.70,5.63]): Here using arc length to put the function on the cone results in relatively straight ridges that do not narrow at the top of the cone. Note that we had to adjust the function to be properly scaled for the arc length parameter instead of angle. +// f = function(x,y) cos(60*x)+1; +// plot_revolution(f,z=[0:.1:20], arclength=[-15:.1:15], r1=20,r2=10, horiz=true); +// cyl(h=20, r1=20,r2=10,anchor=BOT,$fn=64); +// Example(3D,VPR=[57.10,0.00,148.90],VPD=100.87,VPT=[-1.40,-0.72,4.63]): Changing the arc length range position changes how the function maps onto the surface. +// f = function(x,y) cos(60*x)+1; +// plot_revolution(f,z=[0:.1:20], arclength=[0:.1:30], r1=20,r2=10, horiz=true); +// cyl(h=20, r1=20,r2=10,anchor=BOT,$fn=64); +// Example(3D,Med,NoAxes,VPR=[73.90,0.00,17.30],VPD=124.53,VPT=[-10.15,31.37,-9.82]): Here we construct a model using a circular arc for the path, resulting in a spherical shape. The left model has `horiz=false` and the right hand one has `horiz=true`. +// hcount=4; // Number of ribs to create +// vcount=2; // How periods of oscillation for each rib +// stretch_ang=200; // Angle extent of oscillations +// g = function(x,y) sin(hcount * x + stretch_ang * sin(18 * vcount * y)); +// xcopies(spacing=30) +// plot_revolution(g, [0:3:360], path=arc(200, r=10, start=-89, angle=178),style="min_edge", horiz=$idx==1); -module plot_revolution(f,angle,z,arclength, path, rclip, rspan, horiz=false,r1,r2,r,d1,d2,d,convexity=4, - anchor="origin", orient=UP, spin=0, atype="hull", cp="centroid", style="min_edge") - vnf_polyhedron(plot_revolution(f=f,angle=angle,z=z,arclength=arclength,path=path, rclip=rclip, rspan=rspan, horiz=horiz, style=style, +module plot_revolution(f,angle,z,arclength, path, rclip, rspan, horiz=false,r1,r2,r,d1,d2,d,convexity=4, + anchor="origin", orient=UP, spin=0, atype="hull", cp="centroid", style="min_edge", reverse=false) + vnf_polyhedron(plot_revolution(f=f,angle=angle,z=z,arclength=arclength,path=path, rclip=rclip, rspan=rspan, horiz=horiz, style=style, reverse=reverse, r=r,d=d,r1=r1,d1=d1,r2=r2,d2=d2), anchor=anchor, orient=orient, spin=spin, atype=atype, cp=cp); - - + function plot_revolution(f,angle,z,arclength, path, rclip, rspan, horiz=false,r1,r2,r,d1,d2,d, - anchor="origin", orient=UP, spin=0, atype="hull", cp="centroid", style="min_edge") = + anchor="origin", orient=UP, spin=0, atype="hull", cp="centroid", style="min_edge", reverse=false) = assert(num_defined([angle,arclength])==1, "must define exactly one of angle and arclength") - assert(is_vector(z) || valid_range(z), "z must be a vector or nonempty range") - assert(is_range(z) || is_increasing(z, strict=true), "z must be strictly increasing") + assert(is_undef(z) || is_vector(z) || valid_range(z), "z must be a vector or nonempty range") assert(is_undef(path) || num_defined([r1,r2,d1,d2,r,d,z])==0, "Cannot define the z parameter or any radius or diameter parameters in combination with path") assert(num_defined([rclip,rspan])<2, "Cannot give both rclip and rspan") assert(is_undef(rclip) || (is_list(rclip) && len(rclip)==2 && is_finite(rclip[0]) && rclip[0]>0 && is_num(rclip[1])), @@ -4407,12 +4434,14 @@ function plot_revolution(f,angle,z,arclength, path, rclip, rspan, horiz=false,r1 let( r1 = get_radius(r1=r1, r=r, d1=d1, d=d), r2 = get_radius(r1=r2, r=r, d1=d2, d=d), + dummy3=assert(is_def(path) || all_defined([r1,r2,z]), "\nMust give either path or both the 'z' and radius parameters."), rmin=0.01, z = list(z), thetarange = list(first_defined([angle,arclength])), dummy = assert(is_vector(thetarange) && len(thetarange)>1 && is_increasing(thetarange,strict=true), "angle/arclength must be a strictly increasing array or range with at least 2 elements") - assert(is_def(arclength) || (last(thetarange)-thetarange[0])<=360, "angle span exceeds 360 degrees"), + assert(is_def(path)|| (len(z)>1 && is_increasing(z, strict=true)),"z must be a strictly increasing array or range with at least 2 elements") + assert(is_def(arclength) || (last(thetarange)-thetarange[0])<=360, "angle span exceeds 360 degrees"), path = is_def(path) ? path : let( rvals = add_scalar(add_scalar(z,-z[0]) / (last(z)-z[0]) * (r2-r1) ,r1) @@ -4420,37 +4449,33 @@ function plot_revolution(f,angle,z,arclength, path, rclip, rspan, horiz=false,r1 hstack([rvals,z]), normals = horiz ? repeat([1,0], len(path)) : path_normals(path), - rclip = default(rclip, [0.01,INF]), + rclip = default(rclip, [-INF,INF]), rdata = [for(pt=path) - let( - angscale = is_def(angle) ? 1 : 360/2/PI/pt.x - ) - [for(theta=thetarange) min(max(f(theta /*angscale*/,pt.y),rclip[0]),rclip[1])]], + [for(theta=thetarange) min(max(f(theta,pt.y),rclip[0]),rclip[1])]], dummy2=assert(len(rdata[0])>1 && len(rdata)>1, "xrange and yrange must both provide at least 2 points"), minval = min(flatten(rdata)), maxval = max(flatten(rdata)), - sdata = is_undef(rspan) && minval>0 ? rdata - : is_undef(rspan) ? [for(row=rdata) [for (entry=row) max(rmin,entry)]] + sdata = is_undef(rspan) ? rdata : let( scale = (rspan[1]-rspan[0])/(maxval-minval) ) [for(row=rdata) [for (entry=row) scale*(entry.z-minval)+rspan[0]]], - closed = is_def(angle) && last(thetarange)-thetarange[0]==360, + closed = is_def(angle) && last(thetarange)-thetarange[0]==360, final = [for(i=idx(path)) let( angscale = is_def(angle) ? 1 : 360/2/PI/path[i].x - ) assert(angscale*(last(thetarange)-thetarange[0])<=360, str("arclength span is more than 360 degrees at profile index ",i," with radius ",path[i].x)) [ if (!closed) [0,0,path[i].y], for(j=idx(sdata[0])) - cylindrical_to_xyz(path[i].x+sdata[i][j]*normals[i].x, angscale*thetarange[j], path[i].y+sdata[i][j]*normals[i].y) + cylindrical_to_xyz(max(rmin,path[i].x+sdata[i][j]*normals[i].x), angscale*thetarange[j], path[i].y+sdata[i][j]*normals[i].y) ] ] ) - vnf_vertex_array(final, col_wrap=true, caps=true); + vnf_vertex_array(final, col_wrap=true, caps=true,reverse=!reverse, style=style); + @@ -4591,64 +4616,65 @@ function heightfield(data, size=[100,100], bottom=-20, maxz=100, xrange=[-1:0.04 ) reorient(anchor,spin,orient, vnf=vnf, p=vnf); -// Function&Module: cylindrical_heightfield() -// Synopsis: Generates a cylindrical 3d surface from a 2D grid of values. -// SynTags: Geom, VNF -// Topics: Extrusion, Textures, Knurling, Heightfield -// Usage: As Function -// vnf = cylindrical_heightfield(data, l|length=|h=|height=, r|d=, [base=], [transpose=], [aspect=]); -// Usage: As Module -// cylindrical_heightfield(data, l|length=|h=|height=, r|d=, [base=], [transpose=], [aspect=]) [ATTACHMENTS]; -// Description: -// Given a regular rectangular 2D grid of scalar values, or a function literal of signature (x,y), generates -// a cylindrical 3D surface where the height at any given point above the radius `r=`, is the scalar value -// for that position. -// One script to convert a grayscale image to a heightfield array in a .scad file can be found at: -// https://raw.githubusercontent.com/BelfrySCAD/BOSL2/master/scripts/img2scad.py -// Arguments: -// data = This is either the 2D rectangular array of heights, or a function literal of signature `(x, y)`. -// l / length / h / height = The length of the cylinder to wrap around. -// r = The radius of the cylinder to wrap around. -// --- -// r1 = The radius of the bottom of the cylinder to wrap around. -// r2 = The radius of the top of the cylinder to wrap around. -// d = The diameter of the cylinder to wrap around. -// d1 = The diameter of the bottom of the cylinder to wrap around. -// d2 = The diameter of the top of the cylinder to wrap around. -// base = The radius for the bottom of the heightfield object to create. Any heights smaller than this will be truncated to very slightly above this height. Default: -20 -// transpose = If true, swaps the radial and length axes of the data. Default: false -// aspect = The aspect ratio of the generated heightfield at the surface of the cylinder. Default: 1 -// xrange = A range of values to iterate X over when calculating a surface from a function literal. Default: [-1 : 0.01 : 1] -// yrange = A range of values to iterate Y over when calculating a surface from a function literal. Default: [-1 : 0.01 : 1] -// maxh = The maximum height above the radius to model. Truncates anything taller to this height. Default: 99 -// style = The style of subdividing the quads into faces. Valid options are "default", "alt", and "quincunx". Default: "default" -// convexity = Max number of times a line could intersect a wall of the surface being formed. Module only. Default: 10 -// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER` -// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#subsection-spin). Default: `0` -// orient = Vector to rotate top towards. See [orient](attachments.scad#subsection-orient). Default: `UP` -// Example(VPD=400;VPR=[55,0,150]): -// cylindrical_heightfield(l=100, r=30, base=5, data=[ -// for (y=[-180:4:180]) [ -// for(x=[-180:4:180]) -// 5*cos(5*norm([x,y]))+5 -// ] -// ]); -// Example(VPD=400;VPR=[55,0,150]): -// cylindrical_heightfield(l=100, r1=60, r2=30, base=5, data=[ -// for (y=[-180:4:180]) [ -// for(x=[-180:4:180]) -// 5*cos(5*norm([x,y]))+5 -// ] -// ]); -// Example(VPD=400;VPR=[55,0,150]): Heightfield by Function -// fn = function (x,y) 5*sin(x*360)*cos(y*360)+5; -// cylindrical_heightfield(l=100, r=30, data=fn); -// Example(VPD=400;VPR=[55,0,150]): Heightfield by Function, with Specific Ranges -// fn = function (x,y) 2*cos(5*norm([x,y])); -// cylindrical_heightfield( -// l=100, r=30, base=5, data=fn, -// xrange=[-180:2:180], yrange=[-180:2:180] -// ); +/// Function&Module: cylindrical_heightfield() +/// Synopsis: Generates a cylindrical 3d surface from a 2D grid of values. +/// SynTags: Geom, VNF +/// Topics: Extrusion, Textures, Knurling, Heightfield +/// Usage: As Function +/// vnf = cylindrical_heightfield(data, l|length=|h=|height=, r|d=, [base=], [transpose=], [aspect=]); +/// Usage: As Module +/// cylindrical_heightfield(data, l|length=|h=|height=, r|d=, [base=], [transpose=], [aspect=]) [ATTACHMENTS]; +/// Description: +/// Given a regular rectangular 2D grid of scalar values, or a function literal of signature (x,y), generates +/// a cylindrical 3D surface where the height at any given point above the radius `r=`, is the scalar value +/// for that position. +/// One script to convert a grayscale image to a heightfield array in a .scad file can be found at: +/// https://raw.githubusercontent.com/BelfrySCAD/BOSL2/master/scripts/img2scad.py +/// Arguments: +/// data = This is either the 2D rectangular array of heights, or a function literal of signature `(x, y)`. +/// l / length / h / height = The length of the cylinder to wrap around. +/// r = The radius of the cylinder to wrap around. +/// --- +/// r1 = The radius of the bottom of the cylinder to wrap around. +/// r2 = The radius of the top of the cylinder to wrap around. +/// d = The diameter of the cylinder to wrap around. +/// d1 = The diameter of the bottom of the cylinder to wrap around. +/// d2 = The diameter of the top of the cylinder to wrap around. +/// base = The radius for the bottom of the heightfield object to create. Any heights smaller than this will be truncated to very slightly above this height. Default: -20 +/// transpose = If true, swaps the radial and length axes of the data. Default: false +/// aspect = The aspect ratio of the generated heightfield at the surface of the cylinder. Default: 1 +/// xrange = A range of values to iterate X over when calculating a surface from a function literal. Default: [-1 : 0.01 : 1] +/// yrange = A range of values to iterate Y over when calculating a surface from a function literal. Default: [-1 : 0.01 : 1] +/// maxh = The maximum height above the radius to model. Truncates anything taller to this height. Default: 99 +/// style = The style of subdividing the quads into faces. Valid options are "default", "alt", and "quincunx". Default: "default" +/// convexity = Max number of times a line could intersect a wall of the surface being formed. Module only. Default: 10 +/// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER` +/// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#subsection-spin). Default: `0` +/// orient = Vector to rotate top towards. See [orient](attachments.scad#subsection-orient). Default: `UP` +/// Example(VPD=400;VPR=[55,0,150]): +/// cylindrical_heightfield(l=100, r=30, base=5, data=[ +/// for (y=[-180:4:180]) [ +/// for(x=[-180:4:180]) +/// 5*cos(5*norm([x,y]))+5 +/// ] +/// ]); +/// Example(VPD=400;VPR=[55,0,150]): +/// cylindrical_heightfield(l=100, r1=60, r2=30, base=5, data=[ +/// for (y=[-180:4:180]) [ +/// for(x=[-180:4:180]) +/// 5*cos(5*norm([x,y]))+5 +/// ] +/// ]); +/// Example(VPD=400;VPR=[55,0,150]): Heightfield by Function +/// fn = function (x,y) 5*sin(x*360)*cos(y*360)+5; +/// cylindrical_heightfield(l=100, r=30, data=fn); +/// Example(VPD=400;VPR=[55,0,150]): Heightfield by Function, with Specific Ranges +/// fn = function (x,y) 2*cos(5*norm([x,y])); +/// cylindrical_heightfield( +/// l=100, r=30, base=5, data=fn, +/// xrange=[-180:2:180], yrange=[-180:2:180] +/// ); + function cylindrical_heightfield( data, l, r, base=1, @@ -4660,6 +4686,9 @@ function cylindrical_heightfield( anchor=CTR, spin=0, orient=UP ) = let( + dummy=is_function(data) + ? echo("***** cylindrical_heightfield() is deprecated and will be removed in a future version. For creating functions on cylinders use plot_revolution(). *****") + : echo("***** cylindrical_heightfield() is deprecated and will be removed in a future version. For displaying arrays on a cylinder use rotate_sweep() *****"), l = one_defined([l, h, height, length], "l,h,height,l"), r1 = get_radius(r1=r1, r=r, d1=d1, d=d), r2 = get_radius(r1=r2, r=r, d1=d2, d=d) diff --git a/skin.scad b/skin.scad index 9df6d233..74243511 100644 --- a/skin.scad +++ b/skin.scad @@ -4052,7 +4052,6 @@ function texture(tex, n, border, gap, roughness, inset) = /// _textured_linear_sweep(region, texture, tex_size, h, ...) [ATTACHMENTS]; /// _textured_linear_sweep(region, texture, counts=, h=, ...) [ATTACHMENTS]; /// Topics: Sweep, Extrusion, Textures, Knurling -/// See Also: heightfield(), cylindrical_heightfield(), texture() /// Description: /// Given a [[Region|regions.scad]], creates a linear extrusion of it vertically, optionally twisted, scaled, and/or shifted, /// with a given texture tiled evenly over the side surfaces. The texture can be given in one of three ways: @@ -4374,7 +4373,6 @@ function _tile_edge_path_list(vnf, axis, maxopen=1) = /// _textured_revolution(shape, texture, tex_size, [tex_scale=], ...) [ATTACHMENTS]; /// _textured_revolution(shape, texture, counts=, [tex_scale=], ...) [ATTACHMENTS]; /// Topics: Sweep, Extrusion, Textures, Knurling -/// See Also: heightfield(), cylindrical_heightfield(), texture() /// Description: /// Given a 2D region or path, fully in the X+ half-plane, revolves that shape around the Z axis (after rotating its Y+ to Z+). /// This creates a solid from that surface of revolution, possibly capped top and bottom, with the sides covered in a given tiled texture. diff --git a/utility.scad b/utility.scad index 28e1c7a6..70f12384 100644 --- a/utility.scad +++ b/utility.scad @@ -884,7 +884,7 @@ module deprecate(new_name) // echo_viewport(); // Description: // Display the current viewport parameters so that they can be pasted into examples for the wiki. -// The viewport should have a 4x3 aspect ratio to ensure proper framing of the object. +// The viewport should have a 4:3 aspect ratio to ensure proper framing of the object. module echo_viewport() {