This commit is contained in:
Alex Matulich 2025-01-13 03:16:54 +00:00 committed by GitHub
commit 9091fc1c12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 876 additions and 472 deletions

View file

@ -24,6 +24,7 @@ PrioritizeFiles:
shapes2d.scad shapes2d.scad
shapes3d.scad shapes3d.scad
drawing.scad drawing.scad
text.scad
masks2d.scad masks2d.scad
masks3d.scad masks3d.scad
distributors.scad distributors.scad

View file

@ -2067,8 +2067,6 @@ function reuleaux_polygon(n=3, r, d, anchor=CENTER, spin=0) =
// Function&Module: supershape() // Function&Module: supershape()
// Synopsis: Creates a 2D [Superformula](https://en.wikipedia.org/wiki/Superformula) shape. // Synopsis: Creates a 2D [Superformula](https://en.wikipedia.org/wiki/Superformula) shape.
// SynTags: Geom, Path // SynTags: Geom, Path
@ -2156,118 +2154,6 @@ function _superformula(theta,m1,m2,n1,n2=1,n3=1,a=1,b=1) =
// Section: Text
// Module: text()
// Synopsis: Creates an attachable block of text.
// SynTags: Geom
// Topics: Attachments, Text
// See Also: text3d(), attachable()
// Usage:
// text(text, [size], [font], ...);
// Description:
// Creates a 3D text block that can be attached to other attachable objects.
// You cannot attach children to text.
// .
// Historically fonts were specified by their "body size", the height of the metal body
// on which the glyphs were cast. This means the size was an upper bound on the size
// of the font glyphs, not a direct measurement of their size. In digital typesetting,
// the metal body is replaced by an invisible box, the em square, whose side length is
// defined to be the font's size. The glyphs can be contained in that square, or they
// can extend beyond it, depending on the choices made by the font designer. As a
// result, the meaning of font size varies between fonts: two fonts at the "same" size
// can differ significantly in the actual size of their characters. Typographers
// customarily specify the size in the units of "points". A point is 1/72 inch. In
// OpenSCAD, you specify the size in OpenSCAD units (often treated as millimeters for 3d
// printing), so if you want points you will need to perform a suitable unit conversion.
// In addition, the OpenSCAD font system has a bug: if you specify size=s you will
// instead get a font whose size is s/0.72. For many fonts this means the size of
// capital letters will be approximately equal to s, because it is common for fonts to
// use about 70% of their height for the ascenders in the font. To get the customary
// font size, you should multiply your desired size by 0.72.
// .
// To find the fonts that you have available in your OpenSCAD installation,
// go to the Help menu and select "Font List".
// Arguments:
// text = Text to create.
// size = The font will be created at this size divided by 0.72. Default: 10
// font = Font to use. Default: "Liberation Sans" (standard OpenSCAD default)
// ---
// halign = If given, specifies the horizontal alignment of the text. `"left"`, `"center"`, or `"right"`. Overrides `anchor=`.
// valign = If given, specifies the vertical alignment of the text. `"top"`, `"center"`, `"baseline"` or `"bottom"`. Overrides `anchor=`.
// spacing = The relative spacing multiplier between characters. Default: `1.0`
// direction = The text direction. `"ltr"` for left to right. `"rtl"` for right to left. `"ttb"` for top to bottom. `"btt"` for bottom to top. Default: `"ltr"`
// language = The language the text is in. Default: `"en"`
// script = The script the text is in. Default: `"latin"`
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"baseline"`
// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#subsection-spin). Default: `0`
// Named Anchors:
// "baseline" = Anchors at the baseline of the text, at the start of the string.
// str("baseline",VECTOR) = Anchors at the baseline of the text, modified by the X and Z components of the appended vector.
// Examples(2D):
// text("Foobar", size=10);
// text("Foobar", size=12, font="Liberation Mono");
// text("Foobar", anchor=CENTER);
// text("Foobar", anchor=str("baseline",CENTER));
// Example: Using line_copies() distributor
// txt = "This is the string.";
// line_copies(spacing=[10,-5],n=len(txt))
// text(txt[$idx], size=10, anchor=CENTER);
// Example: Using arc_copies() distributor
// txt = "This is the string";
// arc_copies(r=50, n=len(txt), sa=0, ea=180)
// text(select(txt,-1-$idx), size=10, anchor=str("baseline",CENTER), spin=-90);
module text(text, size=10, font, halign, valign, spacing=1.0, direction="ltr", language="en", script="latin", anchor="baseline", spin=0) {
no_children($children);
dummy1 =
assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Invalid anchor: ",anchor))
assert(is_finite(spin), str("Invalid spin: ",spin));
anchor = default(anchor, CENTER);
geom = attach_geom(size=[size,size],two_d=true);
anch = !any([for (c=anchor) c=="["])? anchor :
let(
parts = str_split(str_split(str_split(anchor,"]")[0],"[")[1],","),
vec = [for (p=parts) parse_float(str_strip(p," ",start=true))]
) vec;
ha = halign!=undef? halign :
anchor=="baseline"? "left" :
anchor==anch && is_string(anchor)? "center" :
anch.x<0? "left" :
anch.x>0? "right" :
"center";
va = valign != undef? valign :
starts_with(anchor,"baseline")? "baseline" :
anchor==anch && is_string(anchor)? "center" :
anch.y<0? "bottom" :
anch.y>0? "top" :
"center";
base = anchor=="baseline"? CENTER :
anchor==anch && is_string(anchor)? CENTER :
anch.z<0? BOTTOM :
anch.z>0? TOP :
CENTER;
m = _attach_transform(base,spin,undef,geom);
multmatrix(m) {
$parent_anchor = anchor;
$parent_spin = spin;
$parent_orient = undef;
$parent_geom = geom;
$parent_size = _attach_geom_size(geom);
$attach_to = undef;
if (_is_shown()){
_color($color) {
_text(
text=text, size=size, font=font,
halign=ha, valign=va, spacing=spacing,
direction=direction, language=language,
script=script
);
}
}
}
}
// Section: Rounding 2D shapes // Section: Rounding 2D shapes
// Module: round2d() // Module: round2d()

View file

@ -871,7 +871,7 @@ function octahedron(size=1, anchor=CENTER, spin=0, orient=UP) =
// labeling proceeds clockwise. The top and bottom edge anchors label edges directly above and below the face with the same label. // labeling proceeds clockwise. The top and bottom edge anchors label edges directly above and below the face with the same label.
// If you set `realign=true` then "face0" is oriented in the X+ direction. // If you set `realign=true` then "face0" is oriented in the X+ direction.
// . // .
// This module is very similar to {{cyl()}}. It differs in the following ways: you can specify side length or inner radius/diameter, you can apply roundings with // This module is similar to {{cyl()}}. It differs in the following ways: you can specify side length or inner radius/diameter, you can apply roundings with
// different `$fn` than the number of prism faces, you can apply texture to the flat faces without forcing a high facet count, // different `$fn` than the number of prism faces, you can apply texture to the flat faces without forcing a high facet count,
// anchors are located on the true object instead of the ideal cylinder and you can anchor to the edges and faces. // anchors are located on the true object instead of the ideal cylinder and you can anchor to the edges and faces.
// Named Anchors: // Named Anchors:
@ -2732,7 +2732,7 @@ function sphere(r, d, anchor=CENTER, spin=0, orient=UP) =
// spheroid(d=100, style="stagger", circum=true, $fn=10); // spheroid(d=100, style="stagger", circum=true, $fn=10);
// Example: style="octa", octahedral based tesselation. In this style, $fn is quantized to a multiple of 4. // Example: style="octa", octahedral based tesselation. In this style, $fn is quantized to a multiple of 4.
// spheroid(d=100, style="octa", $fn=10); // spheroid(d=100, style="octa", $fn=10);
// Example: style="octa", with circum=true, produces mostly very irregular hexagonal faces // Example: style="octa", with circum=true, produces mostly irregular hexagonal faces
// spheroid(d=100, style="octa", circum=true, $fn=16); // spheroid(d=100, style="octa", circum=true, $fn=16);
// Example: style="icosa", icosahedral based tesselation. In this style, $fn is quantized to a multiple of 5. // Example: style="icosa", icosahedral based tesselation. In this style, $fn is quantized to a multiple of 5.
// spheroid(d=100, style="icosa", $fn=10); // spheroid(d=100, style="icosa", $fn=10);
@ -2781,7 +2781,7 @@ module spheroid(r, style="aligned", d, circum=false, dual=false, anchor=CENTER,
scale(r) rotate(180) rotate_extrude(convexity=2,$fn=sides) polygon(path); scale(r) rotate(180) rotate_extrude(convexity=2,$fn=sides) polygon(path);
} }
// Don't now how to construct faces for these efficiently, so use hull_points, which // Don't now how to construct faces for these efficiently, so use hull_points, which
// is very much faster than using hull() as happens in the spheroid() function // is much faster than using hull() as happens in the spheroid() function
else if (circum && (style=="octa" || style=="icosa")) { else if (circum && (style=="octa" || style=="icosa")) {
orig_sphere = spheroid(r,style,circum=false); orig_sphere = spheroid(r,style,circum=false);
dualvert = _dual_vertices(orig_sphere); dualvert = _dual_vertices(orig_sphere);
@ -3375,356 +3375,6 @@ function onion(r, ang=45, cap_h, d, anchor=CENTER, spin=0, orient=UP) =
) reorient(anchor,spin,orient, r=r, anchors=anchors, p=vnf); ) reorient(anchor,spin,orient, r=r, anchors=anchors, p=vnf);
// Section: Text
// Module: text3d()
// Synopsis: Creates an attachable 3d text block.
// SynTags: Geom
// Topics: Attachments, Text
// See Also: path_text(), text()
// Usage:
// text3d(text, [h], [size], [font], [language=], [script=], [direction=], [atype=], [anchor=], [spin=], [orient=]);
// Description:
// Creates a 3D text block that supports anchoring and single-parameter attachment to attachable objects. You cannot attach children to text.
// .
// Historically fonts were specified by their "body size", the height of the metal body
// on which the glyphs were cast. This means the size was an upper bound on the size
// of the font glyphs, not a direct measurement of their size. In digital typesetting,
// the metal body is replaced by an invisible box, the em square, whose side length is
// defined to be the font's size. The glyphs can be contained in that square, or they
// can extend beyond it, depending on the choices made by the font designer. As a
// result, the meaning of font size varies between fonts: two fonts at the "same" size
// can differ significantly in the actual size of their characters. Typographers
// customarily specify the size in the units of "points". A point is 1/72 inch. In
// OpenSCAD, you specify the size in OpenSCAD units (often treated as millimeters for 3d
// printing), so if you want points you will need to perform a suitable unit conversion.
// In addition, the OpenSCAD font system has a bug: if you specify size=s you will
// instead get a font whose size is s/0.72. For many fonts this means the size of
// capital letters will be approximately equal to s, because it is common for fonts to
// use about 70% of their height for the ascenders in the font. To get the customary
// font size, you should multiply your desired size by 0.72.
// .
// To find the fonts that you have available in your OpenSCAD installation,
// go to the Help menu and select "Font List".
// Arguments:
// text = Text to create.
// h / height / thickness = Extrusion height for the text. Default: 1
// size = The font will be created at this size divided by 0.72. Default: 10
// font = Font to use. Default: "Liberation Sans" (standard OpenSCAD default)
// ---
// spacing = The relative spacing multiplier between characters. Default: `1.0`
// direction = The text direction. `"ltr"` for left to right. `"rtl"` for right to left. `"ttb"` for top to bottom. `"btt"` for bottom to top. Default: `"ltr"`
// language = The language the text is in. Default: `"en"`
// script = The script the text is in. Default: `"latin"`
// atype = Change vertical center between "baseline" and "ycenter". Default: "baseline"
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"baseline"`
// center = Center the text. Equivalent to `atype="center", anchor=CENTER`. Default: false
// 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`
// Anchor Types:
// baseline = Anchor center is relative to text baseline
// ycenter = Anchor center is relative to the actual y direction center of the text
// Examples:
// text3d("Fogmobar", h=3, size=10);
// text3d("Fogmobar", h=2, size=12, font=":style=bold");
// text3d("Fogmobar", h=2, anchor=CENTER);
// text3d("Fogmobar", h=2, anchor=CENTER, atype="ycenter");
// text3d("Fogmobar", h=2, anchor=RIGHT);
// text3d("Fogmobar", h=2, anchor=RIGHT+BOT, atype="ycenter");
module text3d(text, h, size=10, font, spacing=1.0, direction="ltr", language="en", script="latin",
height, thickness, atype, center=false,
anchor, spin=0, orient=UP) {
no_children($children);
h = one_defined([h,height,thickness],"h,height,thickness",dflt=1);
assert(is_undef(atype) || in_list(atype,["ycenter","baseline"]), "atype must be \"ycenter\" or \"baseline\"");
assert(is_bool(center));
assert(is_undef($attach_to),"text3d() does not support parent-child anchor attachment with two parameters");
atype = default(atype, center?"ycenter":"baseline");
anchor = default(anchor, center?CENTER:LEFT);
geom = attach_geom(size=[size,size,h]);
ha = anchor.x<0? "left"
: anchor.x>0? "right"
: "center";
va = anchor.y<0? "bottom"
: anchor.y>0? "top"
: atype=="baseline"? "baseline"
: "center";
m = _attach_transform([0,0,anchor.z],spin,orient,geom);
multmatrix(m) {
$parent_anchor = anchor;
$parent_spin = spin;
$parent_orient = orient;
$parent_geom = geom;
$parent_size = _attach_geom_size(geom);
$attach_to = undef;
if (_is_shown()) {
_color($color) {
linear_extrude(height=h, center=true)
_text(
text=text, size=size, font=font,
halign=ha, valign=va, spacing=spacing,
direction=direction, language=language,
script=script
);
}
}
}
}
// This could be replaced with _cut_to_seg_u_form
function _cut_interp(pathcut, path, data) =
[for(entry=pathcut)
let(
a = path[entry[1]-1],
b = path[entry[1]],
c = entry[0],
i = max_index(v_abs(b-a)),
factor = (c[i]-a[i])/(b[i]-a[i])
)
(1-factor)*data[entry[1]-1]+ factor * data[entry[1]]
];
// Module: path_text()
// Synopsis: Creates 2d or 3d text placed along a path.
// SynTags: Geom
// Topics: Text, Paths, Paths (2D), Paths (3D), Path Generators, Path Generators (2D)
// See Also, text(), text2d()
// Usage:
// path_text(path, text, [size], [thickness], [font], [lettersize=], [offset=], [reverse=], [normal=], [top=], [textmetrics=], [kern=])
// Description:
// Place the text letter by letter onto the specified path using textmetrics (if available and requested)
// or user specified letter spacing. The path can be 2D or 3D. In 2D the text appears along the path with letters upright
// as determined by the path direction. In 3D by default letters are positioned on the tangent line to the path with the path normal
// pointing toward the reader. The path normal points away from the center of curvature (the opposite of the normal produced
// by path_normals()). Note that this means that if the center of curvature switches sides the text will flip upside down.
// If you want text on such a path you must supply your own normal or top vector.
// .
// Text appears starting at the beginning of the path, so if the 3D path moves right to left
// then a left-to-right reading language will display in the wrong order. (For a 2D path text will appear upside down.)
// The text for a 3D path appears positioned to be read from "outside" of the curve (from a point on the other side of the
// curve from the center of curvature). If you need the text to read properly from the inside, you can set reverse to
// true to flip the text, or supply your own normal.
// .
// If you do not have the experimental textmetrics feature enabled then you must specify the space for the letters
// using lettersize, which can be a scalar or array. You will have the easiest time getting good results by using
// a monospace font such as "Liberation Mono". Note that even with text metrics, spacing may be different because path_text()
// doesn't do kerning to adjust positions of individual glyphs. Also if your font has ligatures they won't be used.
// .
// By default letters appear centered on the path. The offset can be specified to shift letters toward the reader (in
// the direction of the normal).
// .
// You can specify your own normal by setting `normal` to a direction or a list of directions. Your normal vector should
// point toward the reader. You can also specify
// top, which directs the top of the letters in a desired direction. If you specify your own directions and they
// are not perpendicular to the path then the direction you specify will take priority and the
// letters will not rest on the tangent line of the path. Note that the normal or top directions that you
// specify must not be parallel to the path.
// .
// Historically fonts were specified by their "body size", the height of the metal body
// on which the glyphs were cast. This means the size was an upper bound on the size
// of the font glyphs, not a direct measurement of their size. In digital typesetting,
// the metal body is replaced by an invisible box, the em square, whose side length is
// defined to be the font's size. The glyphs can be contained in that square, or they
// can extend beyond it, depending on the choices made by the font designer. As a
// result, the meaning of font size varies between fonts: two fonts at the "same" size
// can differ significantly in the actual size of their characters. Typographers
// customarily specify the size in the units of "points". A point is 1/72 inch. In
// OpenSCAD, you specify the size in OpenSCAD units (often treated as millimeters for 3d
// printing), so if you want points you will need to perform a suitable unit conversion.
// In addition, the OpenSCAD font system has a bug: if you specify size=s you will
// instead get a font whose size is s/0.72. For many fonts this means the size of
// capital letters will be approximately equal to s, because it is common for fonts to
// use about 70% of their height for the ascenders in the font. To get the customary
// font size, you should multiply your desired size by 0.72.
// .
// To find the fonts that you have available in your OpenSCAD installation,
// go to the Help menu and select "Font List".
// Arguments:
// path = path to place the text on
// text = text to create
// size = The font will be created at this size divided by 0.72.
// thickness / h / height = thickness of letters (not allowed for 2D path)
// font = Font to use. Default: "Liberation Sans" (standard OpenSCAD default)
// ---
// lettersize = scalar or array giving size of letters
// center = center text on the path instead of starting at the first point. Default: false
// offset = distance to shift letters "up" (towards the reader). Not allowed for 2D path. Default: 0
// normal = direction or list of directions pointing towards the reader of the text. Not allowed for 2D path.
// top = direction or list of directions pointing toward the top of the text
// reverse = reverse the letters if true. Not allowed for 2D path. Default: false
// textmetrics = if set to true and lettersize is not given then use the experimental textmetrics feature. You must be running a dev snapshot that includes this feature and have the feature turned on in your preferences. Default: false
// valign = align text to the path using "top", "bottom", "center" or "baseline". You can also adjust position with a numerical offset as in "top-5" or "bottom+2". This only works with textmetrics enabled. You can give a simple numerical offset, which will be relative to the baseline and works even without textmetrics. Default: "baseline"
// kern = scalar or array giving spacing adjusments between each letter. If it's an array it should have one less entry than the text string. Default: 0
// language = text language, passed to OpenSCAD `text()`. Default: "en"
// script = text script, passed to OpenSCAD `text()`. Default: "latin"
// Example(3D,NoScales): The examples use Liberation Mono, a monospaced font. The width is 1/1.2 times the specified size for this font. This text could wrap around a cylinder.
// path = path3d(arc(100, r=25, angle=[245, 370]));
// color("red")stroke(path, width=.3);
// path_text(path, "Example text", font="Liberation Mono", size=5, lettersize = 5/1.2);
// Example(3D,NoScales): By setting the normal to UP we can get text that lies flat, for writing around the edge of a disk:
// path = path3d(arc(100, r=25, angle=[245, 370]));
// color("red")stroke(path, width=.3);
// path_text(path, "Example text", font="Liberation Mono", size=5, lettersize = 5/1.2, normal=UP);
// Example(3D,NoScales): If we want text that reads from the other side we can use reverse. Note we have to reverse the direction of the path and also set the reverse option.
// path = reverse(path3d(arc(100, r=25, angle=[65, 190])));
// color("red")stroke(path, width=.3);
// path_text(path, "Example text", font="Liberation Mono", size=5, lettersize = 5/1.2, reverse=true);
// Example(3D,Med,NoScales): text debossed onto a cylinder in a spiral. The text is 1 unit deep because it is half in, half out.
// text = ("A long text example to wrap around a cylinder, possibly for a few times.");
// L = 5*len(text);
// maxang = 360*L/(PI*50);
// spiral = [for(a=[0:1:maxang]) [25*cos(a), 25*sin(a), 10-30/maxang*a]];
// difference(){
// cyl(d=50, l=50, $fn=120);
// path_text(spiral, text, size=5, lettersize=5/1.2, font="Liberation Mono", thickness=2);
// }
// Example(3D,Med,NoScales): Same example but text embossed. Make sure you have enough depth for the letters to fully overlap the object.
// text = ("A long text example to wrap around a cylinder, possibly for a few times.");
// L = 5*len(text);
// maxang = 360*L/(PI*50);
// spiral = [for(a=[0:1:maxang]) [25*cos(a), 25*sin(a), 10-30/maxang*a]];
// cyl(d=50, l=50, $fn=120);
// path_text(spiral, text, size=5, lettersize=5/1.2, font="Liberation Mono", thickness=2);
// Example(3D,NoScales): Here the text baseline sits on the path. (Note the default orientation makes text readable from below, so we specify the normal.)
// path = arc(100, points = [[-20, 0, 20], [0,0,5], [20,0,20]]);
// color("red")stroke(path,width=.2);
// path_text(path, "Example Text", size=5, lettersize=5/1.2, font="Liberation Mono", normal=FRONT);
// Example(3D,NoScales): If we use top to orient the text upward, the text baseline is no longer aligned with the path.
// path = arc(100, points = [[-20, 0, 20], [0,0,5], [20,0,20]]);
// color("red")stroke(path,width=.2);
// path_text(path, "Example Text", size=5, lettersize=5/1.2, font="Liberation Mono", top=UP);
// Example(3D,Med,NoScales): This sine wave wrapped around the cylinder has a twisting normal that produces wild letter layout. We fix it with a custom normal which is different at every path point.
// path = [for(theta = [0:360]) [25*cos(theta), 25*sin(theta), 4*cos(theta*4)]];
// normal = [for(theta = [0:360]) [cos(theta), sin(theta),0]];
// zrot(-120)
// difference(){
// cyl(r=25, h=20, $fn=120);
// path_text(path, "A sine wave wiggles", font="Liberation Mono", lettersize=5/1.2, size=5, normal=normal);
// }
// Example(3D,Med,NoScales): The path center of curvature changes, and the text flips.
// path = zrot(-120,p=path3d( concat(arc(100, r=25, angle=[0,90]), back(50,p=arc(100, r=25, angle=[268, 180])))));
// color("red")stroke(path,width=.2);
// path_text(path, "A shorter example", size=5, lettersize=5/1.2, font="Liberation Mono", thickness=2);
// Example(3D,Med,NoScales): We can fix it with top:
// path = zrot(-120,p=path3d( concat(arc(100, r=25, angle=[0,90]), back(50,p=arc(100, r=25, angle=[268, 180])))));
// color("red")stroke(path,width=.2);
// path_text(path, "A shorter example", size=5, lettersize=5/1.2, font="Liberation Mono", thickness=2, top=UP);
// Example(2D,NoScales): With a 2D path instead of 3D there's no ambiguity about direction and it works by default:
// path = zrot(-120,p=concat(arc(100, r=25, angle=[0,90]), back(50,p=arc(100, r=25, angle=[268, 180]))));
// color("red")stroke(path,width=.2);
// path_text(path, "A shorter example", size=5, lettersize=5/1.2, font="Liberation Mono");
// Example(3D,NoScales): The kern parameter lets you adjust the letter spacing either with a uniform value for each letter, or with an array to make adjustments throughout the text. Here we show a case where adding some extra space gives a better look in a tight circle. When textmetrics are off, `lettersize` can do this job, but with textmetrics, you'll need to use `kern` to make adjustments relative to the text metric sizes.
// path = path3d(arc(100, r=12, angle=[150, 450]));
// color("red")stroke(path, width=.3);
// kern = [1,1.2,1,1,.3,-.2,1,0,.8,1,1.1];
// path_text(path, "Example text", font="Liberation Mono", size=5, lettersize = 5/1.2, kern=kern, normal=UP);
module path_text(path, text, font, size, thickness, lettersize, offset=0, reverse=false, normal, top, center=false,
textmetrics=false, kern=0, height,h, valign="baseline", language, script)
{
no_children($children);
dummy2=assert(is_path(path,[2,3]),"Must supply a 2d or 3d path")
assert(num_defined([normal,top])<=1, "Cannot define both \"normal\" and \"top\"")
assert(all_positive([size]), "Must give positive text size");
dim = len(path[0]);
normalok = is_undef(normal) || is_vector(normal,3) || (is_path(normal,3) && len(normal)==len(path));
topok = is_undef(top) || is_vector(top,dim) || (dim==2 && is_vector(top,3) && top[2]==0)
|| (is_path(top,dim) && len(top)==len(path));
dummy4 = assert(dim==3 || !any_defined([thickness,h,height]), "Cannot give a thickness or height with 2d path")
assert(dim==3 || !reverse, "Reverse not allowed with 2d path")
assert(dim==3 || offset==0, "Cannot give offset with 2d path")
assert(dim==3 || is_undef(normal), "Cannot define \"normal\" for a 2d path, only \"top\"")
assert(normalok,"\"normal\" must be a vector or path compatible with the given path")
assert(topok,"\"top\" must be a vector or path compatible with the given path");
thickness = one_defined([thickness,h,height],"thickness,h,height",dflt=1);
normal = is_vector(normal) ? repeat(normal, len(path))
: is_def(normal) ? normal
: undef;
top = is_vector(top) ? repeat(dim==2?point2d(top):top, len(path))
: is_def(top) ? top
: undef;
kern = force_list(kern, len(text)-1);
dummy3 = assert(is_list(kern) && len(kern)==len(text)-1, "kern must be a scalar or list whose length is len(text)-1");
lsize = is_def(lettersize) ? force_list(lettersize, len(text))
: textmetrics ? [for(letter=text) let(t=textmetrics(letter, font=font, size=size)) t.advance[0]]
: assert(false, "textmetrics disabled: Must specify letter size");
lcenter = convolve(lsize,[1,1]/2)+[0,each kern,0] ;
textlength = sum(lsize)+sum(kern);
ascent = !textmetrics ? undef
: textmetrics(text, font=font, size=size).ascent;
descent = !textmetrics ? undef
: textmetrics(text, font=font, size=size).descent;
vadjustment = is_num(valign) ? -valign
: !textmetrics ? assert(valign=="baseline","valign requires textmetrics support") 0
: let(
table = [
["baseline", 0],
["top", -ascent],
["bottom", descent],
["center", (descent-ascent)/2]
],
match = [for(i=idx(table)) if (starts_with(valign,table[i][0])) i]
)
assert(len(match)==1, "Invalid valign value")
table[match[0]][1] - parse_num(substr(valign,len(table[match[0]][0])));
dummy1 = assert(textlength<=path_length(path),"Path is too short for the text");
start = center ? (path_length(path) - textlength)/2 : 0;
pts = path_cut_points(path, add_scalar(cumsum(lcenter),start), direction=true);
usernorm = is_def(normal);
usetop = is_def(top);
normpts = is_undef(normal) ? (reverse?1:-1)*column(pts,3) : _cut_interp(pts,path, normal);
toppts = is_undef(top) ? undef : _cut_interp(pts,path,top);
attachable(){
for (i = idx(text)) {
tangent = pts[i][2];
checks =
assert(!usetop || !approx(tangent*toppts[i],norm(top[i])*norm(tangent)),
str("Specified top direction parallel to path at character ",i))
assert(usetop || !approx(tangent*normpts[i],norm(normpts[i])*norm(tangent)),
str("Specified normal direction parallel to path at character ",i));
adjustment = usetop ? (tangent*toppts[i])*toppts[i]/(toppts[i]*toppts[i])
: usernorm ? (tangent*normpts[i])*normpts[i]/(normpts[i]*normpts[i])
: [0,0,0];
move(pts[i][0]) {
if (dim==3) {
frame_map(
x=tangent-adjustment,
z=usetop ? undef : normpts[i],
y=usetop ? toppts[i] : undef
) up(offset-thickness/2) {
linear_extrude(height=thickness)
back(vadjustment)
{
left(lsize[i]/2)
text(text[i], font=font, size=size, language=language, script=script);
}
}
} else {
frame_map(
x=point3d(tangent-adjustment),
y=point3d(usetop ? toppts[i] : -normpts[i])
) left(lsize[0]/2) {
text(text[i], font=font, size=size, language=language, script=script);
}
}
}
}
union();
}
}
// Section: Miscellaneous // Section: Miscellaneous
@ -3846,7 +3496,7 @@ module fillet(l, r, ang, r1, r2, excess=0.01, d1, d2,d,length, h, height, anchor
// Arguments: // Arguments:
// data = This is either the 2D rectangular array of heights, or a function literal that takes X and Y 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]` // 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 // bottom = The Z coordinate for the bottom of the heightfield object to create. Any heights lower than this will be truncated to 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 // 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] // 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] // yrange = A range of values to iterate Y over when calculating a surface from a function literal. Default: [-1 : 0.01 : 1]
@ -3985,7 +3635,7 @@ function heightfield(data, size=[100,100], bottom=-20, maxz=100, xrange=[-1:0.04
// d = The diameter 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. // d1 = The diameter of the bottom of the cylinder to wrap around.
// d2 = The diameter of the top 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 // base = The radius for the bottom of the heightfield object to create. Any heights smaller than this will be truncated to slightly above this height. Default: -20
// transpose = If true, swaps the radial and length axes of the data. Default: false // 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 // 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] // xrange = A range of values to iterate X over when calculating a surface from a function literal. Default: [-1 : 0.01 : 1]
@ -4208,7 +3858,4 @@ module ruler(length=100, width, thickness=1, depth=3, labels=false, pipscale=1/3
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap

View file

@ -39,6 +39,8 @@ include <vnf.scad>
include <utility.scad> include <utility.scad>
include <partitions.scad> include <partitions.scad>
include <structs.scad> include <structs.scad>
include <text.scad>
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap

868
text.scad Normal file
View file

@ -0,0 +1,868 @@
//////////////////////////////////////////////////////////////////////
// LibFile: text.scad
// This file contains useful utilities for managing text objects.
// * A replacement module for OpenSCAD's `text()` is povided to make the block of text attachable to children.
// * Text wrapping between specified margins can be done with proportionally spaced fonts
// (requires an OpenSCAD build more recent than 2021-08-16) or fixed-width fonts (all versions),
// and an array of wrapped text can be rendered as an attachable object as with `text()`.
// * Create attachable 3D text blocks, and arrange 2D or 3D text along an arbitrary path.
// Includes:
// include <BOSL2/std.scad>
// FileGroup: Basic Modeling
// FileSummary: Attachable text objects in 2D and 3D.
// FileFootnotes: STD=Included in std.scad
//////////////////////////////////////////////////////////////////////
use <builtins.scad>
// Section: 2D Text
// The 2D text operations include a replacement for OpenSCAD's `text()` that creates an attachable block of text,
// another replacement called `write()` that can display multi-line text with optional margin justification,
// and a `textwrap()` function to word-wrap text within a specified width
// .
// **Text wrapping with proportional fonts requires an OpenSCAD snapshot or stable build released after 2021-08-16.**
// While you *can* pass a proportional font to `wraptext()` in older versions of OpenSCAD, and display the wrapped
// text using that proportional font, the wrapping algorithm still treats it as a monospace font. This can give reasonable
// results much of the time but the line lengths would not be the same as from text wrapped using proportional character spacing.
// .
// If you are using a version of OpenSCAD dated earlier than 2021-08-16 when `textmetrics()` appeared the OpenSCAD snapshots,
// then only monospace fonts are supported, and unless specified otherwise in `textwrap()`,
// the width of all characters is assumed to be 83.35% of the font size, corresponding to the font "Liberation Mono:style=Bold".
// Results may be different with other monospace fonts.
// Module: text()
// Synopsis: Creates an attachable block of text.
// SynTags: Geom
// Topics: Attachments, Text
// See Also: text3d(), attachable()
// Usage:
// text(text, [size], [font], ...);
// Description:
// Creates a 3D text block that can be attached to other attachable objects.
// You cannot attach children to text.
// .
// Historically fonts were specified by their "body size", the height of the metal body
// on which the glyphs were cast. This means the size was an upper bound on the size
// of the font glyphs, not a direct measurement of their size. In digital typesetting,
// the metal body is replaced by an invisible box, the em square, whose side length is
// defined to be the font's size. The glyphs can be contained in that square, or they
// can extend beyond it, depending on the choices made by the font designer. As a
// result, the meaning of font size varies between fonts: two fonts at the "same" size
// can differ significantly in the actual size of their characters. Typographers
// customarily specify the size in the units of "points". A point is 1/72 inch. In
// OpenSCAD, you specify the size in OpenSCAD units (often treated as millimeters for 3d
// printing), so if you want points you will need to perform a suitable unit conversion.
// In addition, the OpenSCAD font system has a bug: if you specify size=s you will
// instead get a font whose size is s/0.72. For many fonts this means the size of
// capital letters will be approximately equal to s, because it is common for fonts to
// use about 70% of their height for the ascenders in the font. To get the customary
// font size, you should multiply your desired size by 0.72.
// .
// To find the fonts that you have available in your OpenSCAD installation,
// go to the Help menu and select "Font List".
// Arguments:
// text = Text to create.
// size = The font will be created at this size divided by 0.72. Default: 10
// font = Font to use. Default: "Liberation Sans" (standard OpenSCAD default)
// ---
// halign = If given, specifies the horizontal alignment of the text. `"left"`, `"center"`, or `"right"`. Overrides `anchor=`.
// valign = If given, specifies the vertical alignment of the text. `"top"`, `"center"`, `"baseline"` or `"bottom"`. Overrides `anchor=`.
// spacing = The relative spacing multiplier between characters. Default: `1.0`
// direction = The text direction. `"ltr"` for left to right. `"rtl"` for right to left. `"ttb"` for top to bottom. `"btt"` for bottom to top. Default: `"ltr"`
// language = The language the text is in. Default: `"en"`
// script = The script the text is in. Default: `"latin"`
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"baseline"`
// spin = Rotate this many degrees around the Z axis. See [spin](attachments.scad#subsection-spin). Default: `0`
// Named Anchors:
// "baseline" = Anchors at the baseline of the text, at the start of the string.
// str("baseline",VECTOR) = Anchors at the baseline of the text, modified by the X and Z components of the appended vector.
// Examples(2D):
// text("Foobar", size=10);
// text("Foobar", size=12, font="Liberation Mono");
// text("Foobar", anchor=CENTER);
// text("Foobar", anchor=str("baseline",CENTER));
// Example: Using line_copies() distributor
// txt = "This is the string.";
// line_copies(spacing=[10,-5],n=len(txt))
// text(txt[$idx], size=10, anchor=CENTER);
// Example: Using arc_copies() distributor
// txt = "This is the string";
// arc_copies(r=50, n=len(txt), sa=0, ea=180)
// text(select(txt,-1-$idx), size=10, anchor=str("baseline",CENTER), spin=-90);
module text(text, size=10, font, halign, valign, spacing=1.0, direction="ltr", language="en", script="latin", anchor="baseline", spin=0) {
no_children($children);
dummy1 =
assert(is_undef(anchor) || is_vector(anchor) || is_string(anchor), str("Invalid anchor: ",anchor))
assert(is_finite(spin), str("Invalid spin: ",spin));
anchor = default(anchor, CENTER);
geom = attach_geom(size=[size,size],two_d=true);
anch = !any([for (c=anchor) c=="["])? anchor :
let(
parts = str_split(str_split(str_split(anchor,"]")[0],"[")[1],","),
vec = [for (p=parts) parse_float(str_strip(p," ",start=true))]
) vec;
ha = halign!=undef? halign :
anchor=="baseline"? "left" :
anchor==anch && is_string(anchor)? "center" :
anch.x<0? "left" :
anch.x>0? "right" :
"center";
va = valign != undef? valign :
starts_with(anchor,"baseline")? "baseline" :
anchor==anch && is_string(anchor)? "center" :
anch.y<0? "bottom" :
anch.y>0? "top" :
"center";
base = anchor=="baseline"? CENTER :
anchor==anch && is_string(anchor)? CENTER :
anch.z<0? BOTTOM :
anch.z>0? TOP :
CENTER;
m = _attach_transform(base,spin,undef,geom);
multmatrix(m) {
$parent_anchor = anchor;
$parent_spin = spin;
$parent_orient = undef;
$parent_geom = geom;
$parent_size = _attach_geom_size(geom);
$attach_to = undef;
if (_is_shown()){
_color($color) {
_text(
text=text, size=size, font=font,
halign=ha, valign=va, spacing=spacing,
direction=direction, language=language,
script=script
);
}
}
}
}
// Function: textwrap()
// Synopsis: Wraps a text string with a specified font to fit within a specified width, returning an array of strings.
// Topics: Text
// See Also: text(), str_split(), text_array_size()
// Usage:
// text_array = textwrap(string, width, [optimize=], [line_spacing=], ...);
// Description:
// Returns an array of paragraphs, where each paragraph is an array of substrings of the original text,
// such that each substring fits within a specified width when displayed with the specified font.
// By default, the text wrapping is optimized so that each line of text is roughly the same
// length to minimize the occurrence of an unusually short final line.
// The actual overall width of the final text is less than or equal to the requested width.
// You can use `{{text_array_size()}}` to get the actual bounding box of the wrapped text.
// .
// Multple paragraphs are returned if the `string` argument contains newline (`\n`) characters that split the string.
// To insert a blank line, use two newlines with a space in between (`\n \n`).
// Referring to Example 3 below, if the return value is in `text_array`, then the lines of text are in the paragraph `text_array[paragraph_number]`.
// If the string contains no newlines, then a single paragraph is returned in `text_arra`,
// and `text_array[0]` contains the substrings lines from the wrapped text, as shown in Examples 1 and 2.
// .
// Several parameters are the same as for OpenSCAD's builtin `text()`. Some don't seem to have any effect, but are present here for compatibility: `direction`, `language`, and `script`.
// Arguments:
// string = The text to generate. Any leading whitespace, trailing whitespace, and consecutive spaces are stripped before word-wrapping.
// width = the maximum width of a line of text in display units.
// ---
// optimize = When false, tries to fit as many words as possible on each successive line, which may result in a widow (a word all by itself) on the last line. When true, attempts to make the wrapped lines more equal in length. Default: true
// size = font size (decimal number). See the [OpenSCAD documentation](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Text) for this the following parameters.
// font = The name of the font that should be used, including an optional style parameter. Default: `"Liberation Sans:style=Bold"` (for OpenSCAD builds before 2021-08-16, default is `"Liberation Mono:style=Bold"`)
// spacing = Factor to increase/decrease the character spacing. Default: 1.0
// direction = Direction of the text flow, "ltr" (left-to-right), "rtl" (right-to-left). This function does not support "ttb" (top-to-bottom), or "btt" (bottom-to-top). Default: "ltr"
// language = Two-letter language code for the text. Default: "en"
// script = The script of the text. Default: "latin"
// charwidth = monospace character width to use for OpenSCAD builds prior to 2021-08-16. If not set, the value is calculated as 83.35% of the font size using font "Liberation Mono:style=Bold". Setting this in later builds results in an error. Default: undef
// Example: Basic textwrap of a single long string to fit within 240 units. Because line length optimization is enabled by default, the resulting lines have roughly equal length, although significantly narrower than 240 units.
// sample = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
// sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
// textwidth = 240;
// text_array = textwrap(sample, textwidth, font="Liberation Serif");
// /*
// text_array contains one paragraph of four lines roughly equal in size:
// [
// [
// "Lorem ipsum dolor sit amet,",
// "consectetur adipiscing elit, sed",
// "do eiusmod tempor incididunt ut",
// "labore et dolore magna aliqua."
// ]
// ]
// */
// Example: Here is the same thing but with optimization disabled. The lines are as wide as can fit within 240 units for the specified font (and default `size=10`), with a lone word by itself on the last line.
// sample = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
// sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
// textwidth = 240;
// text_array = textwrap(sample, textwidth, font="Liberation Serif", optimize=false);
// /*
// text_array is one paragraph of four lines consuming the maximum width:
// [
// [
// "Lorem ipsum dolor sit amet, consectetur",
// "adipiscing elit, sed do eiusmod tempor",
// "incididunt ut labore et dolore magna",
// "aliqua."
// ]
// ]
// */
// Example: Here is a longer string with a space between two newlines (`\n \n`) inserted to create paragraphs separated by a blank line. In this case, the optimization tries to maintain equal margins for both paragraphs, and sacrifices the length of the last line of the first paragraph to avoid the second paragraph wrapping to another line.
// sample = "Lorem ipsum dolor sit amet, consectetur adipiscing \
// elit, sed do eiusmod tempor incididunt ut labore et dolore magna \
// aliqua.\n \nUt enim ad minim veniam, quis nostrud exercitation \
// ullamco laboris nisi ut aliquip ex ea commodo consequat.";
// textwidth = 240;
// text_array = textwrap(sample, textwidth, font="Liberation Serif");
// /*
// text_array contains one paragraph with four lines of roughly equal length:
// [
// // first paragraph
// [
// "Lorem ipsum dolor sit amet,",
// "consectetur adipiscing elit, sed do",
// "eiusmod tempor incididunt ut labore et",
// "dolore magna aliqua."
// ],
// // second paragraph is a blank line
// [ "" ],
// // third paragraph
// [
// "Ut enim ad minim veniam, quis nostrud",
// "exercitation ullamco laboris nisi ut",
// "aliquip ex ea commodo consequat."
// ]
// ]
// */
function textwrap(string, width, optimize=true, size=10, font, spacing=1, direction="ltr", language="en", script="latin", charwidth, _fdata) =
assert(version_num()<20210816 || is_undef(charwidth), "Parameter charwidth cannot be specified for OpenSCAD builds dated after 2021-08-16.")
assert(is_def(width), "Width units must be specified.")
let(
fontname = is_def(font) ? font
: version_num()>=20210816 ? "Liberation Sans:style=Bold"
: "Liberation Mono:style=Bold",
gd = is_undef(_fdata) ? _glyphdata(fontname, size) : _fdata,
charwid = version_num()>=20210816 ? undef : is_def(charwidth) ? charwidth : gd[1],
spc = is_def(charwid) ? charwid
: textmetrics(text=" ", size=size, font=fontname, direction=direction, language=language, script=script, spacing=spacing).advance[0],
words = [
for(p=str_split(string, "\n", false)) let(
line = list_remove_values(str_split(str_strip(p," \t\r\n"), " \t"), "", all=true)
) len(line)==0 ? [""] : line
],
wlens = [
for(p=words) [
for(w=p) is_def(charwid)
? len(w)*charwid + spc // only for old builds of OpenSCAD
: textmetrics(text=w, size=size, font=fontname, direction=direction, language=language, script=script, spacing=spacing).advance[0]
+ spc // all words must have a trailing space; EOL is accounted for later
]
],
maxwordwid = max(flatten(wlens)) - spc // length of longest word in text
) assert(maxwordwid <= width, "A word width exceeds the specified width.")
// Create the array line_indexes with same paragraph structure as words.
// Each line of wrapped text is represented by an array of [start,end] pairs,
// with start and end pointing to indexes in cumlen, which is a cumulative list
// of line lengths at the end of each word, and also has the same paragraph structure.
let(
cumlen = [ for(wl=wlens) cumsum(wl) ],
npara = len(words),
line_indexes = [ for(cl=cumlen) _getlines(width, spc, cl, len(cl)) ],
maxlinewid=_get_lines_maxwid(line_indexes, spc, cumlen),
final_line_indexes = optimize
? _wrap_optimize(len(flatten(line_indexes)), maxwordwid, line_indexes, maxlinewid-0.01, spc, cumlen)
: line_indexes,
nlines = len(line_indexes)
) [ // output an array of paragraphs containing lists of wordwrapped strings
for(i=[0:nlines-1]) let(li=final_line_indexes[i]) [
for(j=[0:len(li)-1]) str_join(slice(words[i], li[j][0], li[j][1]), " ")
]
];
/// Private function: _wrap_optimize(), called by textwrap()
/// Recursively find minimum wrap width in all paragraphs represented by line_indexes
/// such that the total number of lines of wrapped text does not increase.
/// Arguments:
/// maxlines = total number of lines not to exceed
/// minwid = minimum wrap width allowable (length of longest word)
/// line_indexes = array of [start,end] pairs, with start and end pointing to indexes in cumlen parameter
/// reqwid = requested wrap width
/// spc = length of a space
/// cumlen = cumulative list of line lengths with same paragraph structure as line_indexes
/// iter = maximum number of recursions allowed
function _wrap_optimize(maxlines, minwid, line_indexes, reqwid, spc, cumlen, iter=50) =
let(
new_li = [ for(cl=cumlen) _getlines(reqwid, spc, cl, len(cl)) ],
nlines = len(flatten(new_li)),
newreqwid = _get_lines_maxwid(new_li, spc, cumlen) - 0.01
)
iter<=0 || nlines>maxlines || newreqwid<=minwid
? line_indexes
: _wrap_optimize(maxlines, minwid, new_li, newreqwid, spc, cumlen, iter-1);
/// Private function: _getlines(), called by wraptext() and _wrap_optimize()
/// "Greedy" word-wrap, returns a list of text strings that fit within the specified width.
/// Arguments:
/// width = width in which to fit text
/// spc = width of a space character
/// cl = array of cumulative line lengths in a single paragraph
/// nwords = number of words total
/// The parameters istart and res are maintained internally.
function _getlines(width, spc, cl, nwords, istart=0, res=[]) =
istart >= nwords ? res
: let(
prevlinelen = istart>0 ? cl[istart-1] : 0,
lastindx = _bsearch_lowindex(width+prevlinelen+spc, cl, nwords, istart, nwords-1)
) _getlines(width, spc, cl, nwords, lastindx+1, concat(res, [[istart,lastindx]]));
/// Private function: _bsearch_lowindex(), binary search called by _getlines()
/// Return the index of the element in ordered_list of length n that is less than or equal to value.
/// The parameters low and high should initially be set to 0 and n-1, respectively.
function _bsearch_lowindex(value, ordered_list, n, low, high) =
high < low ? -1
: let(mid = low + floor(0.5*(high-low)))
mid >= n-1
? n-1
: (ordered_list[mid] <= value && value < ordered_list[min(mid+1,n-1)]
? mid
: (ordered_list[mid] > value
? _bsearch_lowindex(value, ordered_list, n, low, mid-1)
: _bsearch_lowindex(value, ordered_list, n, mid+1, high))
);
/// Private function: _get_lines_maxwid(), called by textwrap() and _wrap_optimize()
/// Return the maximum width of the lines represented by line_indexes, which contains array indexes pointing into cumlen, a list of cumulative lengths at end of each word.
function _get_lines_maxwid(line_indexes, spc, cumlen) =
let(widths = [
for(i=[0:len(line_indexes)-1])
for(j = [0:len(line_indexes[i])-1])
cumlen[i][line_indexes[i][j][1]] - (j>0 ? cumlen[i][line_indexes[i][j-1][1]] : 0)
]) max(widths) - spc; // all words have a trailing space, so subtract EOL trailing space
// Module: write()
// Synopsis: Render multi-line text as an attachable 2D block using the specified font characteristics.
// SynTags: Geom
// Topics: Attachments, Text
// See Also: textwrap()
// Usage:
// write(text_array, width, [line_spacing=], ...);
// Usage: With attachments
// write(text_array, width, [line_spacing=], ...) [ATTACHMENTS];
// Description:
// Creates a 2D geometry of the array of text strings, using the given line spacing and font specifications.
// The font parameters work the same as with OpenSCAD's builtin text() command.
// The 'halign' and `valign` parameters are not used because they is not relevant for fitting multi-line text inside a bounding box.
// The `justify` parameter can be used to justify the text left, right, center, or full within the bounding box.
// You would use the `anchor` parameter to set the origin of the block to position it.
// Arguments:
// text_array = An array of text strings to display. The array may be a simple list of strings, or a list of paragraphs as returned from `textwrap()`, with each paragraph being a list of strings.
// ---
// line_spacing = the proportion of font interline height to use for line spacing. The interline height accounts for the nominal extents of ascenders and descenders in the font glyphs. The actual character size is typically about 72% of the interline height. Default: 1.0
// justify = The horizontal alignment for the text within its bounding box. Possible values are "left", "center", "right", and "full". When `justify="full"` the spaces between words are stretched so that the left and right ends of each line align with the sides of the bounding box. Default: "left"
// justify_last = How to justify the last line of multi-line text in a paragraph. Possible values are the same as for `justify`. This overrides the `justify` parameter if the paragraph consists of a single line. Default: "left"
// size = font size (decimal number). See the [OpenSCAD documentation](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Text) for this and following parameters. Default: 10
// font = The name of the font that should be used, including an optional style parameter. Default: `"Liberation Sans:style=Bold"` (for OpenSCAD builds before 2021-08-16, the default is `"Liberation Mono:style=Bold"`)
// spacing = Factor to change the character spacing. Default: 1.0
// direction = Direction of the text flow, "ltr" (left-to-right), "rtl" (right-to-left). This function does not support "ttb" (top-to-bottom), or "btt" (bottom-to-top). Default: "ltr"
// language = Two-letter language code for the text. Default: "en"
// script = The script of the text. Default: "latin"
// $fn = used for subdividing the curved path segments.
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `BOTTOM+LEFT`
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// Example(3D,Med): Wrap some text and extrude the resulting array in 3D.
// string = "Go placidly amid the noise and haste, \
// and remember what peace there may be in silence.";
// fontname = "Lucida Serif:style=Bold Italic";
// text_array = textwrap(string, width=130, font=fontname);
// color("lightblue") linear_extrude(4) write(text_array, font=fontname);
module write(text_array, line_spacing=1, justify="left", justify_last="left", size=10, font, spacing=1, direction="ltr", language="en", script="latin", anchor=BOTTOM+LEFT, spin=0) {
assert(direction=="ltr" || direction=="rtl", "Only directions 'ltr' and 'rtl' are supported.");
lines = flatten(text_array);
nlines = len(lines);
fontname = is_def(font) ? font
: version_num()>=20210816 ? "Liberation Sans:style=Bold"
: "Liberation Mono:style=Bold";
gd = _glyphdata(fontname, size);
lineht = gd[0] * line_spacing;
bboxsize = text_array_size(lines, line_spacing, size, fontname, spacing, gd);
height = bboxsize[1];
width = bboxsize[0];
attachable(anchor, spin, two_d=true, size=bboxsize) {
translate([0,height/2 + gd[3]]) {
for(i=[0:nlines-1]) translate([0,-i*lineht]) {
dum=echo("width in array_text:", width);
_justify_text(lines[i], width=width,
justify=(i==nlines-1 ? justify_last : justify),
size=size, font=fontname, valign="baseline", spacing=spacing, direction=direction, language=language, script=script, gd);
}
}
children();
}
}
// was going to make this public, but might make it private
module _justify_text(string, width, justify="left", size=10, font, valign="baseline", spacing=1, direction="ltr", language="en", script="latin", charwidth, _fdata) {
dum=echo("width in _justify_text:", width);
dummy = assert(is_num(width), "The width parameter must be specified as a number.");
fontname = is_def(font) ? font
: version_num()>=20210816 ? "Liberation Sans:style=Bold"
: "Liberation Mono:style=Bold";
gd = is_undef(_fdata) ? _glyphdata(fontname, size) : _fdata;
charwid = version_num()>=20210816 ? undef : is_def(charwidth) ? charwidth : gd[1];
txheight = gd[0];
union() {
translate([-width/2,-txheight/2]) {
if (justify=="full") {
words = list_remove_values(str_split(str_strip(string," \t\r\n"), " \t"), "", all=true);
wlens = [
for(w=words) is_def(charwid)
? len(w)*charwid // only for old builds of OpenSCAD
: textmetrics(text=w, size=size, font=fontname, direction=direction, language=language, script=script, spacing=spacing).advance[0]
];
cumwid = cumsum(wlens);
nwords = len(words);
totalwordwidth = cumwid[nwords-1];
spc = nwords<=1 ? 0 : (width - totalwordwidth) / (len(words)-1);
for(i=[0:nwords-1])
translate([i*spc + (i>0 ? cumwid[i-1] : 0), 0])
_text(words[i], size=size, font=fontname, halign="left", valign=valign, spacing=spacing, direction=direction, language=language, script=script);
} else {
translate([justify=="right"?width:justify=="center"?width/2:0, 0])
_text(string, size=size, font=fontname, halign=justify, valign=valign, spacing=spacing, direction=direction, language=language, script=script);
}
}
}
}
/// Private function: _glyphdata(), called by textwrap(), write(), and text_array_size()
/// Get font data, used for positioning lines of text in bounding box.
function _glyphdata(fontname, size=10) =
// textmetrics() added to OpenSCAD on 2021-08-16 in https://github.com/openscad/openscad/pull/3684
version_num() >= 20210816
? let(
fm = fontmetrics(size, fontname),
tmN = textmetrics("N", size, fontname)
) [
fm.interline, // line height
tmN.advance[0], // monospace N width (here only for index compatibility)
fm.nominal.ascent, // nominal char height from baseline
fm.nominal.descent // nominal char descent from baseline
]
: let(scl=size/10) [ // For OpenSCAD older than 2021-08-16
scl*15.7335, // line height from fontmetrics 'interline'
scl*8.3347, // glyph width of "Liberation Mono:style=Bold" N from textmetrics 'advance'
scl*11.5628, // glyph height from baseline
-scl*4.1707 // descent from baseline
];
// Function: text_array_size()
// Synopsis: Returns the bounding box dimensions of an array of text given the line spacing and font specifications.
// Topics: Text
// See Also: textwrap(), write()
// Usage:
// bbox = text_array_size((text_array, line_spacing, size, font, spacing);
// Description:
// Returns the bounding box dimensions `[width, height]` of an array of text strings (paragraphs or simple list)
// given the line spacing and font specifications. While only `text_array` is required as an argument,
// you must pass the font specifications if you use other than the default values.
// Arguments:
// text_array = An array of text strings. The array may be a simple list of strings, or a list of paragraphs as returned from `textwrap()`, with each paragraph being a list of strings. If you have a single string of text, then convert it to a list by surrounding it with square brackets; that is, `string` would be passed as `[string]`.
// ---
// line_spacing = the proportion of font interline height to use for line spacing. Default: 1.0
// size = font size (decimal number). See the [OpenSCAD documentation](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Text) for this and following parameters. Default: 10
// font = The name of the font that should be used, including an optional style parameter. Default: "Liberation Sans:style=Bold" (for OpenSCAD builds before 2021-08-16, default is "Liberation Mono:style=Bold")
// spacing = Factor to increase/decrease the character spacing. Default: 1.0
// Example(2D,Med): This example demonstrates several things at once. The text is wrapped using the default font and size. The original requested width of 280 units is the yellow rectangle, and anchored to it are the wrapped text displayed by `write()` in black, and the bounding box of the wrapped text in green.
// sample = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
// sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n \n\
// Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris \
// nisi ut aliquip ex ea commodo consequat.";
// textwidth = 280;
// text_array = textwrap(sample, textwidth);
// bbox = text_array_size(text_array);
// cube([textwidth, bbox[1], 1]) attach(TOP,BOT) {
// color("palegreen") cube(point3d(bbox,1));
// color("black") linear_extrude(2) write(text_array, anchor=CENTER);
// }
function text_array_size(text_array, line_spacing=1, size=10, font, spacing=1, _fdata) =
assert(is_type(text_array, "list") && is_type(text_array[0], ["string","list"]), "text_array must be an array of paragraphs or strings.")
let(
fontname = is_def(font) ? font
: version_num()>=20210816 ? "Liberation Sans:style=Bold"
: "Liberation Mono:style=Bold",
gd = is_undef(_fdata) ? _glyphdata(fontname, size) : _fdata,
sl = flatten(text_array),
width = _stringlist_width(sl, gd, size, fontname, spacing),
height = len(sl)*line_spacing*gd[0]
) [width, height];
/// Private function: _stringlist_width(), called by text_array_size()
/// Recursively find maximum width in CAD units (not characters) of array of text strings
/// sl = string list
/// gd = glyph data
/// size, font, spacing = font specs
function _stringlist_width(sl, gd, size, font, spacing, width=0, i=0) =
i>=len(sl)
? width
: let(
strwid = version_num() < 20210816 ? len(sl[i])*gd[1] : textmetrics(sl[i], size, font, spacing=spacing).advance[0]
)
_stringlist_width(sl, gd, size, font, spacing, max(width, strwid), i+1);
// Section: 3D Text
// Historically fonts were specified by their "body size", the height of the metal body
// on which the glyphs were cast. This means the size was an upper bound on the size
// of the font glyphs, not a direct measurement of their size. In digital typesetting,
// the metal body is replaced by an invisible box, the em square, whose side length is
// defined to be the font's size. The glyphs can be contained in that square, or they
// can extend beyond it, depending on the choices made by the font designer. As a
// result, the meaning of font size varies between fonts: two fonts at the "same" size
// can differ significantly in the actual size of their characters. Typographers
// customarily specify the size in the units of "points". A point is 1/72 inch. In
// OpenSCAD, you specify the size in OpenSCAD units (often treated as millimeters for 3d
// printing), so if you want points you will need to perform a suitable unit conversion.
// In addition, the OpenSCAD font system has a bug: if you specify size=s you will
// instead get a font whose size is s/0.72. For many fonts this means the size of
// capital letters will be approximately equal to s, because it is common for fonts to
// use about 70% of their height for the ascenders in the font. To get the customary
// font size, you should multiply your desired size by 0.72.
// .
// To find the fonts that you have available in your OpenSCAD installation,
// go to the Help menu and select "Font List".
// Module: text3d()
// Synopsis: Creates an attachable 3d text block.
// SynTags: Geom
// Topics: Attachments, Text
// See Also: path_text(), text()
// Usage:
// text3d(text, [h], [size], [font], [language=], [script=], [direction=], [atype=], [anchor=], [spin=], [orient=]);
// Description:
// Creates a 3D text block that supports anchoring and single-parameter attachment to attachable objects. You cannot attach children to text.
// Arguments:
// text = Text to create.
// h / height / thickness = Extrusion height for the text. Default: 1
// size = The font will be created at this size divided by 0.72. Default: 10
// font = Font to use. Default: "Liberation Sans" (standard OpenSCAD default)
// ---
// spacing = The relative spacing multiplier between characters. Default: `1.0`
// direction = The text direction. `"ltr"` for left to right. `"rtl"` for right to left. `"ttb"` for top to bottom. `"btt"` for bottom to top. Default: `"ltr"`
// language = The language the text is in. Default: `"en"`
// script = The script the text is in. Default: `"latin"`
// atype = Change vertical center between "baseline" and "ycenter". Default: "baseline"
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"baseline"`
// center = Center the text. Equivalent to `atype="center", anchor=CENTER`. Default: false
// 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`
// Anchor Types:
// baseline = Anchor center is relative to text baseline
// ycenter = Anchor center is relative to the actual y direction center of the text
// Examples:
// text3d("Fogmobar", h=3, size=10);
// text3d("Fogmobar", h=2, size=12, font=":style=bold");
// text3d("Fogmobar", h=2, anchor=CENTER);
// text3d("Fogmobar", h=2, anchor=CENTER, atype="ycenter");
// text3d("Fogmobar", h=2, anchor=RIGHT);
// text3d("Fogmobar", h=2, anchor=RIGHT+BOT, atype="ycenter");
module text3d(text, h, size=10, font, spacing=1.0, direction="ltr", language="en", script="latin",
height, thickness, atype, center=false,
anchor, spin=0, orient=UP) {
no_children($children);
h = one_defined([h,height,thickness],"h,height,thickness",dflt=1);
assert(is_undef(atype) || in_list(atype,["ycenter","baseline"]), "atype must be \"ycenter\" or \"baseline\"");
assert(is_bool(center));
assert(is_undef($attach_to),"text3d() does not support parent-child anchor attachment with two parameters");
atype = default(atype, center?"ycenter":"baseline");
anchor = default(anchor, center?CENTER:LEFT);
geom = attach_geom(size=[size,size,h]);
ha = anchor.x<0? "left"
: anchor.x>0? "right"
: "center";
va = anchor.y<0? "bottom"
: anchor.y>0? "top"
: atype=="baseline"? "baseline"
: "center";
m = _attach_transform([0,0,anchor.z],spin,orient,geom);
multmatrix(m) {
$parent_anchor = anchor;
$parent_spin = spin;
$parent_orient = orient;
$parent_geom = geom;
$parent_size = _attach_geom_size(geom);
$attach_to = undef;
if (_is_shown()) {
_color($color) {
linear_extrude(height=h, center=true)
_text(
text=text, size=size, font=font,
halign=ha, valign=va, spacing=spacing,
direction=direction, language=language,
script=script
);
}
}
}
}
// This could be replaced with _cut_to_seg_u_form
function _cut_interp(pathcut, path, data) =
[for(entry=pathcut)
let(
a = path[entry[1]-1],
b = path[entry[1]],
c = entry[0],
i = max_index(v_abs(b-a)),
factor = (c[i]-a[i])/(b[i]-a[i])
)
(1-factor)*data[entry[1]-1]+ factor * data[entry[1]]
];
// Module: path_text()
// Synopsis: Creates 2d or 3d text placed along a path.
// SynTags: Geom
// Topics: Text, Paths, Paths (2D), Paths (3D), Path Generators, Path Generators (2D)
// See Also, text(), text2d()
// Usage:
// path_text(path, text, [size], [thickness], [font], [lettersize=], [offset=], [reverse=], [normal=], [top=], [textmetrics=], [kern=])
// Description:
// Place the text letter by letter onto the specified path using textmetrics (if available and requested)
// or user specified letter spacing. The path can be 2D or 3D. In 2D the text appears along the path with letters upright
// as determined by the path direction. In 3D by default letters are positioned on the tangent line to the path with the path normal
// pointing toward the reader. The path normal points away from the center of curvature (the opposite of the normal produced
// by path_normals()). Note that this means that if the center of curvature switches sides the text will flip upside down.
// If you want text on such a path you must supply your own normal or top vector.
// .
// Text appears starting at the beginning of the path, so if the 3D path moves right to left
// then a left-to-right reading language will display in the wrong order. (For a 2D path text will appear upside down.)
// The text for a 3D path appears positioned to be read from "outside" of the curve (from a point on the other side of the
// curve from the center of curvature). If you need the text to read properly from the inside, you can set reverse to
// true to flip the text, or supply your own normal.
// .
// If you do not have the experimental textmetrics feature enabled then you must specify the space for the letters
// using lettersize, which can be a scalar or array. You will have the easiest time getting good results by using
// a monospace font such as "Liberation Mono". Note that even with text metrics, spacing may be different because path_text()
// doesn't do kerning to adjust positions of individual glyphs. Also if your font has ligatures they won't be used.
// .
// By default letters appear centered on the path. The offset can be specified to shift letters toward the reader (in
// the direction of the normal).
// .
// You can specify your own normal by setting `normal` to a direction or a list of directions. Your normal vector should
// point toward the reader. You can also specify
// top, which directs the top of the letters in a desired direction. If you specify your own directions and they
// are not perpendicular to the path then the direction you specify will take priority and the
// letters will not rest on the tangent line of the path. Note that the normal or top directions that you
// specify must not be parallel to the path.
// .
// Historically fonts were specified by their "body size", the height of the metal body
// on which the glyphs were cast. This means the size was an upper bound on the size
// of the font glyphs, not a direct measurement of their size. In digital typesetting,
// the metal body is replaced by an invisible box, the em square, whose side length is
// defined to be the font's size. The glyphs can be contained in that square, or they
// can extend beyond it, depending on the choices made by the font designer. As a
// result, the meaning of font size varies between fonts: two fonts at the "same" size
// can differ significantly in the actual size of their characters. Typographers
// customarily specify the size in the units of "points". A point is 1/72 inch. In
// OpenSCAD, you specify the size in OpenSCAD units (often treated as millimeters for 3d
// printing), so if you want points you will need to perform a suitable unit conversion.
// In addition, the OpenSCAD font system has a bug: if you specify size=s you will
// instead get a font whose size is s/0.72. For many fonts this means the size of
// capital letters will be approximately equal to s, because it is common for fonts to
// use about 70% of their height for the ascenders in the font. To get the customary
// font size, you should multiply your desired size by 0.72.
// .
// To find the fonts that you have available in your OpenSCAD installation,
// go to the Help menu and select "Font List".
// Arguments:
// path = path to place the text on
// text = text to create
// size = The font will be created at this size divided by 0.72.
// thickness / h / height = thickness of letters (not allowed for 2D path)
// font = Font to use. Default: "Liberation Sans" (standard OpenSCAD default)
// ---
// lettersize = scalar or array giving size of letters
// center = center text on the path instead of starting at the first point. Default: false
// offset = distance to shift letters "up" (towards the reader). Not allowed for 2D path. Default: 0
// normal = direction or list of directions pointing towards the reader of the text. Not allowed for 2D path.
// top = direction or list of directions pointing toward the top of the text
// reverse = reverse the letters if true. Not allowed for 2D path. Default: false
// textmetrics = if set to true and lettersize is not given then use the experimental textmetrics feature. You must be running a dev snapshot that includes this feature and have the feature turned on in your preferences. Default: false
// valign = align text to the path using "top", "bottom", "center" or "baseline". You can also adjust position with a numerical offset as in "top-5" or "bottom+2". This only works with textmetrics enabled. You can give a simple numerical offset, which will be relative to the baseline and works even without textmetrics. Default: "baseline"
// kern = scalar or array giving spacing adjusments between each letter. If it's an array it should have one less entry than the text string. Default: 0
// language = text language, passed to OpenSCAD `text()`. Default: "en"
// script = text script, passed to OpenSCAD `text()`. Default: "latin"
// Example(3D,NoScales): The examples use Liberation Mono, a monospaced font. The width is 1/1.2 times the specified size for this font. This text could wrap around a cylinder.
// path = path3d(arc(100, r=25, angle=[245, 370]));
// color("red")stroke(path, width=.3);
// path_text(path, "Example text", font="Liberation Mono", size=5, lettersize = 5/1.2);
// Example(3D,NoScales): By setting the normal to UP we can get text that lies flat, for writing around the edge of a disk:
// path = path3d(arc(100, r=25, angle=[245, 370]));
// color("red")stroke(path, width=.3);
// path_text(path, "Example text", font="Liberation Mono", size=5, lettersize = 5/1.2, normal=UP);
// Example(3D,NoScales): If we want text that reads from the other side we can use reverse. Note we have to reverse the direction of the path and also set the reverse option.
// path = reverse(path3d(arc(100, r=25, angle=[65, 190])));
// color("red")stroke(path, width=.3);
// path_text(path, "Example text", font="Liberation Mono", size=5, lettersize = 5/1.2, reverse=true);
// Example(3D,Med,NoScales): text debossed onto a cylinder in a spiral. The text is 1 unit deep because it is half in, half out.
// text = ("A long text example to wrap around a cylinder, possibly for a few times.");
// L = 5*len(text);
// maxang = 360*L/(PI*50);
// spiral = [for(a=[0:1:maxang]) [25*cos(a), 25*sin(a), 10-30/maxang*a]];
// difference(){
// cyl(d=50, l=50, $fn=120);
// path_text(spiral, text, size=5, lettersize=5/1.2, font="Liberation Mono", thickness=2);
// }
// Example(3D,Med,NoScales): Same example but text embossed. Make sure you have enough depth for the letters to fully overlap the object.
// text = ("A long text example to wrap around a cylinder, possibly for a few times.");
// L = 5*len(text);
// maxang = 360*L/(PI*50);
// spiral = [for(a=[0:1:maxang]) [25*cos(a), 25*sin(a), 10-30/maxang*a]];
// cyl(d=50, l=50, $fn=120);
// path_text(spiral, text, size=5, lettersize=5/1.2, font="Liberation Mono", thickness=2);
// Example(3D,NoScales): Here the text baseline sits on the path. (Note the default orientation makes text readable from below, so we specify the normal.)
// path = arc(100, points = [[-20, 0, 20], [0,0,5], [20,0,20]]);
// color("red")stroke(path,width=.2);
// path_text(path, "Example Text", size=5, lettersize=5/1.2, font="Liberation Mono", normal=FRONT);
// Example(3D,NoScales): If we use top to orient the text upward, the text baseline is no longer aligned with the path.
// path = arc(100, points = [[-20, 0, 20], [0,0,5], [20,0,20]]);
// color("red")stroke(path,width=.2);
// path_text(path, "Example Text", size=5, lettersize=5/1.2, font="Liberation Mono", top=UP);
// Example(3D,Med,NoScales): This sine wave wrapped around the cylinder has a twisting normal that produces wild letter layout. We fix it with a custom normal which is different at every path point.
// path = [for(theta = [0:360]) [25*cos(theta), 25*sin(theta), 4*cos(theta*4)]];
// normal = [for(theta = [0:360]) [cos(theta), sin(theta),0]];
// zrot(-120)
// difference(){
// cyl(r=25, h=20, $fn=120);
// path_text(path, "A sine wave wiggles", font="Liberation Mono", lettersize=5/1.2, size=5, normal=normal);
// }
// Example(3D,Med,NoScales): The path center of curvature changes, and the text flips.
// path = zrot(-120,p=path3d( concat(arc(100, r=25, angle=[0,90]), back(50,p=arc(100, r=25, angle=[268, 180])))));
// color("red")stroke(path,width=.2);
// path_text(path, "A shorter example", size=5, lettersize=5/1.2, font="Liberation Mono", thickness=2);
// Example(3D,Med,NoScales): We can fix it with top:
// path = zrot(-120,p=path3d( concat(arc(100, r=25, angle=[0,90]), back(50,p=arc(100, r=25, angle=[268, 180])))));
// color("red")stroke(path,width=.2);
// path_text(path, "A shorter example", size=5, lettersize=5/1.2, font="Liberation Mono", thickness=2, top=UP);
// Example(2D,NoScales): With a 2D path instead of 3D there's no ambiguity about direction and it works by default:
// path = zrot(-120,p=concat(arc(100, r=25, angle=[0,90]), back(50,p=arc(100, r=25, angle=[268, 180]))));
// color("red")stroke(path,width=.2);
// path_text(path, "A shorter example", size=5, lettersize=5/1.2, font="Liberation Mono");
// Example(3D,NoScales): The kern parameter lets you adjust the letter spacing either with a uniform value for each letter, or with an array to make adjustments throughout the text. Here we show a case where adding some extra space gives a better look in a tight circle. When textmetrics are off, `lettersize` can do this job, but with textmetrics, you'll need to use `kern` to make adjustments relative to the text metric sizes.
// path = path3d(arc(100, r=12, angle=[150, 450]));
// color("red")stroke(path, width=.3);
// kern = [1,1.2,1,1,.3,-.2,1,0,.8,1,1.1];
// path_text(path, "Example text", font="Liberation Mono", size=5, lettersize = 5/1.2, kern=kern, normal=UP);
module path_text(path, text, font, size, thickness, lettersize, offset=0, reverse=false, normal, top, center=false,
textmetrics=false, kern=0, height,h, valign="baseline", language, script)
{
no_children($children);
dummy2=assert(is_path(path,[2,3]),"Must supply a 2d or 3d path")
assert(num_defined([normal,top])<=1, "Cannot define both \"normal\" and \"top\"")
assert(all_positive([size]), "Must give positive text size");
dim = len(path[0]);
normalok = is_undef(normal) || is_vector(normal,3) || (is_path(normal,3) && len(normal)==len(path));
topok = is_undef(top) || is_vector(top,dim) || (dim==2 && is_vector(top,3) && top[2]==0)
|| (is_path(top,dim) && len(top)==len(path));
dummy4 = assert(dim==3 || !any_defined([thickness,h,height]), "Cannot give a thickness or height with 2d path")
assert(dim==3 || !reverse, "Reverse not allowed with 2d path")
assert(dim==3 || offset==0, "Cannot give offset with 2d path")
assert(dim==3 || is_undef(normal), "Cannot define \"normal\" for a 2d path, only \"top\"")
assert(normalok,"\"normal\" must be a vector or path compatible with the given path")
assert(topok,"\"top\" must be a vector or path compatible with the given path");
thickness = one_defined([thickness,h,height],"thickness,h,height",dflt=1);
normal = is_vector(normal) ? repeat(normal, len(path))
: is_def(normal) ? normal
: undef;
top = is_vector(top) ? repeat(dim==2?point2d(top):top, len(path))
: is_def(top) ? top
: undef;
kern = force_list(kern, len(text)-1);
dummy3 = assert(is_list(kern) && len(kern)==len(text)-1, "kern must be a scalar or list whose length is len(text)-1");
lsize = is_def(lettersize) ? force_list(lettersize, len(text))
: textmetrics ? [for(letter=text) let(t=textmetrics(letter, font=font, size=size)) t.advance[0]]
: assert(false, "textmetrics disabled: Must specify letter size");
lcenter = convolve(lsize,[1,1]/2)+[0,each kern,0] ;
textlength = sum(lsize)+sum(kern);
ascent = !textmetrics ? undef
: textmetrics(text, font=font, size=size).ascent;
descent = !textmetrics ? undef
: textmetrics(text, font=font, size=size).descent;
vadjustment = is_num(valign) ? -valign
: !textmetrics ? assert(valign=="baseline","valign requires textmetrics support") 0
: let(
table = [
["baseline", 0],
["top", -ascent],
["bottom", descent],
["center", (descent-ascent)/2]
],
match = [for(i=idx(table)) if (starts_with(valign,table[i][0])) i]
)
assert(len(match)==1, "Invalid valign value")
table[match[0]][1] - parse_num(substr(valign,len(table[match[0]][0])));
dummy1 = assert(textlength<=path_length(path),"Path is too short for the text");
start = center ? (path_length(path) - textlength)/2 : 0;
pts = path_cut_points(path, add_scalar(cumsum(lcenter),start), direction=true);
usernorm = is_def(normal);
usetop = is_def(top);
normpts = is_undef(normal) ? (reverse?1:-1)*column(pts,3) : _cut_interp(pts,path, normal);
toppts = is_undef(top) ? undef : _cut_interp(pts,path,top);
attachable(){
for (i = idx(text)) {
tangent = pts[i][2];
checks =
assert(!usetop || !approx(tangent*toppts[i],norm(top[i])*norm(tangent)),
str("Specified top direction parallel to path at character ",i))
assert(usetop || !approx(tangent*normpts[i],norm(normpts[i])*norm(tangent)),
str("Specified normal direction parallel to path at character ",i));
adjustment = usetop ? (tangent*toppts[i])*toppts[i]/(toppts[i]*toppts[i])
: usernorm ? (tangent*normpts[i])*normpts[i]/(normpts[i]*normpts[i])
: [0,0,0];
move(pts[i][0]) {
if (dim==3) {
frame_map(
x=tangent-adjustment,
z=usetop ? undef : normpts[i],
y=usetop ? toppts[i] : undef
) up(offset-thickness/2) {
linear_extrude(height=thickness)
back(vadjustment)
{
left(lsize[i]/2)
text(text[i], font=font, size=size, language=language, script=script);
}
}
} else {
frame_map(
x=point3d(tangent-adjustment),
y=point3d(usetop ? toppts[i] : -normpts[i])
) left(lsize[0]/2) {
text(text[i], font=font, size=size, language=language, script=script);
}
}
}
}
union();
}
}