discuss@lists.openscad.org

OpenSCAD general discussion

View all threads

Please eyeball braille.scad, v0.1

JH
John Heim
Tue, Aug 23, 2022 4:25 PM

All,

Below is the Open SCAD code I wrote for braille. My goal is to write
code that makes up for the deficiencies of the OpenSCAD code I found on
the internet (primarily github). I couldn't find any code that would
generate officially correct braille. So I wrote a library that takes
unicode braille characters and draws them as 3D braille characters. No
attempt is made to do the transcription. I'll be posting that in a
separate library in a separate message.

Meanwhile, I would very much appreciate feedback on the drawing part of
my code. Does it look close? If so, I am going to put a stl file on a
thumb drive and go down to the local makers space to try it out.

=== cut here ===

/*
This OpenSCAD library accepts unicode braille characters and draws them
as 3D braille characters.

Notes:

  1. This library is intended to work regardless of language or braille
    system. Thus the reliance
    on unicode braille characters. No attempt at braille transcription, the
    process of changing plain text into braille characters, is attempted.
  2. This library is intended  to conform to the ADA specifications for
    braille signage. Feedback
    and comments in this regard is appreciated. The modules drawDot and
    drawCharacter are
    used to draw dots and characters respectively. Suggestions for
    improvements to these modules can be sent to the author.

Author: John Heim, john@johnheim.com

*/

//For the given unicode braille character, return a true/false value
// depending on whether the character has a dot at the given index.
function hasDot (character, idx) = ((floor((ord(character) - 10240) /
2^idx) % 2)) ? true : false;

// Return an array of true/false values representing the dots in a given
unicode braille character.
function unicodeCharacterDots (character) = [for(idx = [0:5])
hasDot(character, idx) ];

/*
The following code is mostly borrowed from
*/
$fa = 0.01; $fs = 0.5;

// global dimensions from
http://www.brailleauthority.org/sizespacingofbraille/sizespacingofbraille.pdf
$dotHeight = 0.48;
$dotBase = 1.44;
$dotRadius = $dotBase /2;
$dotWidth= 2.34;
$charWidth = 6.2;
$lineHeight = 10;

// compute the sphere to make the raised dot
$dotSphereRadius  =  chord_radius($dotBase,$dotHeight);
$dotSphereOffset = $dotSphereRadius - $dotHeight;

function max_length_r(v, i, max) =
     i == len(v) ? max : max_length_r(v, i+1, len(v[i]) > max ?
len(v[i]) : max);

function max_length(v) = max_length_r(v,0,0);

// dot is not a hemisphere
function chord_radius(length,height) = ( length * length /(4 * height) +
height)/2;

module drawDot(location) {
    translate(location) translate ([0,0, -$dotSphereOffset ])
sphere($dotSphereRadius);
}

module drawCharacter(character) {
    dots = unicodeCharacterDots (character);
    for (dot = [1:6]) {
        if (dots[dot - 1]) {
            drawDot(   [floor(dot / 3) * $dotWidth,  -((1+(dot % 3)) *
$dotWidth),   0] );
        }
    }
}

module drawLine(line) {
    for(idx = [0:len(line)-1]) {
        translate([$charWidth * idx, 0, 0]) {
            drawCharacter (line[idx]);
        }
    }
}

module drawText(text, just=1) {
    totalHeight = len(text) * $lineHeight;
    mx = max_length(text);
    //echo(mx, $charWidth);
    hz = -1 * mx * $charWidth / 2;
    translate([0, 0, 1]) {
        for(i = [0: len(text)-1]) {
            vrt = totalHeight-$lineHeight*(i);
            if (just==1) {
                hz = -len(text[i])*$charWidth/2;
                translate([hz, vrt, 0]) {
                    drawLine(text[i]);
                }
            } else {
                hz = -1 * mx * $charWidth / 2;
                translate([hz, vrt, 0]) {
                    drawLine(text[i]);
                }
            }
        }
    }
}

module brailleLabel(text, depth=2, just=1) {
    width = (max_length(text) + 2)  * $charWidth;
    height = len(text) * $lineHeight;
    union() {
        translate([0, height/2, 0]) {
            cube([width, height, depth], true);
        }
        drawText(text, just);
    }
}
// EOF=== cut here ===

All, Below is the Open SCAD code I wrote for braille. My goal is to write code that makes up for the deficiencies of the OpenSCAD code I found on the internet (primarily github). I couldn't find any code that would generate officially correct braille. So I wrote a library that takes unicode braille characters and draws them as 3D braille characters. No attempt is made to do the transcription. I'll be posting that in a separate library in a separate message. Meanwhile, I would very much appreciate feedback on the drawing part of my code. Does it look close? If so, I am going to put a stl file on a thumb drive and go down to the local makers space to try it out. === cut here === /* This OpenSCAD library accepts unicode braille characters and draws them as 3D braille characters. Notes: 1. This library is intended to work regardless of language or braille system. Thus the reliance on unicode braille characters. No attempt at braille transcription, the process of changing plain text into braille characters, is attempted. 2. This library is intended  to conform to the ADA specifications for braille signage. Feedback and comments in this regard is appreciated. The modules drawDot and drawCharacter are used to draw dots and characters respectively. Suggestions for improvements to these modules can be sent to the author. Author: John Heim, john@johnheim.com */ //For the given unicode braille character, return a true/false value // depending on whether the character has a dot at the given index. function hasDot (character, idx) = ((floor((ord(character) - 10240) / 2^idx) % 2)) ? true : false; // Return an array of true/false values representing the dots in a given unicode braille character. function unicodeCharacterDots (character) = [for(idx = [0:5]) hasDot(character, idx) ]; /* The following code is mostly borrowed from */ $fa = 0.01; $fs = 0.5; // global dimensions from http://www.brailleauthority.org/sizespacingofbraille/sizespacingofbraille.pdf $dotHeight = 0.48; $dotBase = 1.44; $dotRadius = $dotBase /2; $dotWidth= 2.34; $charWidth = 6.2; $lineHeight = 10; // compute the sphere to make the raised dot $dotSphereRadius  =  chord_radius($dotBase,$dotHeight); $dotSphereOffset = $dotSphereRadius - $dotHeight; function max_length_r(v, i, max) =      i == len(v) ? max : max_length_r(v, i+1, len(v[i]) > max ? len(v[i]) : max); function max_length(v) = max_length_r(v,0,0); // dot is not a hemisphere function chord_radius(length,height) = ( length * length /(4 * height) + height)/2; module drawDot(location) {     translate(location) translate ([0,0, -$dotSphereOffset ]) sphere($dotSphereRadius); } module drawCharacter(character) {     dots = unicodeCharacterDots (character);     for (dot = [1:6]) {         if (dots[dot - 1]) {             drawDot(   [floor(dot / 3) * $dotWidth,  -((1+(dot % 3)) * $dotWidth),   0] );         }     } } module drawLine(line) {     for(idx = [0:len(line)-1]) {         translate([$charWidth * idx, 0, 0]) {             drawCharacter (line[idx]);         }     } } module drawText(text, just=1) {     totalHeight = len(text) * $lineHeight;     mx = max_length(text);     //echo(mx, $charWidth);     hz = -1 * mx * $charWidth / 2;     translate([0, 0, 1]) {         for(i = [0: len(text)-1]) {             vrt = totalHeight-$lineHeight*(i);             if (just==1) {                 hz = -len(text[i])*$charWidth/2;                 translate([hz, vrt, 0]) {                     drawLine(text[i]);                 }             } else {                 hz = -1 * mx * $charWidth / 2;                 translate([hz, vrt, 0]) {                     drawLine(text[i]);                 }             }         }     } } module brailleLabel(text, depth=2, just=1) {     width = (max_length(text) + 2)  * $charWidth;     height = len(text) * $lineHeight;     union() {         translate([0, height/2, 0]) {             cube([width, height, depth], true);         }         drawText(text, just);     } } // EOF=== cut here ===
JB
Jordan Brown
Sun, Sep 4, 2022 6:58 PM

Sorry for the delay in reviewing this.

I did what I would consider a thorough review, and so I wrote a lot. 
Don't take that as criticism.  Overall I found it well-organized and
easy to follow.  Most of what I wrote is style and organizational
advice.  I did find what looks to me to be a bug, in how drawCharacter
indexes through the dots.

On 8/23/2022 9:25 AM, John Heim wrote:

  1. This library is intended  to conform to the ADA specifications for
    braille signage.

You might include a pointer to those specifications.  (Perhaps that is
the link below, but it might be better here.)

//For the given unicode braille character, return a true/false value
// depending on whether the character has a dot at the given index.
function hasDot (character, idx) =
((floor((ord(character) - 10240) / 2^idx) % 2)) ? true : false;

This might be the first time that I've seen a good use case for bitwise
arithmetic in OpenSCAD.

Rather than using the implicit test for zero and then picking true or
false, I would just directly test for zero.

function hasDot (character, idx) =
    ((floor((ord(character) - 10240) / 2^idx) % 2)) != 0;

I'm a little surprised that you use idx as a variable name.  Doesn't
your screen reader read that as three separate letters, I D X?  I would
have expected you to prefer either the single letter i or the full word
index.

/*
The following code is mostly borrowed from
*/

Is mostly borrowed from ... what?

$dotHeight = 0.48;

Is there a particular reason why you're using dollar-sign variables in
these cases?  Usual convention it to use "normal" variables unless you
need the special call-stack scope of $ variables.

$dotSphereOffset = $dotSphereRadius - $dotHeight;

You might consider reversing the sense of this subtract, so that it
represents the actual offset and you can avoid the negation below.

function max_length_r(v, i, max) =
    i == len(v)
? max
: max_length_r(v, i+1, len(v[i]) > max ? len(v[i]) : max);
function max_length(v) = max_length_r(v,0,0);

Consider instead a non-recursive solution using a list comprehension and
the max() function:

function max_length(v) = max([ for (item=v) len(item) ]);

A trick to avoid the need for the separate _r() function:  use argument
defaulting to set the initial values for those "internal" parameters:

function max_length(v, i=0, max=0) =
     i == len(v) ? max : max_length(v, i+1, len(v[i]) > max ? len(v[i]) : max);

Note that

len(v[i]) > max ? len(v[i]) : max

is the same as

max(len(v[i]), max)

You could avoid the "max" parameter by doing the comparisons on the way
out instead of on the way in.

function max_length(v, i=0) =
     i == len(v) ? 0 : max(len(v[i]), max_length(v, i+1));

I don't know whether there is an optimization difference between those
two based on tail recursion.  For small lists, it won't matter, but it
might matter for large lists.

module drawDot(location) {
    translate(location)
translate ([0,0, -$dotSphereOffset ])
sphere($dotSphereRadius);
}
module drawCharacter(character) {
    dots = unicodeCharacterDots (character);
    for (dot = [1:6]) {
        if (dots[dot - 1]) {
            drawDot([floor(dot / 3) * $dotWidth,  -((1+(dot % 3)) * $dotWidth),   0] );
        }
    }
}

This doesn't seem right.  If dot ranges from 1 to 6, then floor(dot/3)
can be 0, 1, or 2, and 2 is never right.

It also doesn't seem to produce the correct characters.

According to my Unicode table, 10247, 0x2807, should be

O .
O .
O .

and this is yielding

. O
O .
O .

... which aligns with the error that I thought that I saw above. 
Similarly, 10303, 0x283f, should be yielding

O O
O O
O O

and instead is yielding

. O O
O O .
O O .

which isn't valid Braille.

One fix for this is to run the loop from 0 to 5, and eliminate the "-1"
when indexing into the list:

module drawCharacter(character) {
    dots = unicodeCharacterDots (character);
    for (dot = [0:5]) {
        if (dots[dot]) {
            drawDot(   [floor(dot / 3) * $dotWidth,  -((1+(dot % 3)) * $dotWidth),   0] );
        }
    }
}

Note that standard Braille terminology (according to Wikipedia) is to
number the dots 1-6, but for this binary representation 0-5 is more
convenient.  For this isolated use, where the dot number is not exposed,
I would use 0-5, but if you wanted to use 1-6 I'd introduce an
intermediate variable:

module drawCharacter(character) {
    dots = unicodeCharacterDots (character);
    for (dot = [1:6]) {
        idx = dot-1;
        if (dots[idx]) {
            drawDot(   [floor(idx / 3) * $dotWidth,  -((1+(idx % 3)) * $dotWidth),   0] );
        }
    }
}

You have drawCharacter taking responsibility for the X-Y positioning of
the dots, and drawDot taking responsibility for the Z positioning.  You
might consider giving drawDot all of the positioning responsibility, so
that drawCharacter is responsible only for picking which dots to draw. 
Something like:

module drawDot(dot) {
    idx = dot-1;
    location = [
        floor(idx / 3) * $dotWidth,
        -((1+(idx % 3)) * $dotWidth),
        -$dotSphereOffset
    ];
    translate(location) sphere($dotSphereRadius);
}

module drawCharacter(character) {
    dots = unicodeCharacterDots (character);
    for (dot = [1:6]) {
        if (dots[dot-1]) {
            drawDot(dot);
        }
    }
}

Note that because drawDot takes the dot position number as an argument I
had it use the standard Braille one-based numbering; the zero-based
numbering is used only internally.  If you wanted to use standard
Braille one-based numbering a bit more universally, you could introduce
a dummy value to the front of the list returned by
unicodeCharacterDots(), so that it's indexed by the one-based dot number.

I notice that you have drawCharacter draw the Braille character in
negative-Y, below the X axis.  I probably would have drawn it in
positive-Y, above the X axis, for consistency with plain text.  What
you've done is actually more convenient, since when you have multiple
lines they all stay in negative-Y, but isn't how conventional text is done.

module drawText(text, just=1) {

You might pick a more self-explanatory value for just.  Perhaps call it
"halign" (to match text()), and then use values like "left" and
"center".  But:  I see that it always centers the text block, and the
question here is whether each individual line is centered.  Still, it
seems like there could be a more self-explanatory value.

    totalHeight = len(text) * $lineHeight;
    mx = max_length(text);
    //echo(mx, $charWidth);
    hz = -1 * mx * $charWidth / 2;

You are never using this first calculation of hz; you override it below.

What does the z in hz stand for?

    translate([0, 0, 1]) {

Where is this offset of 1 in Z coming from?  It seems like it's there to
move the dots to the top of the cube, but drawText() shouldn't know
about the cube.  Consider that if the depth of the cube isn't 2, this
will be wrong.  drawText() should draw the dots based on Z=0, and
brailleLabel() should take responsibility for positioning that on top of
the cube.

        for(i = [0: len(text)-1]) {
            vrt = totalHeight-$lineHeight*(i);

Unnecessary parentheses around the i.

            if (just==1) {
                hz = -len(text[i])*$charWidth/2;
                translate([hz, vrt, 0]) {
                    drawLine(text[i]);
                }
            } else {
                hz = -1 * mx * $charWidth / 2;

Note that the "-1 *" could be replaced with just negating the
expression, as you did above for the per-line calculation.

                translate([hz, vrt, 0]) {
                    drawLine(text[i]);
                }

It doesn't really matter for cases this small, but note that if you used
an in-line "if" to determine the value of hz, you could have used a
single translate and drawLine, e.g.:

hz = just == 1
    ? -len(text[i]) * $charWidth / 2
    : -mx * $charWidth / 2;
translate([hz, vrt, 0]) {
    drawLine(text[i]);
}

module brailleLabel(text, depth=2, just=1) {
    width = (max_length(text) + 2)  * $charWidth;
    height = len(text) * $lineHeight;
    union() {

The explicit union isn't necessary.  (It's actually somewhat rare that
explicit unions are necessary.  In a 12 thousand line project, I only
have 39 of them.  Most of those are for modules that treat different
children differently.)

        translate([0, height/2, 0]) {
            cube([width, height, depth], true);

I would explicitly say "center=true", rather than leaving the
non-self-explanatory "true" by itself.

However, since you really only need centering in X, and you have to
explicitly back it out in Y and take it into account in Z, I would just
do your own centering in X:

translate([-width/2, 0, 0])
    cube([width, height, depth]);

(Actually, I would use a library module that I wrote that creates a cube
centered or justified in any of the three axes, where I can say "center
in X, positive in Y and Z".)

Sorry for the delay in reviewing this. I did what I would consider a thorough review, and so I wrote a lot.  Don't take that as criticism.  Overall I found it well-organized and easy to follow.  Most of what I wrote is style and organizational advice.  I did find what looks to me to be a bug, in how drawCharacter indexes through the dots. On 8/23/2022 9:25 AM, John Heim wrote: > 2. This library is intended  to conform to the ADA specifications for > braille signage. You might include a pointer to those specifications.  (Perhaps that is the link below, but it might be better here.) > //For the given unicode braille character, return a true/false value > // depending on whether the character has a dot at the given index. > function hasDot (character, idx) = > ((floor((ord(character) - 10240) / 2^idx) % 2)) ? true : false; This might be the first time that I've seen a good use case for bitwise arithmetic in OpenSCAD. Rather than using the implicit test for zero and then picking true or false, I would just directly test for zero. function hasDot (character, idx) = ((floor((ord(character) - 10240) / 2^idx) % 2)) != 0; I'm a little surprised that you use idx as a variable name.  Doesn't your screen reader read that as three separate letters, I D X?  I would have expected you to prefer either the single letter i or the full word index. > /* > The following code is mostly borrowed from > */ Is mostly borrowed from ... what? > $dotHeight = 0.48; Is there a particular reason why you're using dollar-sign variables in these cases?  Usual convention it to use "normal" variables unless you need the special call-stack scope of $ variables. > $dotSphereOffset = $dotSphereRadius - $dotHeight; You might consider reversing the sense of this subtract, so that it represents the actual offset and you can avoid the negation below. > function max_length_r(v, i, max) = >     i == len(v) > ? max > : max_length_r(v, i+1, len(v[i]) > max ? len(v[i]) : max); > function max_length(v) = max_length_r(v,0,0); Consider instead a non-recursive solution using a list comprehension and the max() function: function max_length(v) = max([ for (item=v) len(item) ]); A trick to avoid the need for the separate _r() function:  use argument defaulting to set the initial values for those "internal" parameters: function max_length(v, i=0, max=0) = i == len(v) ? max : max_length(v, i+1, len(v[i]) > max ? len(v[i]) : max); Note that len(v[i]) > max ? len(v[i]) : max is the same as max(len(v[i]), max) You could avoid the "max" parameter by doing the comparisons on the way out instead of on the way in. function max_length(v, i=0) = i == len(v) ? 0 : max(len(v[i]), max_length(v, i+1)); I don't know whether there is an optimization difference between those two based on tail recursion.  For small lists, it won't matter, but it might matter for large lists. > module drawDot(location) { >     translate(location) > translate ([0,0, -$dotSphereOffset ]) > sphere($dotSphereRadius); > } > module drawCharacter(character) { >     dots = unicodeCharacterDots (character); >     for (dot = [1:6]) { >         if (dots[dot - 1]) { >             drawDot([floor(dot / 3) * $dotWidth,  -((1+(dot % 3)) * $dotWidth),   0] ); >         } >     } > } This doesn't seem right.  If dot ranges from 1 to 6, then floor(dot/3) can be 0, 1, or 2, and 2 is never right. It also doesn't seem to produce the correct characters. According to my Unicode table, 10247, 0x2807, should be O . O . O . and this is yielding . O O . O . ... which aligns with the error that I thought that I saw above.  Similarly, 10303, 0x283f, should be yielding O O O O O O and instead is yielding . O O O O . O O . which isn't valid Braille. One fix for this is to run the loop from 0 to 5, and eliminate the "-1" when indexing into the list: module drawCharacter(character) {     dots = unicodeCharacterDots (character);     for (dot = [0:5]) {         if (dots[dot]) {             drawDot(   [floor(dot / 3) * $dotWidth,  -((1+(dot % 3)) * $dotWidth),   0] );         }     } } Note that standard Braille terminology (according to Wikipedia) is to number the dots 1-6, but for this binary representation 0-5 is more convenient.  For this isolated use, where the dot number is not exposed, I would use 0-5, but if you wanted to use 1-6 I'd introduce an intermediate variable: module drawCharacter(character) {     dots = unicodeCharacterDots (character);     for (dot = [1:6]) { idx = dot-1;         if (dots[idx]) {             drawDot(   [floor(idx / 3) * $dotWidth,  -((1+(idx % 3)) * $dotWidth),   0] );         }     } } You have drawCharacter taking responsibility for the X-Y positioning of the dots, and drawDot taking responsibility for the Z positioning.  You might consider giving drawDot all of the positioning responsibility, so that drawCharacter is responsible only for picking which dots to draw.  Something like: module drawDot(dot) { idx = dot-1; location = [ floor(idx / 3) * $dotWidth, -((1+(idx % 3)) * $dotWidth), -$dotSphereOffset ]; translate(location) sphere($dotSphereRadius); } module drawCharacter(character) { dots = unicodeCharacterDots (character); for (dot = [1:6]) { if (dots[dot-1]) { drawDot(dot); } } } Note that because drawDot takes the dot position number as an argument I had it use the standard Braille one-based numbering; the zero-based numbering is used only internally.  If you wanted to use standard Braille one-based numbering a bit more universally, you could introduce a dummy value to the front of the list returned by unicodeCharacterDots(), so that it's indexed by the one-based dot number. I notice that you have drawCharacter draw the Braille character in negative-Y, below the X axis.  I probably would have drawn it in positive-Y, above the X axis, for consistency with plain text.  What you've done is actually more convenient, since when you have multiple lines they all stay in negative-Y, but isn't how conventional text is done. > module drawText(text, just=1) { You might pick a more self-explanatory value for just.  Perhaps call it "halign" (to match text()), and then use values like "left" and "center".  But:  I see that it always centers the text block, and the question here is whether each individual line is centered.  Still, it seems like there could be a more self-explanatory value. >     totalHeight = len(text) * $lineHeight; >     mx = max_length(text); >     //echo(mx, $charWidth); >     hz = -1 * mx * $charWidth / 2; You are never using this first calculation of hz; you override it below. What does the z in hz stand for? >     translate([0, 0, 1]) { Where is this offset of 1 in Z coming from?  It seems like it's there to move the dots to the top of the cube, but drawText() shouldn't know about the cube.  Consider that if the depth of the cube isn't 2, this will be wrong.  drawText() should draw the dots based on Z=0, and brailleLabel() should take responsibility for positioning that on top of the cube. >         for(i = [0: len(text)-1]) { >             vrt = totalHeight-$lineHeight*(i); Unnecessary parentheses around the i. >             if (just==1) { >                 hz = -len(text[i])*$charWidth/2; >                 translate([hz, vrt, 0]) { >                     drawLine(text[i]); >                 } >             } else { >                 hz = -1 * mx * $charWidth / 2; Note that the "-1 *" could be replaced with just negating the expression, as you did above for the per-line calculation. >                 translate([hz, vrt, 0]) { >                     drawLine(text[i]); >                 } It doesn't really matter for cases this small, but note that if you used an in-line "if" to determine the value of hz, you could have used a single translate and drawLine, e.g.: hz = just == 1 ? -len(text[i]) * $charWidth / 2 : -mx * $charWidth / 2; translate([hz, vrt, 0]) { drawLine(text[i]); } > module brailleLabel(text, depth=2, just=1) { >     width = (max_length(text) + 2)  * $charWidth; >     height = len(text) * $lineHeight; >     union() { The explicit union isn't necessary.  (It's actually somewhat rare that explicit unions are necessary.  In a 12 thousand line project, I only have 39 of them.  Most of those are for modules that treat different children differently.) >         translate([0, height/2, 0]) { >             cube([width, height, depth], true); I would explicitly say "center=true", rather than leaving the non-self-explanatory "true" by itself. However, since you really only need centering in X, and you have to explicitly back it out in Y and take it into account in Z, I would just do your own centering in X: translate([-width/2, 0, 0]) cube([width, height, depth]); (Actually, I would use a library module that I wrote that creates a cube centered or justified in any of the three axes, where I can say "center in X, positive in Y and Z".)