From 32ff742d0e98c133b32ce4bff0fb59e4991ade10 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Sun, 20 Apr 2025 17:45:25 -0700 Subject: [PATCH 01/50] fix bunching in vnf_tri_array --- vnf.scad | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/vnf.scad b/vnf.scad index 3b512602..d08f875a 100644 --- a/vnf.scad +++ b/vnf.scad @@ -311,7 +311,7 @@ function vnf_vertex_array( texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0, tex_depth=1, tex_extra, tex_skip, sidecaps,sidecap1,sidecap2, normals ) = - assert(in_list(style,["default","alt","quincunx", "convex","concave", "min_edge","min_area","flip1","flip2"])) + assert(in_list(style,["default","alt","quincunx", "convex","concave", "min_edge","min_area","flip1","flip2","diagnormals"])) assert(is_matrix(points[0], n=3),"\nPoint array has the wrong shape or points are not 3d.") assert(is_consistent(points), "\nNon-rectangular or invalid point array.") assert(is_bool(triangulate)) @@ -347,6 +347,7 @@ function vnf_vertex_array( ) mean([pts[i1], pts[i2], pts[i3], pts[i4]]) ], + surfnorms = style=="diagnormals" ? surface_normals(points, row_wrap=row_wrap, col_wrap=col_wrap) : [], allfaces = [ if (cap1) count(cols,reverse=!reverse), if (cap2) count(cols,(rows-1)*cols, reverse=reverse), @@ -401,6 +402,16 @@ function vnf_vertex_array( : [[i1,i3,i2],[i1,i4,i3]] ) concavefaces + : style=="diagnormals"? + let( + ang13 = vector_angle(surfnorms[r][c],surfnorms[(r+1)%rows][(c+1)%cols]), + ang24 = vector_angle(surfnorms[(r+1)%rows][c],surfnorms[r][(c+1)%cols]), + smallang = ang24=n1 && t2>=n2 ? trilist : - _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse, concat(trilist, [triangle]), d12=n1?i1:t1) : i1, userow==2 ? (t2>=n2?i2:t2) : i2, tc1, tc2, trimax) : // equal row lengths let(n=n1, i=i1, @@ -606,7 +628,7 @@ function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[], [i2offset+t, i1offset+t, d12=n ? trilist : - _lofttri(p1, p2, i1offset, i2offset, n, n, reverse, concat(trilist, [triangle1, triangle2]), t, t); + _lofttri(p1, p2, i1offset, i2offset, n, n, reverse, concat(trilist, [triangle1, triangle2]), t, t, 0,0,trimax); // Function: vnf_join() From 8d926f93307e542084fb336b667d62c4bd5caba7 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Mon, 21 Apr 2025 14:54:47 -0700 Subject: [PATCH 02/50] fix bunching in vnf_tri_array, added example --- vnf.scad | 72 ++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/vnf.scad b/vnf.scad index 907b533a..0817f407 100644 --- a/vnf.scad +++ b/vnf.scad @@ -330,7 +330,7 @@ function vnf_vertex_array( texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0, tex_scaling="default", tex_depth=1, tex_extra, tex_skip, sidecaps,sidecap1,sidecap2, normals ) = - assert(in_list(style,["default","alt","quincunx", "convex","concave", "min_edge","min_area","flip1","flip2","diagnormals"])) + assert(in_list(style,["default","alt","quincunx", "convex","concave", "min_edge","min_area","flip1","flip2"])) assert(is_matrix(points[0], n=3),"\nPoint array has the wrong shape or points are not 3d.") assert(is_consistent(points), "\nNon-rectangular or invalid point array.") assert(is_bool(triangulate)) @@ -366,7 +366,6 @@ function vnf_vertex_array( ) mean([pts[i1], pts[i2], pts[i3], pts[i4]]) ], - surfnorms = style=="diagnormals" ? surface_normals(points, row_wrap=row_wrap, col_wrap=col_wrap) : [], allfaces = [ if (cap1) count(cols,reverse=!reverse), if (cap2) count(cols,(rows-1)*cols, reverse=reverse), @@ -421,16 +420,6 @@ function vnf_vertex_array( : [[i1,i3,i2],[i1,i4,i3]] ) concavefaces - : style=="diagnormals"? - let( - ang13 = vector_angle(surfnorms[r][c],surfnorms[(r+1)%rows][(c+1)%cols]), - ang24 = vector_angle(surfnorms[(r+1)%rows][c],surfnorms[r][(c+1)%cols]), - smallang = ang24 Date: Mon, 21 Apr 2025 15:02:16 -0700 Subject: [PATCH 03/50] Corrected docsgen example parameter --- vnf.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vnf.scad b/vnf.scad index 0817f407..da610755 100644 --- a/vnf.scad +++ b/vnf.scad @@ -519,7 +519,7 @@ function vnf_vertex_array( // vnf = vnf_tri_array(pts); // vnf_wireframe(vnf,width=0.1); // color("red")move_copies(flatten(pts)) sphere(r=.15,$fn=9); -// Example(3D,Med,NoAxes,ShowEdges=true,VPR=[29,0,341],VPD=45,VPT=[11,5,2]): The default parameter `limit_bunching=true` prevents too many triangles from sharing a single vertex in one row, if several points of one row happen to be closest to a single point on another row. In the left figure, `limit_bunching=false`, causing an endpoint on each row to get many triangles from the other row, because the algorithm seeks the shortest triangle leg distance once the first two points of each row are connected. This doesn't happen if both rows are the same length. The figure on the right uses the default `limit_bunching=true`, forcing the triangulation to stop adding too many triangles to the same vertex. +// Example(3D,Med,NoAxes,Edges,VPR=[29,0,341],VPD=45,VPT=[11,5,2]): The default parameter `limit_bunching=true` prevents too many triangles from sharing a single vertex in one row, if several points of one row happen to be closest to a single point on another row. In the left figure, `limit_bunching=false`, causing an endpoint on each row to get many triangles from the other row, because the algorithm seeks the shortest triangle leg distance once the first two points of each row are connected. This doesn't happen if both rows are the same length. The figure on the right uses the default `limit_bunching=true`, forcing the triangulation to stop adding too many triangles to the same vertex. // pts = [ // [[5,0,0], [4,0,1.4], [3,0,2], [2,0,1.4], [1,0,0]], // [[14,10,0], [12,9,5], [9,8,7], [6,7,7], [3,6,5], [0,5,0]] From 949f6bbf5957fb1c71a398e647401e3f6af13f18 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Mon, 21 Apr 2025 16:07:44 -0700 Subject: [PATCH 04/50] spelling --- vnf.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vnf.scad b/vnf.scad index da610755..632e860d 100644 --- a/vnf.scad +++ b/vnf.scad @@ -443,7 +443,7 @@ function vnf_vertex_array( // See Also: vnf_vertex_array(), vnf_join(), vnf_from_polygons(), vnf_merge_points() // Usage: // vnf = vnf_tri_array(points, [caps=], [cap1=], [cap2=], [reverse=], [col_wrap=], [row_wrap=], [limit_bunching=]) -// vnf_vertex_array(points, [caps=], [cap1=], [cap2=], [reverse=], [col_wrap=], [row_wrap=], [limit_bunching=],...) [ATTACHMENTS]; +// vnf_tri_array(points, [caps=], [cap1=], [cap2=], [reverse=], [col_wrap=], [row_wrap=], [limit_bunching=],...) [ATTACHMENTS]; // Description: // Produces a VNF from an array of points where each row length can differ from the adjacent rows by // any amount. This enables the construction of triangular or even irregular VNF patches. The From ee3afde9e8ccffd1a4ea9335e853bd74b5ce8975 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Mon, 21 Apr 2025 19:20:55 -0400 Subject: [PATCH 05/50] strings doc fixes --- strings.scad | 130 +++++++++++++++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 56 deletions(-) diff --git a/strings.scad b/strings.scad index b90a5c3e..e6b764a2 100644 --- a/strings.scad +++ b/strings.scad @@ -28,11 +28,11 @@ function _is_liststr(s) = is_list(s) || is_str(s); // pos = starting index of substring, or vector of first and last position. Default: 0 // len = length of substring, or omit it to get the rest of the string. If len is zero or less then the emptry string is returned. // Example: -// substr("abcdefg",3,3); // Returns "def" -// substr("abcdefg",2); // Returns "cdefg" -// substr("abcdefg",len=3); // Returns "abc" -// substr("abcdefg",[2,4]); // Returns "cde" -// substr("abcdefg",len=-2); // Returns "" +// s1=substr("abcdefg",3,3); // Returns "def" +// s2=substr("abcdefg",2); // Returns "cdefg" +// s3=substr("abcdefg",len=3); // Returns "abc" +// s4=substr("abcdefg",[2,4]); // Returns "cde" +// s5=substr("abcdefg",len=-2); // Returns "" function substr(str, pos=0, len=undef) = assert(is_string(str)) is_list(pos) ? _substr(str, pos[0], pos[1]-pos[0]+1) : @@ -84,20 +84,20 @@ function suffix(str,len) = // all = set to true to return all matches as a list. Overrides last. Default: false // start = index where the search starts // Example: -// str_find("abc123def123abc","123"); // Returns 3 -// str_find("abc123def123abc","b"); // Returns 1 -// str_find("abc123def123abc","1234"); // Returns undef -// str_find("abc",""); // Returns 0 -// str_find("abc123def123", "123", start=4); // Returns 9 -// str_find("abc123def123abc","123",last=true); // Returns 9 -// str_find("abc123def123abc","b",last=true); // Returns 13 -// str_find("abc123def123abc","1234",last=true); // Returns undef -// str_find("abc","",last=true); // Returns 3 -// str_find("abc123def123", "123", start=8, last=true)); // Returns 3 -// str_find("abc123def123abc","123",all=true); // Returns [3,9] -// str_find("abc123def123abc","b",all=true); // Returns [1,13] -// str_find("abc123def123abc","1234",all=true); // Returns [] -// str_find("abc","",all=true); // Returns [0,1,2] +// a=str_find("abc123def123abc","123"); // Returns 3 +// b=str_find("abc123def123abc","b"); // Returns 1 +// c=str_find("abc123def123abc","1234"); // Returns undef +// d=str_find("abc",""); // Returns 0 +// e=str_find("abc123def123", "123", start=4); // Returns 9 +// f=str_find("abc123def123abc","123",last=true); // Returns 9 +// g=str_find("abc123def123abc","b",last=true); // Returns 13 +// h=str_find("abc123def123abc","1234",last=true); // Returns undef +// i=str_find("abc","",last=true); // Returns 3 +// j=str_find("abc123def123", "123", start=8, last=true)); // Returns 3 +// k=str_find("abc123def123abc","123",all=true); // Returns [3,9] +// l=str_find("abc123def123abc","b",all=true); // Returns [1,13] +// m=str_find("abc123def123abc","1234",all=true); // Returns [] +// n=str_find("abc","",all=true); // Returns [0,1,2] function str_find(str,pattern,start=undef,last=false,all=false) = assert(_is_liststr(str), "str must be a string or list") assert(_is_liststr(pattern), "pattern must be a string or list") @@ -136,13 +136,13 @@ function _str_find_all(str,pattern) = // str = String to search // start = Starting index for search in str // pattern = String pattern to search for -// Examples: -// substr_match("abcde",2,"cd"); // Returns true -// substr_match("abcde",2,"cx"); // Returns false -// substr_match("abcde",2,"cdef"); // Returns false -// substr_match("abcde",-2,"cd"); // Returns false -// substr_match("abcde",19,"cd"); // Returns false -// substr_match("abc",1,""); // Returns true +// Example: +// a=substr_match("abcde",2,"cd"); // Returns true +// b=substr_match("abcde",2,"cx"); // Returns false +// c=substr_match("abcde",2,"cdef"); // Returns false +// d=substr_match("abcde",-2,"cd"); // Returns false +// e=substr_match("abcde",19,"cd"); // Returns false +// f=substr_match("abc",1,""); // Returns true // // This is carefully optimized for speed. Precomputing the length @@ -173,9 +173,9 @@ function _substr_match_recurse(str,sindex,pattern,plen,pindex=0,) = // str = String to search. // pattern = String pattern to search for. // Example: -// starts_with("abcdef","abc"); // Returns true -// starts_with("abcdef","def"); // Returns false -// starts_with("abcdef",""); // Returns true +// b1=starts_with("abcdef","abc"); // Returns true +// b2=starts_with("abcdef","def"); // Returns false +// b3=starts_with("abcdef",""); // Returns true function starts_with(str,pattern) = _is_liststr(str) && substr_match(str,0,pattern); @@ -192,9 +192,9 @@ function starts_with(str,pattern) = _is_liststr(str) && substr_match(str,0,patte // str = String to search. // pattern = String pattern to search for. // Example: -// ends_with("abcdef","def"); // Returns true -// ends_with("abcdef","de"); // Returns false -// ends_with("abcdef",""); // Returns true +// b1=ends_with("abcdef","def"); // Returns true +// b2=ends_with("abcdef","de"); // Returns false +// b3=ends_with("abcdef",""); // Returns true function ends_with(str,pattern) = _is_liststr(str) && substr_match(str,len(str)-len(pattern),pattern); @@ -220,12 +220,12 @@ function ends_with(str,pattern) = _is_liststr(str) && substr_match(str,len(str)- // sep = a string or list of strings to use for the separator // keep_nulls = boolean value indicating whether to keep null strings in the output list. Default: true // Example: -// str_split("abc+def-qrs*iop","*-+"); // Returns ["abc", "def", "qrs", "iop"] -// str_split("abc+*def---qrs**iop+","*-+");// Returns ["abc", "", "def", "", "", "qrs", "", "iop", ""] -// str_split("abc def"," "); // Returns ["abc", "", "", "", "", "", "def"] -// str_split("abc def"," ",keep_nulls=false); // Returns ["abc", "def"] -// str_split("abc+def-qrs*iop",["+","-","*"]); // Returns ["abc", "def", "qrs", "iop"] -// str_split("abc+def-qrs*iop",["-","+","*"]); // Returns ["abc+def", "qrs*iop", "", ""] +// s1=str_split("abc+def-qrs*iop","*-+"); // Returns ["abc", "def", "qrs", "iop"] +// s2=str_split("abc+*def---qrs**iop+","*-+");// Returns ["abc", "", "def", "", "", "qrs", "", "iop", ""] +// s3=str_split("abc def"," "); // Returns ["abc", "", "", "", "", "", "def"] +// s4=str_split("abc def"," ",keep_nulls=false); // Returns ["abc", "def"] +// s5=str_split("abc+def-qrs*iop",["+","-","*"]); // Returns ["abc", "def", "qrs", "iop"] +// s6=str_split("abc+def-qrs*iop",["-","+","*"]); // Returns ["abc+def", "qrs*iop", "", ""] function str_split(str,sep,keep_nulls=true) = !keep_nulls ? _remove_empty_strs(str_split(str,sep,keep_nulls=true)) : is_list(sep) ? _str_split_recurse(str,sep,i=0,result=[]) : @@ -265,8 +265,8 @@ function _remove_empty_strs(list) = // list = list of strings to concatenate // sep = separator string to insert. Default: "" // Example: -// str_join(["abc","def","ghi"]); // Returns "abcdefghi" -// str_join(["abc","def","ghi"], " + "); // Returns "abc + def + ghi" +// s1=str_join(["abc","def","ghi"]); // Returns "abcdefghi" +// s2=str_join(["abc","def","ghi"], " + "); // Returns "abc + def + ghi" function str_join(list,sep="",_i=0, _result="") = assert(is_list(list)) _i >= len(list)-1 ? (_i==len(list) ? _result : str(_result,list[_i])) : @@ -285,22 +285,22 @@ function str_join(list,sep="",_i=0, _result="") = // Takes a string `s` and strips off all leading and/or trailing characters that exist in string `c`. // By default strips both leading and trailing characters. If you set start or end to true then // it will strip only the leading or trailing characters respectively. If you set start -// or end to false then it will strip only lthe trailing or leading characters. +// or end to false then it will strip only the trailing or leading characters. // Arguments: // s = The string to strip leading or trailing characters from. // c = The string of characters to strip. // start = if true then strip leading characters // end = if true then strip trailing characters // Example: -// str_strip("--##--123--##--","#-"); // Returns: "123" -// str_strip("--##--123--##--","-"); // Returns: "##--123--##" -// str_strip("--##--123--##--","#"); // Returns: "--##--123--##--" -// str_strip("--##--123--##--","#-",end=true); // Returns: "--##--123" -// str_strip("--##--123--##--","-",end=true); // Returns: "--##--123--##" -// str_strip("--##--123--##--","#",end=true); // Returns: "--##--123--##--" -// str_strip("--##--123--##--","#-",start=true); // Returns: "123--##--" -// str_strip("--##--123--##--","-",start=true); // Returns: "##--123--##--" -// str_strip("--##--123--##--","#",start=true); // Returns: "--##--123--##--" +// s1=str_strip("--##--123--##--","#-"); // Returns: "123" +// s2=str_strip("--##--123--##--","-"); // Returns: "##--123--##" +// s3=str_strip("--##--123--##--","#"); // Returns: "--##--123--##--" +// s4=str_strip("--##--123--##--","#-",end=true); // Returns: "--##--123" +// s5=str_strip("--##--123--##--","-",end=true); // Returns: "--##--123--##" +// s6=str_strip("--##--123--##--","#",end=true); // Returns: "--##--123--##--" +// s7=str_strip("--##--123--##--","#-",start=true); // Returns: "123--##--" +// s8=str_strip("--##--123--##--","-",start=true); // Returns: "##--123--##--" +// s9=str_strip("--##--123--##--","#",start=true); // Returns: "--##--123--##--" function _str_count_leading(s,c,_i=0) = (_i>=len(s)||!in_list(s[_i],[each c]))? _i : @@ -337,6 +337,10 @@ function str_strip(s,c,start,end) = // length = length to pad to // char = character to pad with. Default: " " (space) // left = if true, pad on the left side. Default: false +// Example: +// s1=str_pad("hello", 10, "*"); // Returns: "hello*****" +// s2=str_pad("hello", 10, "*", left=true); // Returns: "*****hello" + function str_pad(str,length,char=" ",left=false) = assert(is_str(str)) assert(is_str(char) && len(char)==1, "char must be a single character string") @@ -349,14 +353,22 @@ function str_pad(str,length,char=" ",left=false) = // Function: str_replace_char() -// Synopsis: Replace given chars in a string with another substring. +// Synopsis: Replace specified character in a string with a string. // Topics: Strings // See Also: suffix(), str_find(), substr_match(), starts_with(), ends_with(), str_split(), str_join(), str_strip() // Usage: // newstr = str_replace_char(str, char, replace); // Description: -// Replace every occurence of `char` in the input string with the string `replace` which -// can be any string. +// Replace every occurence of `char` (a single character string) in the input string +// with the string `replace` which can be any string. +// Arguments: +// str = string to process +// char = single character string to search for +// replace = string that replaces all copies of `char` +// Example: +// s1 = str_replace_char("abcdcba","c","_123_"); // Returns: "ab123d123ba" +// s2 = str_replace_char(" s t r i n g ", " ", ""); // Returns: "string" + function str_replace_char(str,char,replace) = assert(is_str(str)) assert(is_str(char) && len(char)==1, "Search pattern 'char' must be a single character string") @@ -376,7 +388,7 @@ function str_replace_char(str,char,replace) = // Arguments: // str = String to convert. // Example: -// downcase("ABCdef"); // Returns "abcdef" +// s=downcase("ABCdef"); // Returns "abcdef" function downcase(str) = assert(is_string(str)) str_join([for(char=str) let(code=ord(char)) code>=65 && code<=90 ? chr(code+32) : char]); @@ -394,7 +406,7 @@ function downcase(str) = // Arguments: // str = String to convert. // Example: -// upcase("ABCdef"); // Returns "ABCDEF" +// s=upcase("ABCdef"); // Returns "ABCDEF" function upcase(str) = assert(is_string(str)) str_join([for(char=str) let(code=ord(char)) code>=97 && code<=122 ? chr(code-32) : char]); @@ -412,8 +424,12 @@ function upcase(str) = // Produce a random string of length `n`. If you give a string `charset` then the // characters of the random string are drawn from that list, weighted by the number // of times each character appears in the list. If you do not give a character set -// then the string is generated with characters ranging from 0 to z (based on +// then the string is generated with characters ranging from "0" to "z" (based on // character code). +// Arguments: +// n = number of characters to produce +// charset = string to draw the characters from. Default: characters from "0" to "z". +// seed = random number seed function rand_str(n, charset, seed) = is_undef(charset)? str_join([for(c=rand_int(48,122,n,seed)) chr(c)]) : str_join([for(i=rand_int(0,len(charset)-1,n,seed)) charset[i]]); @@ -560,6 +576,8 @@ function parse_frac(str,mixed=true,improper=true,signed=true) = // Description: // Converts a string to a number. The string can be either a fraction (two integers separated by a "/") or a floating point number. // Returns NaN if the conversion fails. +// Arguments: +// str = string to process // Example: // parse_num("3/4"); // Returns 0.75 // parse_num("3.4e-2"); // Returns 0.034 From fc6843ef9daa42a6d46e9459b69a0fef5e14d290 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Mon, 21 Apr 2025 23:27:23 -0400 Subject: [PATCH 06/50] add tex_aspect and pixel_aspect to rotate_sweep --- skin.scad | 99 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 17 deletions(-) diff --git a/skin.scad b/skin.scad index 9814414d..1188b5db 100644 --- a/skin.scad +++ b/skin.scad @@ -724,7 +724,9 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close // cyl(d=14.5,h=1,anchor=BOT,rounding=1/3,$fa=1,$fs=.5); // linear_sweep(circle(d=12), h=12, scale=1.3, texture=diag_weave_vnf, // tex_size=[5,5], convexity=12); -// } +// } + + module linear_sweep( region, height, center, @@ -897,7 +899,13 @@ function linear_sweep( // (Regions are always composed of closed polygons.) If you give a path and specify `closed=false` then the path will be connected to the Y axis by // a horizontal segment at each end, resulting in flat faces at the top and bottom. These flat faces do not receive any applied texture. No segment of of the // region---including the closing segments added to polygons---can lie on the Y axis. When `closed=false` you can terminate one or both ends of the path -// on the Y axis if you want texturing to continue all the way to the center. +// on the Y axis if you want texturing to continue all the way to the center. +// . +// If you want to place just one or a few copies of a texture onto an object rather than texturing the entire object you can do that by using +// and angle smaller than 360. However, if you want to control the aspect ratio of the resulting texture you will have to carefully calculate the proper +// angle to use. To simplify this process you can use `pixel_aspect` or `tex_aspect`. You can set `tex_aspect` for any type of tile and it specifies +// the desired aspect ratio (width/height) for the tiles. You must specify `tex_reps` in order to use this feature. For heightfields you can instead provide +// a pixel aspect ratio, which is suited to the case where your texture is a non-square image that you want to place on a curved object. // Arguments: // shape = The polygon or [region](regions.scad) to sweep around the Z axis. // angle = If given, specifies the number of degrees to sweep the region around the Z axis, counterclockwise from the X+ axis. Default: 360 (full rotation) @@ -911,6 +919,8 @@ function linear_sweep( // tex_depth = Specify texture depth; if negative, invert the texture. Default: 1. // tex_samples = Minimum number of "bend points" to have in VNF texture tiles. Default: 8 // tex_taper = If given as a number, tapers the texture height to zero over the first and last given percentage of the path. If given as a lookup table with indices between 0 and 100, uses the percentage lookup table to ramp the texture heights. Default: `undef` (no taper) +// tex_aspect = Choose the angle of the revolution to maintain this aspect ratio for the tiles. You must specify tex_reps. Overrides any angle specified. +// pixel_aspect = Choose the angle of the revolution to maintain this apsect ratio for pixels in a heightfield texture. You must specify tex_reps. Overrides any angle specified. // style = {{vnf_vertex_array()}} style. Default: "min_edge" // closed = If false, and `shape` is a path, then the revolved path is connected to the axis of rotation with untextured caps. Ignored if `shape` is not a path. Default: `true` // convexity = (Module only) Convexity setting for use with polyhedron. Default: 10 @@ -1097,7 +1107,7 @@ function linear_sweep( // h = 20; // r = 15; // ang = len(img[0])/len(img)*h/(2*PI*r)*360; -// rotate_sweep([[15,-10],[15,10]], texture=img, +// rotate_sweep([[r,-h/2],[r,h/2]], texture=img, // tex_reps=1,angle=ang, closed=false); // // Example(3D,VPR=[80.20,0.00,138.40],VPD=82.67,VPT=[6.88,7.29,1.77],NoAxes): Here we have combined the above model with a suitable cylinder. Note that with a coarse texture like this you need to either match the `$fn` of the cylinder to the texture, or choose a sufficiently fine cylinder to avoid conflicting facets. @@ -1115,19 +1125,55 @@ function linear_sweep( // [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // ]; -// h = 20; -// r = 15; // ang = len(img[0])/len(img)*h/(2*PI*r)*360; -// rotate_sweep([[15,-10],[15,10]], texture=img, +// rotate_sweep([[r,-h/2],[r,h/2]], texture=img, // tex_reps=1,angle=ang, closed=false); // cyl(r=r,h=27,$fn=128); +// Example(3D,VPR=[68.30,0.00,148.90],VPD=91.85,VPT=[-0.56,5.78,-0.90],NoAxes): Above we explicitly calculated the required angle to produce the correct aspect ratio. Here we use `pixel_aspect` which produces an output whose average width has the desired aspect ratio. +// img = [ +// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], +// [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], +// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0,.5,.5, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0,.5,.5, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], +// [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], +// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], +// ]; +// rotate_sweep([[15,-10],[5,10]], texture=img, +// tex_reps=[1,1], closed=false, pixel_aspect=1); +// cyl(r1=16,r2=4,h=24,$fn=128); +// Example(3D,VPR=[96.30,0.00,133.50],VPD=54.24,VPT=[1.94,2.85,-0.47]): Here we apply the texture to a sphere using the automatic `pixel_aspect` to determine the angle. Note that using {{spheroid()}} with the circum option eliminates artifacts arising due to mimatched faceting. +// img = [ +// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], +// [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], +// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0,.5,.5, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0,.5,.5, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], +// [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], +// [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], +// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], +// ]; +// arc = arc(r=10, angle=[-44,44],n=100); +// rotate_sweep(arc, texture=img, tex_reps=[1,1], +// closed=false, pixel_aspect=1); +// spheroid(10,$fn=64,circum=true); + function rotate_sweep( shape, angle=360, texture, tex_size=[5,5], tex_counts, tex_reps, tex_inset=false, tex_rot=0, - tex_scale, tex_depth, tex_samples, + tex_scale, tex_depth, tex_samples, tex_aspect, pixel_aspect, tex_taper, shift=[0,0], closed=true, style="min_edge", cp="centroid", atype="hull", anchor="origin", @@ -1163,7 +1209,7 @@ function rotate_sweep( rot=tex_rot, samples=tex_samples, inhibit_y_slicing=_tex_inhibit_y_slicing, - taper=tex_taper, + taper=tex_taper, tex_aspect=tex_aspect, pixel_aspect=pixel_aspect, shift=shift, closed=closed, angle=angle, @@ -1197,7 +1243,7 @@ module rotate_sweep( tex_scale, tex_depth, tex_samples, tex_taper, shift=[0,0], style="min_edge", - closed=true, tex_extra, + closed=true, tex_extra, tex_aspect, pixel_aspect, cp="centroid", convexity=10, atype="hull", @@ -1234,12 +1280,12 @@ module rotate_sweep( rot=tex_rot, samples=tex_samples, taper=tex_taper, - shift=shift,tex_extra=tex_extra, + shift=shift,tex_extra=tex_extra,tex_aspect=tex_aspect, pixel_aspect=pixel_aspect, closed=closed, inhibit_y_slicing=_tex_inhibit_y_slicing, angle=angle, style=style, - atype=atype, anchor=anchor, + atype=atype, anchor=anchor, spin=spin, orient=orient, start=start ) children(); } else { @@ -4472,7 +4518,7 @@ function _textured_revolution( shape, texture, tex_size, tex_scale=1, inset=false, rot=false, shift=[0,0], taper, closed=true, angle=360, - inhibit_y_slicing=false, + inhibit_y_slicing=false,tex_aspect, pixel_aspect, counts, samples, start=0,tex_extra, style="min_edge", atype="intersect", anchor=CENTER, spin=0, orient=UP @@ -4488,6 +4534,8 @@ function _textured_revolution( assert(taper_is_ok, "Bad taper= value.") assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"") assert(is_undef(tex_extra) || is_finite(tex_extra) || is_vector(tex_extra,2), "tex_extra must be a number of 2-vector") + assert(num_defined([tex_aspect, pixel_aspect])<=1, "Cannot give both tex_aspect and pixel_aspect") + //assert(num_defined([tex_aspect, pixel_aspect])==0 || is_undef(angle), "Cannot give tex_aspect or pixel_aspect if you give angle") let( regions = !is_path(shape,2)? region_parts(shape) : closed? region_parts([shape]) @@ -4512,10 +4560,13 @@ function _textured_revolution( : is_def(tex_extra) ? force_list(tex_extra,2) : counts==[1,1] ? [0,0] : [1,1], - dummy = assert(is_undef(samples) || is_vnf(texture), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"), + dummy = assert(is_def(counts) || num_defined([pixel_aspect,tex_aspect])==0, "Must specify tex_counts (not tex_size) when using pixel_aspect or tex_aspect") + assert(is_undef(pixel_aspect) || !is_vnf(texture), "Cannot give pixel aspect with a VNF texture") + assert(is_undef(samples) || is_vnf(texture), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"), inset = is_num(inset)? inset : inset? 1 : 0, - samples = !is_vnf(texture)? len(texture) : - is_num(samples)? samples : 8, + samples = !is_vnf(texture)? len(texture) + : is_num(samples)? samples + : 8, bounds = pointlist_bounds(flatten(flatten(regions))), maxx = bounds[1].x, miny = bounds[0].y, @@ -4523,6 +4574,20 @@ function _textured_revolution( h = maxy - miny, circumf = 2 * PI * maxx, texcnt = is_vnf(texture) ? undef : [len(texture[0]), len(texture)], + angle = num_defined([tex_aspect,pixel_aspect])==0 ? angle + : let( + paths = flatten(regions), + lengths = [for(path=paths) path_length(path,closed=closed)], + ind = max_index(lengths), + rpath = resample_path(paths[ind], n=counts.y * samples + (closed?0:tex_extra.y), closed=closed), + h = path_length(rpath), + r = mean(column(rpath,0)), + width = counts.x/counts.y * (is_def(pixel_aspect) ? (texcnt.x+tex_extra.x-1)/(texcnt.y+tex_extra.y-1) : tex_aspect) * h + (is_def(pixel_aspect)?1:0), + ang = 360 * width / (2*PI*r), + ) + assert(ang<=360, str("Angle required for requested tile counts and aspect is ",ang, " which exceeds 360 degrees.")) + 360 * width / (2*PI*r), + tile = !is_vnf(texture) || samples==1 ? texture : let( @@ -4764,7 +4829,7 @@ module _textured_revolution( shape, texture, tex_size, tex_scale=1, inset=false, rot=false, shift=[0,0], taper, closed=true, angle=360, - style="min_edge", atype="intersect", + style="min_edge", atype="intersect",tex_aspect, pixel_aspect, inhibit_y_slicing=false,tex_extra, convexity=10, counts, samples, start=0, anchor=CENTER, spin=0, orient=UP @@ -4773,7 +4838,7 @@ module _textured_revolution( vnf = _textured_revolution( shape, texture, tex_size=tex_size, tex_scale=tex_scale, inset=inset, rot=rot, - taper=taper, closed=closed, style=style, + taper=taper, closed=closed, style=style,tex_aspect=tex_aspect, pixel_aspect=pixel_aspect, shift=shift, angle=angle,tex_extra=tex_extra, samples=samples, counts=counts, start=start, inhibit_y_slicing=inhibit_y_slicing From 9698a59693961c446612e3c29b717848b38a2934 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Mon, 21 Apr 2025 23:40:43 -0400 Subject: [PATCH 07/50] remove comma --- skin.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skin.scad b/skin.scad index 1188b5db..9ee44627 100644 --- a/skin.scad +++ b/skin.scad @@ -4583,7 +4583,7 @@ function _textured_revolution( h = path_length(rpath), r = mean(column(rpath,0)), width = counts.x/counts.y * (is_def(pixel_aspect) ? (texcnt.x+tex_extra.x-1)/(texcnt.y+tex_extra.y-1) : tex_aspect) * h + (is_def(pixel_aspect)?1:0), - ang = 360 * width / (2*PI*r), + ang = 360 * width / (2*PI*r) ) assert(ang<=360, str("Angle required for requested tile counts and aspect is ",ang, " which exceeds 360 degrees.")) 360 * width / (2*PI*r), From 00cebb68df9b86535617d3e32142bfd20d076e6b Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Tue, 22 Apr 2025 00:17:11 -0400 Subject: [PATCH 08/50] example fix --- skin.scad | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skin.scad b/skin.scad index 9ee44627..f0cffc4f 100644 --- a/skin.scad +++ b/skin.scad @@ -1125,6 +1125,8 @@ function linear_sweep( // [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // ]; +// h = 20; +// r = 15; // ang = len(img[0])/len(img)*h/(2*PI*r)*360; // rotate_sweep([[r,-h/2],[r,h/2]], texture=img, // tex_reps=1,angle=ang, closed=false); From 51d1951e720b1ec449864fcf582817bb22fcefe8 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Tue, 22 Apr 2025 17:31:46 -0400 Subject: [PATCH 09/50] add region support to offset_sweep (module only) --- rounding.scad | 148 +++++++++++++++++++++++++++++++++++++++++--------- skin.scad | 6 +- 2 files changed, 125 insertions(+), 29 deletions(-) diff --git a/rounding.scad b/rounding.scad index 9f90f578..d2aeed92 100644 --- a/rounding.scad +++ b/rounding.scad @@ -1412,11 +1412,14 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true, // "chamfer". Use the "chamfer" style offset only in cases where the number of steps is small or just one (such as when using // the `os_chamfer` profile type). // . +// The module form only can support a region as input. You can provide different profiles for the cutouts in a region using the `bottom_hole`, `top_hole` +// or `ends_hole` parameters. +// . // This module offers four anchor types. The default is "hull" in which VNF anchors are placed on the VNF of the **unrounded** object. You // can also use "intersect" to get the intersection anchors to the unrounded object. If you prefer anchors that respect the rounding // then use "surf_hull" or "intersect_hull". // Arguments: -// path = 2d path (list of points) to extrude +// path = 2d path (list of points) to extrude or a region for the module form // height / length / l / h = total height (including rounded portions, but not extra sections) of the output. Default: combined height of top and bottom end treatments. // bottom / bot = rounding spec for the bottom end // top = rounding spec for the top end. @@ -1434,6 +1437,9 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true, // angle = default angle for chamfers. Default: 45 // joint = default joint value for smooth roundover. // k = default curvature parameter value for "smooth" roundover +// ends_hole = (module only) rounding spec that applies to top and bottom of holes in a region +// bot_hole / bottom_hole = (module only) rounding spec for bottom end of holes in a region +// top_hole = (module only) rounding spec for top end of holes in a region // convexity = convexity setting for use with polyhedron. (module only) Default: 10 // anchor = Translate so anchor point is at the origin. Default: "base" // spin = Rotate this many degrees around Z axis after anchor. Default: 0 @@ -1573,6 +1579,84 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true, // star = star(5, r=220, ir=130); // rounded_star = round_corners(star, cut=flatten(repeat([5,0],5)), $fn=24); // offset_sweep(rounded_star, height=100, top=os_mask(ogee), bottom=os_mask(ogee,out=true)); +// Example(3D,NoAxes): Applying to a region, with different profiles for the outside in inside curves. +// $fn = 32; +// rgn = difference( +// [ +// rect(50, rounding=5), +// move([15,15], circle(d=10)), +// move([-15,-15], circle(d=10)), +// move([0,25], rect([10,7],anchor=BACK, +// rounding=[-2,-2,2,2])), +// zrot(55, square([4, 100], center=true)), +// ellipse([12,4]) +// ] +// ); +// offset_sweep(rgn, height=12, steps=6, ends_hole=os_chamfer(width=2), +// ends=os_circle(r=1.7)); + + +module _offset_sweep_region(region, height, + bottom, top, + h, l, length, ends, bot, top_hole, bot_hole, bottom_hole, ends_hole, + offset="round", r=0, steps=16, + quality=1, check_valid=true, + extra=0, + cut=undef, chamfer_width=undef, chamfer_height=undef, + joint=undef, k=0.75, angle=45, + convexity=10,anchor="base",cp="centroid", + spin=0, orient=UP, atype="hull") +{ + connected_reg = region_parts(region); + vnf_h_list = [for(reg=connected_reg) + offset_sweep(path=reg[0], height=height, h=h, l=l, length=length, bot=bot, top=top, bottom=bottom, ends=ends, + offset=offset, r=r, steps=steps, + quality=quality, check_valid=check_valid, extra=extra, cut=cut, chamfer_width=chamfer_width, + chamfer_height=chamfer_height, joint=joint, k=k, angle=angle, _return_height=true)]; + vnf_list = column(vnf_h_list,0); + height = vnf_h_list[0][1]; + + holes = [for(reg=connected_reg, i=[1:1:len(reg)-1]) reg[i]]; + + anchors = [ + named_anchor("zcenter", [0,0,0], UP), + named_anchor("base", [0,0,-height/2], UP), + named_anchor("top", [0,0,height/2], UP) + ]; + bottom_hole=first_defined([bottom_hole, bot_hole, ends_hole, bottom, ends]); + top_hole = first_defined([top_hole,ends_hole,top,ends]); + if (in_list(atype,["hull","intersect"])) + attachable(anchor,spin,orient,region=region,h=height,cp=cp,anchors=anchors,extent=atype=="hull"){ + down(height/2) + difference(){ + for(vnf=vnf_list) + polyhedron(vnf[0],vnf[1],convexity=convexity); + for(path=holes) + offset_sweep(path=path, height=height, h=h, l=l, length=length, bot=bot, top=top_hole, bottom=bottom_hole, + offset=offset, r=r, steps=steps, + quality=quality, check_valid=check_valid, extra=extra+0.1, cut=cut, chamfer_width=chamfer_width, + chamfer_height=chamfer_height, joint=joint, k=k, angle=angle, _flipdir=true,convexity=convexity); + } + children(); + } + else { + allvnf=vnf_join(vnf_list); + attachable(anchor,spin.orient,vnf=allvnf, cp=cp,anchors=anchors, extent = atype=="surf_hull"){ + difference(){ + for(vnf=vnf_list) + vnf_polyhedron(vnf,convexity=convexity); + for(path=holes) + offset_sweep(path=path, height=height, h=h, l=l, length=length, bot=bot, top=top_hole, bottom=bottom_hole, + offset=offset, r=r, steps=steps, + quality=quality, check_valid=check_valid, extra=extra+0.1, cut=cut, chamfer_width=chamfer_width, + chamfer_height=chamfer_height, joint=joint, k=k, angle=angle, _flipdir=true,convexity=convexity); + } + children(); + } + } +} + + // This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce @@ -1630,7 +1714,7 @@ function offset_sweep( extra=0, caps=true, cut=undef, chamfer_width=undef, chamfer_height=undef, joint=undef, k=0.75, angle=45, anchor="base", orient=UP, spin=0,atype="hull", cp="centroid", - _return_height=false + _return_height=false, _flipdir=false ) = let( argspec = [ @@ -1668,8 +1752,9 @@ function offset_sweep( ) assert(offsetsok,"Offsets must be one of \"round\", \"delta\", or \"chamfer\"") let( - offsets_bot = _rounding_offsets(bottom, -1), - offsets_top = _rounding_offsets(top, 1), + do_flip = _flipdir ? function(x) xflip(x) : function(x) x , + offsets_bot = do_flip(_rounding_offsets(bottom, -1)), + offsets_top = do_flip(_rounding_offsets(top, 1)), dummy = (struct_val(top,"offset")=="chamfer" && len(offsets_top)>5) || (struct_val(bottom,"offset")=="chamfer" && len(offsets_bot)>5) ? echo("WARNING: You have selected offset=\"chamfer\", which leads to exponential growth in the vertex count and requested more than 5 layers. This can be slow or run out of recursion depth.") @@ -1722,39 +1807,48 @@ function offset_sweep( : reorient(anchor,spin,orient, vnf=vnf, p=vnf, extent=atype=="surf_hull", cp=cp, anchors=anchors) ) _return_height ? [final_vnf,height] : final_vnf; + module offset_sweep(path, height, bottom, top, h, l, length, ends, bot, offset="round", r=0, steps=16, quality=1, check_valid=true, - extra=0, + extra=0, top_hole, bot_hole, bottom_hole, ends_hole, cut=undef, chamfer_width=undef, chamfer_height=undef, joint=undef, k=0.75, angle=45, convexity=10,anchor="base",cp="centroid", - spin=0, orient=UP, atype="hull") + spin=0, orient=UP, atype="hull", _flipdir) { assert(in_list(atype, ["intersect","hull","surf_hull","surf_intersect"]), "Anchor type must be \"hull\" or \"intersect\""); - vnf_h = offset_sweep(path=path, height=height, h=h, l=l, length=length, bot=bot, top=top, bottom=bottom, ends=ends, - offset=offset, r=r, steps=steps, - quality=quality, check_valid=check_valid, extra=extra, cut=cut, chamfer_width=chamfer_width, - chamfer_height=chamfer_height, joint=joint, k=k, angle=angle, _return_height=true); - vnf = vnf_h[0]; - height = vnf_h[1]; - anchors = [ - named_anchor("zcenter", [0,0,0], UP), - named_anchor("base", [0,0,-height/2], UP), - named_anchor("top", [0,0,height/2], UP) - ]; - if (in_list(atype,["hull","intersect"])) - attachable(anchor,spin,orient,region=force_region(path),h=height,cp=cp,anchors=anchors,extent=atype=="hull"){ - down(height/2)polyhedron(vnf[0],vnf[1],convexity=convexity); - children(); - } - else - attachable(anchor,spin.orient,vnf=vnf, cp=cp,anchors=anchors, extent = atype=="surf_hull"){ - vnf_polyhedron(vnf,convexity=convexity); - children(); - } + if (is_region(path) && len(path)>1) + _offset_sweep_region(region=path, height=height, bottom=bottom, top=top, h=h, l=l, length=length, ends=ends, bot=bot, + offset=offset, r=r, steps=steps, quality=quality, check_valid=check_valid, extra=extra, + cut=cut, chamfer_width=chamfer_width, chamfer_height=chamfer_height, joint=joint, k=k, angle=angle, + bot_hole=bot_hole,top_hole=top_hole,bottom_hole=bottom_hole,ends_hole=ends_hole, + convexity=convexity, anchor=anchor, cp=cp, spin=spin, orient=orient, atype=atype) children(); + else { + vnf_h = offset_sweep(path=path, height=height, h=h, l=l, length=length, bot=bot, top=top, bottom=bottom, ends=ends, + offset=offset, r=r, steps=steps, + quality=quality, check_valid=check_valid, extra=extra, cut=cut, chamfer_width=chamfer_width, + chamfer_height=chamfer_height, joint=joint, k=k, angle=angle, _return_height=true, _flipdir=_flipdir); + vnf = vnf_h[0]; + height = vnf_h[1]; + anchors = [ + named_anchor("zcenter", [0,0,0], UP), + named_anchor("base", [0,0,-height/2], UP), + named_anchor("top", [0,0,height/2], UP) + ]; + if (in_list(atype,["hull","intersect"])) + attachable(anchor,spin,orient,region=force_region(path),h=height,cp=cp,anchors=anchors,extent=atype=="hull"){ + down(height/2)polyhedron(vnf[0],vnf[1],convexity=convexity); + children(); + } + else + attachable(anchor,spin.orient,vnf=vnf, cp=cp,anchors=anchors, extent = atype=="surf_hull"){ + vnf_polyhedron(vnf,convexity=convexity); + children(); + } + } } diff --git a/skin.scad b/skin.scad index f0cffc4f..db2e838a 100644 --- a/skin.scad +++ b/skin.scad @@ -903,9 +903,11 @@ function linear_sweep( // . // If you want to place just one or a few copies of a texture onto an object rather than texturing the entire object you can do that by using // and angle smaller than 360. However, if you want to control the aspect ratio of the resulting texture you will have to carefully calculate the proper -// angle to use. To simplify this process you can use `pixel_aspect` or `tex_aspect`. You can set `tex_aspect` for any type of tile and it specifies +// angle to use to ensure that the arc length in the horizontal direction is the proper length compared to the arc length in the vertical direction. +// To simplify this process you can use `pixel_aspect` or `tex_aspect`. You can set `tex_aspect` for any type of tile and it specifies // the desired aspect ratio (width/height) for the tiles. You must specify `tex_reps` in order to use this feature. For heightfields you can instead provide -// a pixel aspect ratio, which is suited to the case where your texture is a non-square image that you want to place on a curved object. +// a pixel aspect ratio, which is suited to the case where your texture is a non-square image that you want to place on a curved object. For a simple cylinder +// it is obvious what the horizontal arc length is; for other objects this is computed based on the average radius of the longest path in `shape`. // Arguments: // shape = The polygon or [region](regions.scad) to sweep around the Z axis. // angle = If given, specifies the number of degrees to sweep the region around the Z axis, counterclockwise from the X+ axis. Default: 360 (full rotation) From 4c80be0feb2a8cd6caaa48d0522f98d7fafefb15 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Tue, 22 Apr 2025 18:23:06 -0400 Subject: [PATCH 10/50] remove tex_size defaults for textured_tile, vnf_vertex_array --- shapes3d.scad | 5 ++--- skin.scad | 11 ++++++----- vnf.scad | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/shapes3d.scad b/shapes3d.scad index 8cc8d555..5ae2e1cb 100644 --- a/shapes3d.scad +++ b/shapes3d.scad @@ -1334,10 +1334,9 @@ function textured_tile( ) = 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(num_defined([tex_size, tex_reps])==1, "Must give exactly one of 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") - let( inset = is_num(tex_inset)? tex_inset : tex_inset? 1 : 0, default_thick = inset>0 ? 0.1+abs(tex_depth)*inset : 0.1, @@ -1362,7 +1361,7 @@ function textured_tile( texture = _get_texture(texture, tex_rot), tex_reps = is_def(tex_reps) ? force_list(tex_reps,2) - : let(tex_size=is_undef(tex_size)? [5,5] : force_list(tex_size,2)) + : let(tex_size=force_list(tex_size,2)) [round(size.x/tex_size.x), round(size.y/tex_size.y)], extra = is_undef(extra)? tex_reps == [1,1] ? [0,0] : [1,1] : force_list(tex_extra,2), diff --git a/skin.scad b/skin.scad index db2e838a..78954185 100644 --- a/skin.scad +++ b/skin.scad @@ -1182,7 +1182,7 @@ function rotate_sweep( style="min_edge", cp="centroid", atype="hull", anchor="origin", spin=0, orient=UP, start=0, - _tex_inhibit_y_slicing=false + _tex_inhibit_y_slicing ) = assert(num_defined([tex_reps,tex_counts])<2, "In rotate_sweep() the 'tex_counts' parameters has been replaced by 'tex_reps'. You cannot give both.") assert(num_defined([tex_scale,tex_depth])<2, "In linear_sweep() the 'tex_scale' parameter has been replaced by 'tex_depth'. You cannot give both.") @@ -4498,7 +4498,7 @@ function _tile_edge_path_list(vnf, axis, maxopen=1) = /// Arguments: /// shape = The path or region to sweep/extrude. /// texture = A texture name string, or a rectangular array of scalar height values (0.0 to 1.0), or a VNF tile that defines the texture to apply to the revolution surface. See {{texture()}} for what named textures are supported. -/// tex_size = An optional 2D target size for the textures. Actual texture sizes are scaled somewhat to evenly fit the available surface. Default: `[5,5]` +/// tex_size = An optional 2D target size for the textures. Actual texture sizes are scaled somewhat to evenly fit the available surface. /// tex_scale = Scaling multiplier for the texture depth. /// --- /// inset = If numeric, lowers the texture into the surface by that amount, before the tex_scale multiplier is applied. If `true`, insets by exactly `1`. Default: `false` @@ -4522,7 +4522,7 @@ function _textured_revolution( shape, texture, tex_size, tex_scale=1, inset=false, rot=false, shift=[0,0], taper, closed=true, angle=360, - inhibit_y_slicing=false,tex_aspect, pixel_aspect, + inhibit_y_slicing,tex_aspect, pixel_aspect, counts, samples, start=0,tex_extra, style="min_edge", atype="intersect", anchor=CENTER, spin=0, orient=UP @@ -4541,6 +4541,7 @@ function _textured_revolution( assert(num_defined([tex_aspect, pixel_aspect])<=1, "Cannot give both tex_aspect and pixel_aspect") //assert(num_defined([tex_aspect, pixel_aspect])==0 || is_undef(angle), "Cannot give tex_aspect or pixel_aspect if you give angle") let( + inhibit_y_slicing = default(inhibit_y_slicing, is_path(shape) && len(path)==2 ? true : false), regions = !is_path(shape,2)? region_parts(shape) : closed? region_parts([shape]) : let( @@ -4861,7 +4862,7 @@ function _textured_point_array(points, texture, tex_reps, tex_size, tex_samples, col_wrap=false, tex_depth=1, row_wrap=false, caps, cap1, cap2, reverse=false, style="min_edge", tex_extra, tex_skip, sidecaps,sidecap1,sidecap2,normals) = 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_num(tex_size) || is_vector(tex_size,2), "tex_size must be a scalar or 2-vector") - assert(num_defined([tex_size, tex_reps])<2, "Cannot give both tex_size and tex_reps") + assert(num_defined([tex_size, tex_reps])==1, "Must give exactly one of tex_size and tex_reps") assert(in_list(style,["default","alt","quincunx", "convex","concave", "min_edge","min_area","flip1","flip2"])) assert(is_matrix(points[0], n=3),"Point array has the wrong shape or points are not 3d") assert(is_consistent(points), "Non-rectangular or invalid point array") @@ -4877,7 +4878,7 @@ function _textured_point_array(points, texture, tex_reps, tex_size, tex_samples, ptsize=[len(points[0]), len(points)], tex_reps = is_def(tex_reps) ? force_list(tex_reps,2) : let( - tex_size = is_undef(tex_size) ? [5,5] : force_list(tex_size,2), + tex_size = force_list(tex_size,2), xsize = norm(points[0][0]-points[0][1])*(ptsize.x+(col_wrap?1:0)), ysize = norm(points[0][0]-points[1][0])*(ptsize.y+(row_wrap?1:0)) ) diff --git a/vnf.scad b/vnf.scad index f89665e0..a8cf8311 100644 --- a/vnf.scad +++ b/vnf.scad @@ -86,7 +86,7 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces. // triangulate = If true, triangulates endcaps to resolve possible CGAL issues. This can be an expensive operation if the endcaps are complex. Default: false // convexity = (module) Max number of times a line could intersect a wall of the shape. // texture = A texture name string, or a rectangular array of scalar height values (0.0 to 1.0), or a VNF tile that defines the texture to apply to vertical surfaces. See {{texture()}} for what named textures are supported. -// tex_size = An optional 2D target size for the textures at `points[0][0]`. Actual texture sizes are scaled somewhat to evenly fit the available surface. Default: `[5,5]` +// tex_size = An optional 2D target size for the textures at `points[0][0]`. Actual texture sizes are scaled somewhat to evenly fit the available surface. // tex_reps = If given instead of tex_size, a 2-vector giving the number of texture tile repetitions in the horizontal and vertical directions. // tex_inset = If numeric, lowers the texture into the surface by the specified proportion, e.g. 0.5 would lower it half way into the surface. If `true`, insets by exactly its full depth. Default: `false` // tex_rot = Rotate texture by specified angle, which must be a multiple of 90 degrees. Default: 0 From 10a0264a919af9ab54dcc334d294d23959d60771 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Tue, 22 Apr 2025 19:54:14 -0400 Subject: [PATCH 11/50] refix --- skin.scad | 2 +- vnf.scad | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skin.scad b/skin.scad index 78954185..f10f0d75 100644 --- a/skin.scad +++ b/skin.scad @@ -4541,7 +4541,7 @@ function _textured_revolution( assert(num_defined([tex_aspect, pixel_aspect])<=1, "Cannot give both tex_aspect and pixel_aspect") //assert(num_defined([tex_aspect, pixel_aspect])==0 || is_undef(angle), "Cannot give tex_aspect or pixel_aspect if you give angle") let( - inhibit_y_slicing = default(inhibit_y_slicing, is_path(shape) && len(path)==2 ? true : false), + inhibit_y_slicing = default(inhibit_y_slicing, is_path(shape) && len(shape)==2 ? true : false), regions = !is_path(shape,2)? region_parts(shape) : closed? region_parts([shape]) : let( diff --git a/vnf.scad b/vnf.scad index 33da16b7..67a2b67b 100644 --- a/vnf.scad +++ b/vnf.scad @@ -260,7 +260,7 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces. // path3d( // smooth_path( // zrot(ridgeamp*sin(360*z/ridgepd), p=star(11, or=r+ridgeamp, ir=r-ridgeamp)), -// relsize=0.6, splinesteps=5, method="corners", closed=true, tex_size=5), +// relsize=0.6, splinesteps=5, method="corners", closed=true), // z) // ]; // vnf_polyhedron(vnf_vertex_array(polystack, col_wrap=true, caps=true)); @@ -278,11 +278,11 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces. // path3d( // smooth_path( // zrot(ridgeamp*sin(360*z/ridgepd), p=star(11, or=r+ridgeamp, ir=r-ridgeamp)), -// relsize=0.6, splinesteps=5, method="corners", closed=true, tex_size=5), +// relsize=0.6, splinesteps=5, method="corners", closed=true), // z) // ]; // vnf_polyhedron(vnf_vertex_array(polystack, col_wrap=true, caps=true, -// texture="dots", tex_samples=1)); +// texture="dots", tex_samples=1, tex_size=5)); // Example(3D,Med,NoAxes,VPR=[0,0,0],VPD=126.00,VPT=[-0.35,-0.54,4.09]): This point array defines a square region but with a non-uniform grid. // pts = [for(x=[-1:.1:1]) // [for(y=[-1:.1:1]) From dbe5e6e10c980231fd0f99725670c482ddd6bc8d Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Tue, 22 Apr 2025 23:45:39 -0700 Subject: [PATCH 12/50] Added img2scad.html web app to scripts directory --- scripts/img2scad.html | 406 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 scripts/img2scad.html diff --git a/scripts/img2scad.html b/scripts/img2scad.html new file mode 100644 index 00000000..e60aacf1 --- /dev/null +++ b/scripts/img2scad.html @@ -0,0 +1,406 @@ + + + + + Image to OpenSCAD array, v5 + + + + +

Convert image to OpenSCAD array

+

This utility accepts any raster image and converts it to grayscale expanded to use the maximum possible luminance range. Alpha channel is ignored. After resizing, rotating, or reflecting the image as desired, you may save it as an OpenSCAD array.

+
+
+
+
+

Select an image

+ +

You can also paste an image (Ctrl+V) into this section from your clipboard.

+
+ +
+

+ +
+
+ +
+
+ +

Transformations

+ +
+ +
+ + + +

Appearance

+ +
+ + +
+ +
+
+ + +
+ +

Output

+ +
+ + +
+ +
+
+
+ +
+

+
+ +
+
+
+ +
+
+ + + + + From a635ff68064ab4c8deb88ba6a82fa11c47353e70 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Wed, 23 Apr 2025 19:40:47 -0400 Subject: [PATCH 13/50] path_text() bugfix, replace im2scad.py with img2tex.py --- scripts/img2scad.py | 87 +++++++++++++++++++++++---- scripts/img2tex.py | 142 -------------------------------------------- shapes3d.scad | 2 +- 3 files changed, 76 insertions(+), 155 deletions(-) delete mode 100755 scripts/img2tex.py diff --git a/scripts/img2scad.py b/scripts/img2scad.py index 99cc777b..6d1e9494 100755 --- a/scripts/img2scad.py +++ b/scripts/img2scad.py @@ -6,24 +6,60 @@ import sys import os.path import argparse -from PIL import Image +from PIL import Image, ImageFilter, ImageOps -def img2scad(filename, varname, resize, outf): +def img2tex(filename, opts, outf): indent = " " * 4 im = Image.open(filename).convert('L') - if resize: - print("Resizing to {}x{}".format(resize[0],resize[1])) - im = im.resize(resize) + if opts.resize: + print("Resizing to {}x{}".format(opts.resize[0], opts.resize[1])) + im = im.resize(opts.resize) + if opts.invert: + print("Inverting luminance.") + im = ImageOps.invert(im) + if opts.blur: + print("Blurring, radius={}.".format(opts.blur)) + im = im.filter(ImageFilter.BoxBlur(opts.blur)) + if opts.rotate: + if opts.rotate in (-90, 270): + print("Rotating 90 degrees clockwise.".format(opts.rotate)) + elif opts.rotate in (90, -270): + print("Rotating 90 degrees counter-clockwise.".format(opts.rotate)) + elif opts.rotate in (180, -180): + print("Rotating 180 degrees.".format(opts.rotate)) + im = im.rotate(opts.rotate, expand=True) + if opts.mirror_x: + print("Mirroring left-to-right.") + im = im.transpose(Image.FLIP_LEFT_RIGHT) + if opts.mirror_y: + print("Mirroring top-to-bottom.") + im = im.transpose(Image.FLIP_TOP_BOTTOM) pix = im.load() width, height = im.size print("// Image {} ({}x{})".format(filename, width, height), file=outf) - print("{} = [".format(varname), file=outf) - line = indent - for x in range(width): - line += "[ " + + if opts.range == "dynamic": + pixmin = 255; + pixmax = 0; for y in range(height): - line += "{:d}, ".format(pix[x,y]) + for x in range(width): + pixmin = min(pixmin, pix[x,y]) + pixmax = max(pixmax, pix[x,y]) + else: + pixmin = 0; + pixmax = 255; + print("// Original luminances: min={}, max={}".format(pixmin, pixmax), file=outf) + print("// Texture heights: min={}, max={}".format(opts.minout, opts.maxout), file=outf) + + print("{} = [".format(opts.varname), file=outf) + line = indent + for y in range(height): + line += "[ " + for x in range(width): + u = (pix[x,y] - pixmin) / (pixmax - pixmin) + val = u * (opts.maxout - opts.minout) + opts.minout + line += "{:.3f}".format(val).rstrip('0').rstrip('.') + ", " if len(line) > 60: print(line, file=outf) line = indent * 2 @@ -35,14 +71,38 @@ def img2scad(filename, varname, resize, outf): print("", file=outf) +def check_nonneg_float(value): + val = float(value) + if val < 0: + raise argparse.ArgumentTypeError("{} is an invalid non-negative float value".format(val)) + return val + + def main(): parser = argparse.ArgumentParser(prog='img2scad') parser.add_argument('-o', '--outfile', help='Output .scad file.') parser.add_argument('-v', '--varname', help='Variable to use in .scad file.') + parser.add_argument('-i', '--invert', action='store_true', + help='Invert luminance values.') parser.add_argument('-r', '--resize', help='Resample image to WIDTHxHEIGHT.') + parser.add_argument('-R', '--rotate', choices=(-270, -180, -90, 0, 90, 180, 270), default=0, type=int, + help='Rotate output by the given number of degrees.') + parser.add_argument('--mirror-x', action="store_true", + help='Mirror output in the X direction.') + parser.add_argument('--mirror-y', action="store_true", + help='Mirror output in the Y direction.') + parser.add_argument('--blur', type=check_nonneg_float, default=0, + help='Perform a box blur on the output with the given radius.') + parser.add_argument('--minout', type=float, default=0.0, + help='The value to output for the minimum luminance.') + parser.add_argument('--maxout', type=float, default=1.0, + help='The value to output for the maximum luminance.') + parser.add_argument('--range', choices=["dynamic", "full"], default="dynamic", + help='If "dynamic", the lowest to brightest luminances are scaled to the minout/maxout range.\n' + 'If "full", 0 to 255 luminances will be scaled to the minout/maxout range.') parser.add_argument('infile', help='Input image file.') opts = parser.parse_args() @@ -54,6 +114,9 @@ def main(): else: opts.varname = "image_data" size_pat = re.compile(r'^([0-9][0-9]*)x([0-9][0-9]*)$') + + opts.invert = bool(opts.invert) + if opts.resize: m = size_pat.match(opts.resize) if not m: @@ -67,9 +130,9 @@ def main(): if opts.outfile: with open(opts.outfile, "w") as outf: - img2scad(opts.infile, opts.varname, opts.resize, outf) + img2tex(opts.infile, opts, outf) else: - img2scad(opts.infile, opts.varname, opts.resize, sys.stdout) + img2tex(opts.infile, opts, sys.stdout) sys.exit(0) diff --git a/scripts/img2tex.py b/scripts/img2tex.py deleted file mode 100755 index 32e9915e..00000000 --- a/scripts/img2tex.py +++ /dev/null @@ -1,142 +0,0 @@ -#!env python3 - -import re -import os -import sys -import os.path -import argparse - -from PIL import Image, ImageFilter, ImageOps - - -def img2tex(filename, opts, outf): - indent = " " * 4 - im = Image.open(filename).convert('L') - if opts.resize: - print("Resizing to {}x{}".format(opts.resize[0], opts.resize[1])) - im = im.resize(opts.resize) - if opts.invert: - print("Inverting luminance.") - im = ImageOps.invert(im) - if opts.blur: - print("Blurring, radius={}.".format(opts.blur)) - im = im.filter(ImageFilter.BoxBlur(opts.blur)) - if opts.rotate: - if opts.rotate in (-90, 270): - print("Rotating 90 degrees clockwise.".format(opts.rotate)) - elif opts.rotate in (90, -270): - print("Rotating 90 degrees counter-clockwise.".format(opts.rotate)) - elif opts.rotate in (180, -180): - print("Rotating 180 degrees.".format(opts.rotate)) - im = im.rotate(opts.rotate, expand=True) - if opts.mirror_x: - print("Mirroring left-to-right.") - im = im.transpose(Image.FLIP_LEFT_RIGHT) - if opts.mirror_y: - print("Mirroring top-to-bottom.") - im = im.transpose(Image.FLIP_TOP_BOTTOM) - pix = im.load() - width, height = im.size - print("// Image {} ({}x{})".format(filename, width, height), file=outf) - - if opts.range == "dynamic": - pixmin = 255; - pixmax = 0; - for y in range(height): - for x in range(width): - pixmin = min(pixmin, pix[x,y]) - pixmax = max(pixmax, pix[x,y]) - else: - pixmin = 0; - pixmax = 255; - print("// Original luminances: min={}, max={}".format(pixmin, pixmax), file=outf) - print("// Texture heights: min={}, max={}".format(opts.minout, opts.maxout), file=outf) - - print("{} = [".format(opts.varname), file=outf) - line = indent - for y in range(height): - line += "[ " - for x in range(width): - u = (pix[x,y] - pixmin) / (pixmax - pixmin) - val = u * (opts.maxout - opts.minout) + opts.minout - line += "{:.3f}".format(val).rstrip('0').rstrip('.') + ", " - if len(line) > 60: - print(line, file=outf) - line = indent * 2 - line += " ]," - if line != indent: - print(line, file=outf) - line = indent - print("];", file=outf) - print("", file=outf) - - -def check_nonneg_float(value): - val = float(value) - if val < 0: - raise argparse.ArgumentTypeError("{} is an invalid non-negative float value".format(val)) - return val - - -def main(): - parser = argparse.ArgumentParser(prog='img2tex') - parser.add_argument('-o', '--outfile', - help='Output .scad file.') - parser.add_argument('-v', '--varname', - help='Variable to use in .scad file.') - parser.add_argument('-i', '--invert', action='store_true', - help='Invert luminance values.') - parser.add_argument('-r', '--resize', - help='Resample image to WIDTHxHEIGHT.') - parser.add_argument('-R', '--rotate', choices=(-270, -180, -90, 0, 90, 180, 270), default=0, type=int, - help='Rotate output by the given number of degrees.') - parser.add_argument('--mirror-x', action="store_true", - help='Mirror output in the X direction.') - parser.add_argument('--mirror-y', action="store_true", - help='Mirror output in the Y direction.') - parser.add_argument('--blur', type=check_nonneg_float, default=0, - help='Perform a box blur on the output with the given radius.') - parser.add_argument('--minout', type=float, default=0.0, - help='The value to output for the minimum luminance.') - parser.add_argument('--maxout', type=float, default=1.0, - help='The value to output for the maximum luminance.') - parser.add_argument('--range', choices=["dynamic", "full"], default="dynamic", - help='If "dynamic", the lowest to brightest luminances are scaled to the minout/maxout range.\n' - 'If "full", 0 to 255 luminances will be scaled to the minout/maxout range.') - parser.add_argument('infile', help='Input image file.') - opts = parser.parse_args() - - non_alnum = re.compile(r'[^a-zA-Z0-9_]') - if not opts.varname: - if opts.outfile: - opts.varname = os.path.splitext(os.path.basename(opts.outfile))[0] - opts.varname = non_alnum.sub("", opts.varname) - else: - opts.varname = "image_data" - size_pat = re.compile(r'^([0-9][0-9]*)x([0-9][0-9]*)$') - - opts.invert = bool(opts.invert) - - if opts.resize: - m = size_pat.match(opts.resize) - if not m: - print("Expected WIDTHxHEIGHT resize format.", file=sys.stderr) - sys.exit(-1) - opts.resize = (int(m.group(1)), int(m.group(2))) - - if not opts.varname or non_alnum.search(opts.varname): - print("Bad variable name: {}".format(opts.varname), file=sys.stderr) - sys.exit(-1) - - if opts.outfile: - with open(opts.outfile, "w") as outf: - img2tex(opts.infile, opts, outf) - else: - img2tex(opts.infile, opts, sys.stdout) - - sys.exit(0) - - -if __name__ == "__main__": - main() - diff --git a/shapes3d.scad b/shapes3d.scad index 5ae2e1cb..9a57bd80 100644 --- a/shapes3d.scad +++ b/shapes3d.scad @@ -4114,7 +4114,7 @@ module path_text(path, text, font, size, thickness, lettersize, offset=0, revers frame_map( x=point3d(tangent-adjustment), y=point3d(usetop ? toppts[i] : -normpts[i]) - ) left(lsize[0]/2) { + ) left(lsize[i]/2) { text(text[i], font=font, size=size, language=language, script=script); } } From 30a913996db1e57be23dad8be358b6452303bd51 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Wed, 23 Apr 2025 20:47:54 -0700 Subject: [PATCH 14/50] Added cropping UI to img2scad.html --- scripts/img2scad.html | 270 +++++++++++++++++++++++++++++++++++------- 1 file changed, 228 insertions(+), 42 deletions(-) diff --git a/scripts/img2scad.html b/scripts/img2scad.html index e60aacf1..b6648975 100644 --- a/scripts/img2scad.html +++ b/scripts/img2scad.html @@ -59,6 +59,62 @@ Versions 1-5: 22 April 2025 - by Alex Matulich .tooltip:hover .tooltiptext { visibility: visible; + } + + /* cropping control panel stuff */ + + .crop-container { + display: grid; + grid-template-areas: + "top top top" + "left center right" + "bottom bottom bottom"; + grid-template-columns: auto 60px auto; + grid-template-rows: auto 60px auto; + gap: 4px; + padding: 2px; + box-sizing: border-box; + width: fit-content; + height: fit-content; + margin-top: 8px; + } + .crop-center { + grid-area: center; + display: flex; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + border: 2px dashed #ccc; + background-color: #eee; + font-weight: bold; + font-size: 1rem; + box-sizing: border-box; + } + .crop-control { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 0.9rem; + } + .crop-control input[type="number"] { + width: 6ch; + padding: 2px; + font-size: 1rem; + text-align: right; + border: 1px solid #ccc; + border-radius: 3px; + } + .crop-top { grid-area: top; } + .crop-left { grid-area: left; } + .crop-right { grid-area: right; } + .crop-bottom { + grid-area: bottom; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } @@ -82,15 +138,34 @@ Versions 1-5: 22 April 2025 - by Alex Matulich
-

Transformations

-
+

+
+
+ + +
+
+ + +
+
Crop
+
+ + +
+
+ + +
+
+

Appearance