Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added text wrapping, consolidated text operations into new file #1542

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

amatulic
Copy link
Contributor

  • Added text wrapping:
    • wraptext() function
    • array_text() module
    • textarray_boundingbox() function
  • Created new file text.scad for text wrapping, and moved text-related features into it:
    • text() from shapes2d.scad
    • text3d() and path_text() from shapes3d.scad

Re-tested examples, did some minor grammar fixing in comments.

@revarbat
Copy link
Collaborator

I'm not sure if there is anything that needs editing in .openscad_docsgenrc for adding this new file, but I recommend checking it out.

@adrianVmariano
Copy link
Collaborator

Typo "paragraph" in docs.

Should indent function bodies.

For consistency probably "text_array" instead of "textarray". (Note: before BOSL2 I never used underscores; that was Revar's requirement.)

The text_array_boundingbox function doesn't return a bounding box, only a width and height. Maybe it should be renamed text_array_size?

The array_text() module maybe should be rolled into text()? That is, if you pass a string it calls the current code and if you pass an array of strings or array of array of strings (?) it calls the new code.

Also, this module doesn't have any attachable handling, which is bad. You should always call attachable() unless there's some very compelling reason you can't, in which case you need to manually support a bunch of stuff---so many things actually that half of them are missing from text() because I didn't realize I needed to add them there. But in particular, take a look at text(). Do you see all the stuff in there that sets $ variables and so on. If you don't call attachable you need to do all that stuff...and more. (Take a look at attachable and what it does when it creates the main child object and then the rest of the children. All that should be happening in text....and in array_text().)

If array_text stays as a separate module I'm not sure about the name. Maybe it should be text_array() if it stays separate? But also, it should be grouped with text() as a rendering module in the docs. Maybe this is obvious, but we always need to think about laying our our code in the way that makes the most sense for a user reading the docs, NOT the way that makes the most sense for a programming reading the code!

@adrianVmariano
Copy link
Collaborator

Hmmm. Relatively new no-arg version of attachable() may eliminate need to avoid attachable(), though some manual setting may still be necessary for $ variables.

There's at least one line ending with ':' where the ':' could go to the next line.

@adrianVmariano
Copy link
Collaborator

It looks like using attachable() with no args can indeed replace the stuff currently in text() and text3d() and provides added functionality.

It looks to me like you have basically reimplemented the anchoring transform for a box instead of just invoking attachable() in array_text(), so it seems like you ought to be able to just invoke attachable with a size arg.

@adrianVmariano
Copy link
Collaborator

The main reason, I think, why text() and text3d() don't do it that way is that they are designed to work in the stable OpenSCAD where we do not know the dimensions of the text.

@amatulic
Copy link
Contributor Author

amatulic commented Jan 12, 2025

I note that the text() implementation can't have things attached to it, in fact the docs for text() state "You cannot attach children to text." All that's needed is to for it to be attachable with an anchor. That's what I was going for too. I tested it, and there's one example using the anchor.

Merging array_text() into text() might save me the trouble of having to figure out attachments again. There may be a complication in that with multi-line text, halign aligns the multi-line text within its bounding box, and that bounding box is what gets the anchor. Another complication is the difference in default fonts, particularly between 2021.01 and the snapshot builds. For those reasons it may be best to keep them separate.

How is a bounding box defined if not with width and height? Those are the dimensions of the box. The position of the box depends on the anchor. I could call it something besides bounding box, maybe textarray_dimensions?

I tend never to use underscores in variable names, just in module and function names. Not a problem to change, however.

@adrianVmariano
Copy link
Collaborator

The reason you cannot attach children to text is that we don't know where the text is because we don't know it's size, so it is simply impossible to implement attachment. This is not true for text_array because you are assuming you have its dimensions, so what reason is there to not support attachment by just using attachable, which makes your code much simpler....and even more so when you consider what you'd need to add to make everything work without it.

All the other attachable features need to work, so you need to make sure that tagging, highlighting, colors and attachment and so on all work. As I said, text() and text3d() are actually broken right now because highlighting and ghosting don't work.

A bounding box is a size and position. As you say, this depends on the box position, hence it doesn't make sense to say you're producing a bounding box. Native OpenSCAD and hence also BOSL2 use "size" to refer to the dimensions of rectangles and cubes so the name should be text_array_size(). As I said, I don't use underscores at all...except in BOSL2 where we use them in all user-facing identifiers to separate all words.

Merging array_text() into text() will not save you any effort because you're going to have to do it as a simple two-way conditional, where you invoke the old code if the old OpenSCAD is running and you have a single string, otherwise you invoke the new code. Keep in mind that attachable does a ton of stuff. If you want attach() to work right you need to make sure you have set all the right $ variables, for example, and set a geometry type. Getting all of that right is much harder than just invoking attachable(), and in this case no weird anchor overrides are required, so it's straight forward. The poor man's quasi-anchoring currently available in text() and text3d() is not something to aspire to. It's a workaround hack.

This does raise a question, though, about anchoring consistency across versions. You can ban children like we do right now, but that seems a bit unfortunate. One would actually like to be able to do things like text("foo") attach(FWD) text("bar") attach(FWD) text("baz"); to create 3 lines of text, though admittedly this isn't necessary with multi-line-text. I suppose being able to position text between two pieces of geometry would be an example. But having children be sometimes supported and sometimes not supported seems kind of wonky. I guess there could be an error that says "Children are not supported in OpenSCAD version 2021.01"

@amatulic
Copy link
Contributor Author

amatulic commented Jan 12, 2025

The only way to get the bounding box is to use textmetrics, which doesn't exist in 2021.01. Perhaps text() could be modified to use this and then the full attachment capabilities would be possible.

Because the bounding box (which I shall rename to text_array_size) is a rectangle, and that's all we're concerned about for the purpose of attachments, how about for now I make the attachments the same as with square()?

The more I think about it, the more I think array_text() should be separate from text(). There are minor differences that could lead to confusion if mashed together into the same module:

  • array_text() needs an additional line height parameter
  • by necessity it uses different defaults for the font face if not specified by the user
  • the valign parameter is meaningless with wrapped text; it's always aligned to fit inside the bounding box, and that is what gets positioned with anchoring
  • the halign parameter doesn't align the text with respect to the origin like with text(), it aligns the text inside the bounding box; e.g. halign="right" right-justifies the text, and halign="center" centers the wrapped text within its container. I thought real hard about this, and decided this is what users would expect halign to do for multi-line text.
  • I was thinking (for later) to expand halign to include a value "justified" to justify both the left and right edges of the wrapped text, further counfounding the difference in meaning from this parameter in text(). That would be a complicated change, however, because I'd need to introduce a variable-width spacing that isn't part of the font. Right now it uses only information in fontmetrics.

P.S. Instead of repurposing halign I could could instead omit it as I do with valign and instead call the parameter justify. Hmm, yes, that would make sense. It could have the values left, right, center, and full.

@adrianVmariano
Copy link
Collaborator

I think you're right that we should not wrap the new functionality into text(). I was losing sight of the fact that text() is a native openscad command.

Your command should be a complete replacement for text and I'm not sure what it should be called, but I feel like it shouldn't have "array" in the name. It's just the new better command for doing text, like we have rect as an alternative to square and cuboid as an alternative to cube.

There should probably also be a 3d version like text3d.

I don't understand what the line_spacing parameter does. There should be a correct line spacing, which should be the default.

The "spacing" parameter is also probably something questionable that perhaps isn't needed. Adding space between letters in a font is typographically bad generally speaking. The font was designed with the correct spacing. But the spacing parameter adds a multiplier which creates a ridiculous look. There's no reason you'd ever want to use it. If you actually wanted to screw up the font by spacing letters apart I think you'd want to add a fixed amount between each letter, not a proportional amount.

It might also be appropriate for a BOSL2 font replacement module to fix the 0.72 factor bug in the font sizes. I'm not sure about that, though.

@amatulic
Copy link
Contributor Author

amatulic commented Jan 13, 2025

The latest commit has some echo statements for debugging.

It works fine for snapshot OpenSCAD, but for 2021.01 it has weird behavior with parameters changing values.

Try this test in a snapshot and in 2021.01.

include<BOSL2/std.scad>
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);

The width value is displayed just before passing it into _justify_text(), and then displayed again as the first line in that module. The value is unchanged as expected in snapshots, but changes to an array (output of _glyphdata() for some reason) in 2021.01.

@amatulic
Copy link
Contributor Author

I don't understand what the line_spacing parameter does. There should be a correct line spacing, which should be the default.

The default is line_spacing=1. It's the vertical couterpart to spacing in text(). Both are proportions of the spacing. At line_spacing=1 you get text lines spaced vertically in increments of the font's nominal line height.

I frequently make use of the spacing parameter in my work, because for most fonts that I need to print, at small sizes the spacing gets smaller than a line width on the 3D printer, causing engraved text to run together when printed. I nearly always set spacing=1.1 in most of my projects that need 3D printed text using size=8 or less.

@Jasonkoolman
Copy link

Jasonkoolman commented Jan 21, 2025

I frequently make use of the spacing parameter in my work, because for most fonts that I need to print, at small sizes the spacing gets smaller than a line width on the 3D printer, causing engraved text to run together when printed. I nearly always set spacing=1.1 in most of my projects that need 3D printed text using size=8 or less.

Line spacing (also known as line height) is indeed commonly used and quite important for having the ability to control it effectively.

Adding space between letters in a font is typographically bad generally speaking.

As an application and web developer with a UX background, I would disagree. There are valid use cases - for example, increasing letter spacing for buttons or headlines to enhance visibility and clarity.

@Jasonkoolman
Copy link

@amatulic For what it's worth, I’d love to see a version of justify_text made public. I often find myself using a basic implementation to center text such as:

/**
 * Centers child text based on its metrics.
 *
 * @param metrics   A textmetrics object.
 */
module center_text(metrics) {
    assert(
        !is_undef(metrics) && is_list(metrics.position) && is_list(metrics.size),
        "Invalid textmetrics object supplied."
    );

    cx = -(metrics.position.x + metrics.size.x / 2);
    cy = -(metrics.position.y + metrics.size.y / 2);

    translate([cx, cy])
        children();
}

@amatulic
Copy link
Contributor Author

@Jasonkoolman - everything is already finished with respect to text justification, centering, line wrapping etc. The problem now is that we're considering making all of this into a wholesale replacement for BOSL2's existing text(), which in turn is a replacement for OpenSCAD's text(). We're planning to call it write() to include all the additional features such as justification, anchoring, attachments, and wordwrapping.

However, when I made this PR I never intended to replace text(), my main focus was to implement wordwrapping as a separate thing, but it turned out that what I created has enough overlap that it can replace text() -- and that's going to be a big job. I've been putting it off because I know I'm in for a lot more revisions, especially regarding anchoring and attachments, which I understand as a user but not as a programmer. In the meantime I've been working on implementing isosurfaces and meta-balls, and that's nearly done.

@Jasonkoolman
Copy link

Jasonkoolman commented Jan 22, 2025

Thanks for the clarification! I’m not sure I fully understand the complexity of anchoring. In my mind, I’m imagining an extruded 2D shape with a rectangular bounding box. Are you thinking about adding anchors to the individual ‘faces’ of characters/glyphs, something of that nature?

@amatulic
Copy link
Contributor Author

@Jasonkoolman - the anchors and attachment points would be at the center, corners, and edges of the text bounding box, as well as the left, center, and right ends of the text baseline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants