Hi,

Guest

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:

- 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. - 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:

- 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".)

Replying to:

Empathy v1.0
2022 ©Harmonylists.com