Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Snipping the ears

114 views
Skip to first unread message

luser droog

unread,
Jul 20, 2019, 4:18:58 PM7/20/19
to
After 27 posts in the other thread, I thought it'd be nice to
have a fresher start on the remaining issues with my code.

The task is to take the glyph of the number 9 using 'charpath',
then traverse the curves like a turtlw with a paintbrush hanging
out one arm's-length away.

The tricky part would be dealing with the actual Bezier curves,
so I call 'flattenpath' first so I can work with just line
segments. The 'setflat' operator can be used to control how
finely the curves are cut into line segments.

(Or maybe, the curves true secret path to the desired result??)

Anyway, to traverse the line segments I use a control structure
I cooked up, called 'fortuplem'. It works kind of like 'forall',
but it takes 2 parameters n and m. n is the step-value between
iterations, and m is the width of a slice of the array produce
using 'getinterval'. Calling with n=2 m=2 iterates over the
points in the line. Calling with n=2 m=4 iterates over 2 point
slices. So I can run through these 2 points slices, get a vector
between them, normalize, take the perpendicular vector, scale
by my "arm's length" and produce a new transformed point.
This just needs one special case for the wrap around the end
from the last point to the first.


So, this all *works* with the untidy caveat that it produces
"ears" when the input path has a tight bend in the curve.

Detecting and pruning these ears would make the output nicer
and more usable. A further nicety would be implementing miter
or bevel joins for the other side of the tight bends.


Any, here's the current state of the code, including the laborious
fixes for the 2 "hacks" described at the end of the last thread.


$ cat stroke2.ps
%!
% Generate offset curves by traversing input path.

/args-begin { dup length dict begin { exch def } forall } def
/map { [ 3 1 roll forall ] } def
/spill { aload pop } def
/head { 0 exch getinterval } def
/tail { 1 index length 1 index sub getinterval } def
/last { 1 index length 1 index sub exch getinterval } def
/droplast { 1 index length exch sub 0 exch getinterval } def
/fortuplem {
{p m n a} args-begin
0 n /a load length m sub
[ /a load /exch cvx m /getinterval cvx /p load /exec cvx ] cvx
end for
} def

/p-p { exch 3 1 roll sub 3 1 roll sub exch } def
/p+v { exch 3 1 roll add 3 1 roll add exch } def
/v*n { 3 2 roll 1 index mul 3 1 roll mul } def
/perp { neg exch } def
/norm {
2 copy dup mul exch dup mul exch add sqrt
1 exch div v*n
} def

/closedsubpaths { [ {[ 3 1 roll} {} {} {]} pathforall ] {{]}stopped{exit}if}loop } def

/drawpath {
{
dup length 2 eq { spill moveto }{
dup 2 head spill moveto
2 tail 2 2 { spill lineto } fortuplem
closepath
} ifelse
} forall
} def

/offsetbyvectors {
/n exch def
{
dup length 2 ne {
suppresszerovectors
offsetsubpath
suppresszerovectors
} if
} map
} def

/suppresszerovectors {
%2 droplast
dup vectors zeros %dup ==
suppresspairs
} def

/vectors {
[ exch
dup 2 4 { spill p-p } fortuplem
counttomark -1 roll
dup 2 last spill 3 2 roll 2 head spill p-p
]
} def

/threshold 0.7 def
/iszero {
%0 eq
abs threshold lt
} def
/zeros {
[ exch
2 2 { spill iszero exch iszero and } fortuplem
]
} def

/suppresspairs { % pairs zeros
false { % p z restart
0 1 3 index length 1 sub { % p z r i
2 index 1 index get % p z r i z_i
{ % p z r i |true
snippair % p' z' r'
exit
}{ % p z r i |false
pop % p z r
} ifelse
} for
not { exit } if false
} loop % p' z'
pop % pairs'
} def

/snippair { % p z r i
true 5 1 roll exch pop % r' p z i
2 copy snipsing 5 1 roll % z' r' p z i
exch pop snippair_ % z' r' p'
3 1 roll % p' z' r'
} def

/snipsing { % array i
2 copy head 3 1 roll
1 add tail cat
} def

/cat { 2 array astore {spill} map } def

/snippair_ { % array i
2 mul
2 copy head 3 1 roll
2 add tail cat
} def

/offsetsubpath {
[ exch
dup 2 4 { offsetvec } fortuplem
counttomark -1 roll
dup 2 last spill 3 2 roll 2 head spill 4 array astore offsetvec
]
} def

/offsetvec {
dup 0 2 getinterval spill 3 2 roll
spill %4 2 roll
p-p norm n v*n
perp p+v
} def

/offsetpath {
closedsubpaths exch
offsetbyvectors
newpath
drawpath
} def

/Calibri-Bold 600 selectfont
100 100 moveto (9) true charpath
gsave stroke grestore
gsave
20 setlinewidth strokepath
1 setlinewidth
%stroke
grestore

1 0 0 setrgbcolor
flattenpath
gsave
10 offsetpath
stroke
grestore
-10 offsetpath
stroke

showpage quit

luser droog

unread,
Jul 20, 2019, 7:51:02 PM7/20/19
to
I've said this before, but I think I can detect the ears now.
I take the vector out of each point, take the angle, take the
diff between adjacent angles, then take the signs of that angular
change.

If I've reasoned correctly, this gives me a chart of left turns vs.
right turns for each point. Running it on the left hand offset curve
(-n offsetpath) gives me this:

[-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1 1 1 1 1 1 1 1 1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 \
1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1]
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1]

And I see a lonely "1* -1 1*" in the first curve, and a "-1* 1 -1*" in the second,
which makes sense considering that the two curves are deliberately described
in differing orientations to take advantage of the even/odd rule.

Running it one the right hand curve (+n offsetpath) gives me this:

[-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 1 1 1 -1 1 1 1 1 1 1 1 1 1 1 -1 1 -1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 \
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1]
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1]

Which shows no ears on the inner curve, but several spots of weirdness in
the outer curve. There's an interesting "-1* 1 1 1 -1*" which looks like the
bunched up points on the foot the '9'.

And two little wiggles "1* -1 1 -1 1 -1*" and "1* -1 1 -1*" which I don't
know what those are.

So, now to try actually snipping. I'll tackly the simple ones first I think.
"-1* 1 -1*" and "1* -1 1*" patterns. I think I need the incoming and outgoing vectors and then take their intersection as they approach their
points.

I'm not sure what to do with the fat ear and the ripples.

luser droog

unread,
Jul 22, 2019, 3:47:45 AM7/22/19
to
[snip]>
> And I see a lonely "1* -1 1*" in the first curve, and a "-1* 1 -1*" in the second,
> which makes sense considering that the two curves are deliberately described
> in differing orientations to take advantage of the even/odd rule.
>
> Running it one the right hand curve (+n offsetpath) gives me this:
>
[snip]>
> Which shows no ears on the inner curve, but several spots of weirdness in
> the outer curve. There's an interesting "-1* 1 1 1 -1*" which looks like the
> bunched up points on the foot the '9'.
>
> And two little wiggles "1* -1 1 -1 1 -1*" and "1* -1 1 -1*" which I don't
> know what those are.
>
> So, now to try actually snipping. I'll tackly the simple ones first I think.
> "-1* 1 -1*" and "1* -1 1*" patterns. I think I need the incoming and outgoing vectors and then take their intersection as they approach their
> points.
>
> I'm not sure what to do with the fat ear and the ripples.

Sigh. It's turning into the same rats' nest as before. So, let's start over.
This fresh program does not do any offsetting yet. It also preserves curves.
Just dumping the numbers for now to get a sense of what's going on in there.
A few 'linetos' here and there but lots of 'curvetos' (the 6 element subarrays).

So, the question changes. Can we detect sharp turns in the curveto operands?
And then do something sensible to produce an offset for that portion?
Wikipedia has some info: https://en.wikipedia.org/wiki/Parallel_curve

$ cat stroke3.ps
%!


/args-begin { dup length dict begin { exch def } forall } def
/map { [ 3 1 roll forall ] } def
/spill { aload pop } def
/head { 0 exch getinterval } def
/tail { 1 index length 1 index sub getinterval } def
/last { 1 index length 1 index sub exch getinterval } def
/droplast { 1 index length exch sub 0 exch getinterval } def
/fortuplem {
{p m n a} args-begin
0 n /a load length m sub
[ /a load /exch cvx m /getinterval cvx /p load /exec cvx ] cvx
end for
} def

/p-p { exch 3 1 roll sub 3 1 roll sub exch } def
/p+v { exch 3 1 roll add 3 1 roll add exch } def
/v*n { 3 2 roll 1 index mul 3 1 roll mul } def
/perp { neg exch } def
/mag { dup mul exch dup mul exch add sqrt } def
/norm { 2 copy mag 1 exch div v*n } def
/ang { exch atan } def
/aa { array astore } def

/closedsubpaths { [ {[ 3 1 roll 2 aa} {2 aa} {6 aa} {]} pathforall ] {{]}stopped{exit}if}loop } def

/drawpath {
{
dup length 1 eq { spill spill moveto }{
dup 1 head spill spill moveto
1 tail { dup length 2 eq { spill lineto }{ spill curveto } ifelse } forall
closepath
} ifelse
} forall
} def

/offsetbyvectors {
/n exch def
{
dup length 2 ne {
offsetsubpath
} if
} map
} def


/offsetpath {
closedsubpaths exch
pop dup ==%offsetbyvectors
newpath
drawpath
} def

/Calibri-Bold 600 selectfont
100 100 moveto (9) true charpath
gsave stroke grestore
gsave
20 setlinewidth strokepath
1 setlinewidth
%stroke
grestore

1 0 0 setrgbcolor
gsave
10 offsetpath
stroke
grestore
-10 offsetpath
stroke

showpage quit


Output:
$ gsnd -q stroke3.ps
[[[380.375 306.421875] [380.25 290.875 379.59375 275.078125 378.03125 258.609375] [376.457031 241.9375 373.492188 226.007813 369.109375 210.21875] [364.695313 194.351563 358.691406 179.414063 350.984375 165.1875] [342.957031 150.90625 333.347656 138.546875 321.171875 127.921875] [308.9375 116.988281 294.179688 108.863281 276.734375 102.625] [259.039063 96.1875 238.675781 93.28125 214.90625 93.28125] [206.21875 93.1875 198.1875 93.8671875 189.90625 95.03125] [181.320313 96.1875 174.066406 97.6640625 167.234375 99.421875] [160.332031 101.03125 154.613281 103.019531 149.84375 104.96875] [144.789063 106.59375 141.613281 108.910156 139.46875 110.953125] [137.164063 112.6875 135.765625 115.976563 134.796875 119.875] [133.5625 123.488281 133.34375 129.238281 133.34375 136.25] [133.226563 142.601563 133.484375 147.851563 133.765625 151.75] [133.800781 155.394531 134.640625 158.660156 135.515625 160.796875] [136.226563 162.675781 137.472656 164.359375 138.734375 165.046875] [139.757813 165.71875 141.515625 166.078125 143.265625 166.078125] [145.519531 166.050781 148.921875 165.394531 153.203125 164.03125] [157.425781 162.65625 162.707031 161.101563 168.84375 159.34375] [174.707031 157.476563 181.957031 156.035156 189.75 154.671875] [197.289063 153.082031 206.21875 152.625 215.78125 152.625] [231.882813 152.414063 245.976563 155.546875 257.28125 161.390625] [268.5 167.113281 277.753906 175.140625 284.765625 185.078125] [291.53125 195.0 296.953125 206.523438 300.265625 219.578125] [303.5 232.4375 305.441406 246.375 305.828125 260.8125] [296.582031 254.882813 286.035156 250.226563 273.359375 246.03125] [260.570313 241.832031 246.175781 239.75 229.8125 239.75] [209.46875 239.5 192.679688 242.378906 178.640625 247.640625] [164.394531 252.6875 153.207031 260.5 144.4375 270.4375] [135.519531 280.238281 129.28125 292.472656 125.28125 306.703125] [121.195313 320.695313 119.296875 337.007813 119.296875 354.9375] [119.195313 373.53125 122.117188 391.238281 127.765625 407.125] [133.164063 422.96875 141.804688 436.75 152.921875 448.34375] [163.726563 459.820313 177.875 468.953125 194.4375 475.390625] [210.851563 481.695313 230.195313 485.046875 252.03125 485.046875] [269.5625 485.03125 284.921875 482.992188 298.078125 478.890625] [311.207031 474.5 322.535156 468.945313 331.984375 461.34375] [341.113281 453.632813 349.335938 444.546875 355.671875 433.734375] [361.78125 422.675781 366.972656 410.882813 370.578125 397.625] [373.988281 384.164063 376.710938 370.046875 378.171875 354.640625] [379.582031 339.03125 380.375 323.171875 380.375 306.421875]] [[304.078125 316.9375] [304.050781 337.476563 302.90625 355.234375 300.5625 369.265625] [298.039063 383.207031 294.722656 394.5 290.046875 402.875] [285.351563 410.96875 279.519531 417.207031 272.5 420.71875] [265.3125 423.988281 257.289063 425.984375 247.9375 425.984375] [238.457031 425.65625 230.875 424.375 224.25 421.15625] [217.539063 417.894531 212.117188 413.414063 207.734375 407.5625] [203.113281 401.53125 200.09375 394.796875 197.9375 386.796875] [195.476563 378.539063 194.71875 370.035156 194.71875 360.484375] [194.476563 350.0 195.644531 341.050781 197.5 333.15625] [199.1875 325.21875 202.273438 318.742188 206.265625 313.578125] [210.175781 308.21875 215.476563 304.5625 221.90625 302.03125] [228.039063 299.488281 236.144531 298.234375 245.3125 298.234375] [256.5 298.15625 267.269531 299.9375 277.3125 303.34375] [287.25 306.632813 296.273438 311.28125 304.078125 316.9375]] [[404.101563 100.0]]]
[[[380.375 306.421875] [380.25 290.875 379.59375 275.078125 378.03125 258.609375] [376.457031 241.9375 373.492188 226.007813 369.109375 210.21875] [364.695313 194.351563 358.691406 179.414063 350.984375 165.1875] [342.957031 150.90625 333.347656 138.546875 321.171875 127.921875] [308.9375 116.988281 294.179688 108.863281 276.734375 102.625] [259.039063 96.1875 238.675781 93.28125 214.90625 93.28125] [206.21875 93.1875 198.1875 93.8671875 189.90625 95.03125] [181.320313 96.1875 174.066406 97.6640625 167.234375 99.421875] [160.332031 101.03125 154.613281 103.019531 149.84375 104.96875] [144.789063 106.59375 141.613281 108.910156 139.46875 110.953125] [137.164063 112.6875 135.765625 115.976563 134.796875 119.875] [133.5625 123.488281 133.34375 129.238281 133.34375 136.25] [133.226563 142.601563 133.484375 147.851563 133.765625 151.75] [133.800781 155.394531 134.640625 158.660156 135.515625 160.796875] [136.226563 162.675781 137.472656 164.359375 138.734375 165.046875] [139.757813 165.71875 141.515625 166.078125 143.265625 166.078125] [145.519531 166.050781 148.921875 165.394531 153.203125 164.03125] [157.425781 162.65625 162.707031 161.101563 168.84375 159.34375] [174.707031 157.476563 181.957031 156.035156 189.75 154.671875] [197.289063 153.082031 206.21875 152.625 215.78125 152.625] [231.882813 152.414063 245.976563 155.546875 257.28125 161.390625] [268.5 167.113281 277.753906 175.140625 284.765625 185.078125] [291.53125 195.0 296.953125 206.523438 300.265625 219.578125] [303.5 232.4375 305.441406 246.375 305.828125 260.8125] [296.582031 254.882813 286.035156 250.226563 273.359375 246.03125] [260.570313 241.832031 246.175781 239.75 229.8125 239.75] [209.46875 239.5 192.679688 242.378906 178.640625 247.640625] [164.394531 252.6875 153.207031 260.5 144.4375 270.4375] [135.519531 280.238281 129.28125 292.472656 125.28125 306.703125] [121.195313 320.695313 119.296875 337.007813 119.296875 354.9375] [119.195313 373.53125 122.117188 391.238281 127.765625 407.125] [133.164063 422.96875 141.804688 436.75 152.921875 448.34375] [163.726563 459.820313 177.875 468.953125 194.4375 475.390625] [210.851563 481.695313 230.195313 485.046875 252.03125 485.046875] [269.5625 485.03125 284.921875 482.992188 298.078125 478.890625] [311.207031 474.5 322.535156 468.945313 331.984375 461.34375] [341.113281 453.632813 349.335938 444.546875 355.671875 433.734375] [361.78125 422.675781 366.972656 410.882813 370.578125 397.625] [373.988281 384.164063 376.710938 370.046875 378.171875 354.640625] [379.582031 339.03125 380.375 323.171875 380.375 306.421875]] [[304.078125 316.9375] [304.050781 337.476563 302.90625 355.234375 300.5625 369.265625] [298.039063 383.207031 294.722656 394.5 290.046875 402.875] [285.351563 410.96875 279.519531 417.207031 272.5 420.71875] [265.3125 423.988281 257.289063 425.984375 247.9375 425.984375] [238.457031 425.65625 230.875 424.375 224.25 421.15625] [217.539063 417.894531 212.117188 413.414063 207.734375 407.5625] [203.113281 401.53125 200.09375 394.796875 197.9375 386.796875] [195.476563 378.539063 194.71875 370.035156 194.71875 360.484375] [194.476563 350.0 195.644531 341.050781 197.5 333.15625] [199.1875 325.21875 202.273438 318.742188 206.265625 313.578125] [210.175781 308.21875 215.476563 304.5625 221.90625 302.03125] [228.039063 299.488281 236.144531 298.234375 245.3125 298.234375] [256.5 298.15625 267.269531 299.9375 277.3125 303.34375] [287.25 306.632813 296.273438 311.28125 304.078125 316.9375]] [[404.101563 100.0]]]

luser droog

unread,
Jul 24, 2019, 3:38:56 AM7/24/19
to
I tried implementing some stuff and the result was ugly. So at long last,
I busted out the pen and paper and began to make some real progress.
I think I have a good idea of what I want it to do, and the challenge
now is just implementing it.

When running through the path, transforming one 'lineto' or 'curveto' at
a time, we need a little surrounding context to get all the interesting
vectors that contribute to the result.

So, my idea is to run through the path elements with a sliding window
of three elements, one on each side.

For a 'x u lineto' element, we only need 2 vectors,
v_in the vector coming in (from currentpoint to x y)
v_out the vector going out (from x y to next element)

x' y' = x y + scale(n. perp(norm(v_in + v_out)))

For a 'xy1 xy2 xy3 curveto' element, there are 4 important vectors
v_enter the vector approaching (from earlier upto currentpoint)
v_in the first hermite vector (from currentpoint to xy1)
v_exit the second hermite vector (from xy2 to xy3)
v_out the vector going out (from xy3 to next element)

Then xy1 should be modified by the perp of the avg of v_enter and v_in.
xy2 and xy3 should be modified by the perp of the avg of v_exit and v_out.

I don't have this all working yet, but it seems like it ought to work.
Another idea is to do one iteration of DeCasteljau's algorithm to aplit
the curve and find the center point and its in/out vectors. Then (somehow)
figure out how to move the original control points s.t. the center point
moves appropriately.

Mark Carroll

unread,
Jul 24, 2019, 3:50:05 AM7/24/19
to
On 24 Jul 2019, luser droog wrote:

> When running through the path, transforming one 'lineto' or 'curveto' at
> a time, we need a little surrounding context to get all the interesting
> vectors that contribute to the result.

I'm afraid that with this kind of thing I always did flattenpath then
just had lineto's to deal with.

-- Mark

luser droog

unread,
Jul 30, 2019, 2:18:40 AM7/30/19
to
I tried that first of course, but it leads to these ugly artifacts at the
discontinuities. I tried flattening and then using the outgoing vectors
from each point. Then I tried the incoming vectors. Then I tried taking
clusters of 3, 4 or 5 consecutive points and averaging the interstitial
vectors. All of these led to ears.

But there may have been errors in my implementation which concealed
positive (or promising) results. Thinking over it, using 3 consecutive
points should have been pretty close if it were done correctly.

But I have a new idea which really seems like it should work.
For lineto segments average the incoming and outgoing vectors as
described earlier. But for curveto segments, if I do a de Casteljau
subdivision just once, then I get a middle point and two tangent
vectors at that point for cheap. If it doubles the number of curves,
that's not too terrible I think.


luser droog

unread,
Aug 7, 2019, 1:05:51 AM8/7/19
to
At long last I've implemented all of these ideas without errors this time.
And it still makes ears. But I think this is good progress.

I found some code in Don Lancaster's pages to measure the length of a curve.
I think I can use that to determine if my new offset curve is bigger or
smaller than the original. And that should tell me whether we're inside or
outside of a bend.

But a numerical/conceptual problem keeps cropping up. Whenever I try to
run through a series of line segments to get the angular change, I don't
know how to bound the output to be 0-360. My modular arithmetic skills
keep crapping out. Maybe pen and paper are the tool for that too.
This problem of getting a usable figure for angular change is exactly
what I need to solve to find out how "tight" the bend is in the curve.

If that all works, then I ought to be able to control ears by snipping
curves that are all clustered up in a corner.

luser droog

unread,
Aug 8, 2019, 3:22:52 AM8/8/19
to
Here's some progress. The image is an inner curve 7 points in. All the
control points on the original curve are drawn with dots. And it produces
some text output which shows

[original line or curve point(s)]
length
angle
(angle out)
(angle out - angle in)
[new (inner) line or curve]
length
angle
(angle out)
(angle change)

I tweaked the formula for angular change, and it seems ok now.
Now, I'm just staring at the numbers to see if I can find that
nasty corner, and hopefully other similar corners more generally.
Then, to figure out how to snip it nicely...


Text Output:
$ gs stroke7.ps
GPL Ghostscript 9.27 (2019-04-04)
Copyright (C) 2018 Artifex Software, Inc. All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
Querying operating system for font files...
Can't find (or can't open) font file /usr/share/ghostscript/9.27/Resource/Font/Calibri-Bold.
Can't find (or can't open) font file Calibri-Bold.
Loading Calibri-Bold font from /usr/share/fonts/microsoft/calibrib.ttf... 4678648 3034560 9299916 7891942 1 done.
[374.530243 302.102081]
0.0
270.0
[366.530243 302.109528]
0.0
270.0

[374.500824 286.917053 373.765564 271.421753 372.236176 255.297028]
46.8781166
269.889
264.581848
-5.30715942
[366.500854 286.932556 365.801514 272.179199 364.272125 256.054474]
46.1354637
269.889
264.581848
-5.30715942

[370.692078 239.106125 367.800934 223.379929 363.506897 207.921387]
48.2356453
264.552277
254.475891
-10.0763855
[362.728027 239.863571 360.09903 225.543411 355.805 210.084869]
46.80653
264.552277
254.475891
-10.0763855

[359.092285 192.377533 353.295319 177.766022 345.754272 163.833908]
47.6239052
254.144913
241.574615
-12.5702972
[351.390381 194.541016 346.266205 181.585892 338.725159 167.653778]
45.8383713
254.144913
241.574615
-12.5702972

[338.11618 149.834152 328.483978 137.737305 316.554779 127.334549]
46.9916382
241.383667
221.089783
-20.2938843
[331.087067 153.654022 323.218842 143.760437 311.289642 133.357681]
44.1787376
241.383667
221.089783
-20.2938843

[304.599121 116.858261 290.125824 108.681931 273.043762 102.576157]
50.3612518
221.226852
199.668808
-21.5580444
[299.333984 122.881401 287.41806 116.209747 270.336 110.103973]
47.4055
221.226852
199.668808
-21.5580444

[255.882309 96.3645 235.785599 93.4174881 212.503647 93.4174881]
61.5353737
199.897919
180.0
-19.8979187
[253.174545 103.892319 235.769211 101.417473 212.487259 101.417473]
58.8120461
199.897919
180.0
-19.8979187

[204.193497 93.3527832 196.140701 93.9910126 188.027603 95.1351089]
24.5589523
180.446106
171.973175
-8.47293091
[204.177109 101.352768 197.246796 101.914177 189.133698 103.058273]
23.4344864
180.446106
171.973175
-8.47293091

[179.813049 96.2703857 172.522 97.7115326 165.839752 99.4291534]
22.6118946
172.131393
165.584641
-6.54675293
[180.919144 104.19355 174.465073 105.471977 167.782822 107.189598]
21.7595253
172.131393
165.584641
-6.54675293

[159.048706 101.085007 153.48262 102.958504 148.804764 104.864349]
17.8974018
166.297012
157.833099
-8.46391296
[160.991776 108.845451 156.403641 110.406166 151.725784 112.312012]
16.8712521
166.297012
157.833099
-8.46391296

[144.048965 106.65844 140.741669 108.730461 138.640228 110.734833]
11.8126049
159.331375
136.354324
-22.9770508
[146.969986 114.106102 146.16127 114.615013 144.05983 116.619385]
8.88110638
159.331375
136.354324
-22.9770508

[136.460861 112.673035 135.016769 115.647987 134.063843 119.464096]
10.0036058
138.351974
104.020691
-34.3312836
[141.880463 118.557587 142.757706 117.667374 141.804779 121.483482]
5.57857656
138.351974
104.020691
-34.3312836

[133.049149 123.190498 132.63446 128.622757 132.63446 135.493225]
16.1340561
105.232254
90.0
-15.232254
[140.790085 125.209885 140.634293 128.675613 140.634293 135.546082]
14.155632
105.232254
90.0
-15.232254

[132.547699 141.754883 132.7771 146.847443 133.063858 150.663559]
15.1813326
90.7938385
85.7026367
-5.09120178
[140.547531 141.807739 140.761749 146.352173 141.048508 150.168289]
14.6331158
90.7938385
85.7026367
-5.09120178

[133.246216 154.410553 133.922668 157.441376 134.781479 159.539871]
9.08163
87.2137451
67.7431412
-19.4706039
[141.230865 153.915283 141.353363 154.477463 142.212173 156.575958]
6.56146193
87.2137451
67.7431412
-19.4706039

[135.590286 161.622192 136.688797 163.025101 137.928482 163.692734]
5.31567192
68.7730179
28.3047085
-40.4683075
[143.020981 158.658279 140.542328 156.014374 141.782013 156.682]
2.48282027
68.7730179
28.3047085
-40.4683075

[139.072571 164.335373 140.646088 164.692719 142.363693 164.692719]
4.59395552
29.3230934
0.0
-29.3230934
[142.926102 157.324646 140.431396 156.695602 142.149 156.695602]
1.69659424
29.3230934
0.0
-29.3230934

[144.590134 164.586838 147.90184 164.023621 152.098816 162.686874]
9.96525478
357.277283
342.333191
-14.9440918
[144.375443 156.589722 145.410614 156.421402 149.60759 155.084656]
7.65997934
357.277283
342.333191
-14.9440918

[156.248749 161.288361 161.401596 159.828094 167.410324 158.110489]
15.9821692
341.376343
344.047333
2.67099
[153.757523 153.686142 159.174896 152.144226 165.183624 150.42662]
16.2589684
341.376343
344.047333
2.67099

[173.413162 156.347275 180.245392 154.864944 187.880554 153.528214]
20.9876537
343.63089
350.06955
6.43865967
[171.186462 148.663406 178.85051 146.987488 186.485672 145.650757]
21.8413944
343.63089
350.06955
6.43865967

[195.459839 152.170883 204.009689 151.528244 213.362457 151.528244]
25.5931606
349.846863
0.0
10.1531372
[194.064957 144.293427 203.974915 143.52832 213.327682 143.52832]
26.9577065
349.846863
0.0
10.1531372

[229.376892 151.417953 242.94136 154.389954 254.00882 160.116333]
41.9528
359.605408
27.3573551
27.7519531
[229.342117 143.41803 246.596436 147.273743 257.663879 153.000122]
45.7254677
359.605408
27.3573551
27.7519531

[265.067444 165.754471 274.049652 173.570511 280.920105 183.304153]
35.8796844
27.0143433
54.7838
27.769455
[268.722504 158.63826 280.593048 168.967911 287.463501 178.701553]
39.6876183
27.0143433
54.7838
27.769455

[287.717041 193.001053 292.846375 204.294968 296.090424 217.080048]
37.2313118
54.9718819
75.762413
20.7905312
[294.260437 188.398453 300.608398 202.358261 303.852448 215.143341]
40.1534
54.9718819
75.762413
20.7905312

[299.227142 229.868057 301.150635 243.322235 301.531525 257.444061]
40.8069153
76.2182465
88.4550095
12.236763
[306.989166 227.931351 306.847 248.939316 307.227875 263.061157]
48.1180305
76.2182465
88.4550095
12.236763

[292.709595 251.882385 282.158325 247.092758 269.755585 242.991364]
34.993576
212.228867
198.298248
-13.9306183
[298.405945 257.499481 279.628632 254.682266 267.225891 250.580872]
42.0174217
212.228867
198.298248
-13.9306183

[257.235229 238.785553 243.134 236.838516 227.103394 236.838516]
43.2831955
198.568115
180.0
-18.5681152
[254.705536 246.375061 243.120193 244.838501 227.089584 244.838501]
40.741478
198.568115
180.0
-18.5681152

[207.369919 236.776764 190.748154 239.412 177.004272 244.56192]
50.9645348
180.179291
159.45871
-20.7205811
[207.35611 244.776749 193.55 246.905304 179.806122 252.055237]
48.1140137
180.179291
159.45871
-20.7205811

[163.194214 249.714767 152.098816 257.157288 143.510742 266.89093]
40.669857
159.538315
131.422226
-28.1160889
[165.996063 257.208069 158.081436 262.468445 149.493362 272.202087]
36.8386078
159.538315
131.422226
-28.1160889

[134.849121 276.58783 128.671295 288.461151 124.758125 302.39032]
40.5021286
131.772385
105.691833
-26.0805511
[140.831741 281.899 136.364914 290.653931 132.451752 304.583099]
36.965847
131.772385
105.691833
-26.0805511

[120.752312 316.245972 118.893517 332.067749 118.893517 349.624786]
47.7526741
106.125137
90.0
-16.1251373
[128.445938 318.438751 126.893517 332.072968 126.893517 349.63]
45.5485268
106.125137
90.0
-16.1251373

[118.869987 368.124481 121.659645 385.165344 127.193375 400.718018]
52.0193253
90.072876
70.4141541
-19.6587219
[126.869987 368.1297 129.196487 382.482819 134.730225 398.035492]
49.3005562
90.072876
70.4141541
-19.6587219

[132.696213 416.173645 140.931366 429.730743 151.810593 441.087921]
47.6400299
70.4021835
46.2313309
-24.1708527
[140.233063 413.491119 146.718033 424.206757 157.59726 435.563934]
44.3188629
70.4021835
46.2313309
-24.1708527

[162.610397 452.440643 176.236633 461.269897 192.45694 467.563904]
48.9071388
46.4297829
21.2078724
-25.2219105
[168.397064 446.916656 179.128 453.810669 195.348312 460.104675]
45.4430084
46.4297829
21.2078724
-25.2219105

[208.605194 473.816742 227.481339 477.010803 248.861847 477.010803]
57.5152283
21.1671257
0.0
-21.1671257
[211.496567 466.357513 227.469421 469.010803 248.84993 469.010803]
54.5728455
21.1671257
0.0
-21.1671257

[265.980652 476.953461 281.061279 475.006439 293.94342 470.999146]
45.6468887
359.808075
342.720459
-17.087616
[265.96875 468.953461 278.666473 467.373291 291.548615 463.366]
43.241848
359.808075
342.720459
-17.087616

[306.803467 466.930115 317.893 461.265503 327.148712 453.822968]
37.6011848
342.442169
321.197144
-21.2450256
[304.408661 459.296967 312.850464 455.05481 322.106171 447.612274]
34.6070518
342.442169
321.197144
-21.2450256

[336.360321 446.271637 344.136658 437.361511 350.336548 426.770508]
35.8204536
320.656403
300.34436
-20.3120422
[331.31778 440.060944 337.216156 433.348175 343.416046 422.757172]
32.9398384
320.656403
300.34436
-20.3120422

[356.418793 416.182465 361.406952 404.397369 364.936279 391.418182]
38.3531647
299.874969
285.212158
-14.6628113
[349.498291 412.169128 353.680267 402.324127 357.209595 389.34494]
36.2558594
299.874969
285.212158
-14.6628113

[368.374481 378.430176 370.950897 364.414246 372.383209 349.336578]
42.783493
284.827301
275.426575
-9.40072632
[360.647797 376.356934 362.985 363.676361 364.417297 348.598694]
41.4277954
284.827301
275.426575
-9.40072632

[373.747894 334.219177 374.530243 318.51358 374.530243 302.102081]
47.2993202
275.158264
270.0
-5.15826416
[365.781982 333.481293 366.530243 318.51358 366.530243 302.102081]
46.5608063
275.158264
270.0
-5.15826416

[299.813904 312.407806]
0.0
35.9335518
[307.49585 310.1745]
0.0
35.9335518

[299.716827 332.80304 298.66687 349.910095 296.372772 363.648071]
51.4078865
90.2727127
99.4803162
9.20760345
[307.716736 332.841125 306.553772 351.250519 304.259674 364.988495]
52.7141571
90.2727127
99.4803162
9.20760345

[294.003693 377.347839 290.646423 388.362366 286.067078 396.571045]
34.6546516
99.8110428
119.155678
19.344635
[301.890594 378.688263 297.617279 392.287567 293.037933 400.496246]
37.3885384
99.8110428
119.155678
19.344635

[281.458344 404.679718 275.767242 410.600189 268.89679 414.035431]
24.8555794
119.61264
153.434845
33.8222046
[288.429199 408.604919 279.319 417.768524 272.448547 421.203766]
29.5303707
119.61264
153.434845
33.8222046

[261.945435 417.448608 254.010284 419.188293 244.850143 419.188293]
24.8113022
153.848541
180.0
26.1514587
[265.497192 424.616943 253.962524 427.188141 244.802383 427.188141]
28.4879551
153.848541
180.0
26.1514587

[235.781189 419.079468 228.147491 417.613312 221.662323 414.464844]
23.8575554
180.6875
205.896
25.2084961
[235.733429 427.079315 224.64093 424.803864 218.155762 421.655396]
27.3933258
180.6875
205.896
25.2084961

[215.15361 411.276672 209.786057 406.878204 205.492 401.15332]
21.1453419
206.097061
233.127609
27.0305481
[211.647049 418.467224 203.369659 411.65625 199.075607 405.931366]
24.9063759
206.097061
233.127609
27.0305481

[201.170029 395.307831 197.999496 388.650574 195.898056 380.824249]
22.6140194
233.521912
254.970062
21.4481506
[194.753632 400.085876 190.280334 390.751678 188.178894 382.925354]
25.5813694
233.521912
254.970062
21.4481506

[193.706924 372.88028 192.751053 364.412781 192.751053 355.06]
26.0351067
254.57991
270.0
15.4200897
[185.987762 374.981384 184.751083 364.432068 184.751083 355.079285]
28.1320286
254.57991
270.0
15.4200897

[192.704 344.880768 193.656921 336.025024 195.468658 328.295746]
26.9633083
269.735138
283.191925
13.4567871
[184.704025 344.900055 185.858459 334.240692 187.670197 326.511414]
28.7757702
269.735138
283.191925
13.4567871

[197.205383 320.516479 200.142105 314.178345 204.056747 309.119629]
21.1823483
282.58493
307.734192
25.1492615
[189.406921 318.732147 193.784897 309.321808 197.699539 304.263092]
24.5610504
282.58493
307.734192
25.1492615

[207.918442 303.999115 213.074234 300.290344 219.36824 297.808044]
19.2845936
307.022186
338.476166
31.4539795
[201.561234 299.142578 210.109406 292.860016 216.403412 290.377716]
23.5147915
307.022186
338.476166
31.4539795

[225.656372 295.269867 233.300354 294.084595 242.273727 294.084595]
23.3466854
338.018738
0.0
21.9812622
[222.691544 287.839539 233.272598 286.084656 242.245972 286.084656]
26.3277283
338.018738
0.0
21.9812622

[253.223526 294.015472 263.786591 295.755157 273.614349 299.096252]
31.8869267
359.638306
18.7762413
19.1379395
[253.19577 286.015533 266.326111 288.168945 276.15387 291.51004]
34.4805
359.638306
18.7762413
19.1379395

[283.423 302.328552 292.178741 306.874054 299.813904 312.407806]
29.5047092
18.2388859
35.9335518
17.6946659
[285.962524 294.74234 296.873505 300.396484 304.508667 305.930237]
31.9229279
18.2388859
35.9335518
17.6946659

>>showpage, press <return> to continue<<


Code:

$ cat stroke7.ps
%!

%errordict/rangecheck{pstack countexecstack array execstack == quit}put
%errordict/typecheck{pstack countexecstack array execstack == quit}put
%errordict/rangecheck{pstack countexecstack array execstack == quit}put

/args-begin { dup length dict begin { exch def } forall } def
/map { [ 3 1 roll forall ] } def
/spill { aload pop } def
/head { 0 exch getinterval } def
/tail { 1 index length 1 index sub getinterval } def
/last { 1 index length 1 index sub exch getinterval } def
/droplast { 1 index length exch sub 0 exch getinterval } def
/fortuplem { {p m n a} args-begin
0 n /a load length m sub
[ /a load /exch cvx m /getinterval cvx /p load /exec cvx ] cvx
end for } def
/p-p { exch 3 1 roll sub 3 1 roll sub exch } def
/p+v { exch 3 1 roll add 3 1 roll add exch } def
/v*n { 3 2 roll 1 index mul 3 1 roll mul } def
/perp { exch neg } def
/mag { dup mul exch dup mul exch add sqrt } def
/norm { 2 copy mag dup 0 eq { }{ 1 exch div } ifelse v*n } def
/ang { exch atan } def
/aa { array astore } def
/cat { 2 aa {spill} map } def
/median { p+v .5 v*n } def
/closedsubpaths { [ {[ 3 1 roll 2 aa} {2 aa} {6 aa} {]} pathforall ] {{]}stopped{exit}if}loop } def
/drawpath {
{
dup length 1 eq { spill spill moveto }{
dup 1 head spill spill moveto
1 tail { dup length 2 eq { spill lineto }{ spill curveto } ifelse } forall
closepath
} ifelse
} forall
} def


/offsetbyvectors {
{ dup length 1 ne {offsetsubpath} if } map
} def

/pq { pstack / = quit } def

/offsetsubpath {
[ exch
dup dup 2 last exch 2 head cat offsetelement
1 index dup 1 last exch 3 head cat offsetelement
2 index 1 4 { offsetelement } fortuplem
counttomark -1 roll
dup 3 last exch 1 head cat offsetelement
]
} def

/offsetelement {
dup 2 get length 2 eq { offsetline }{ offsetcurve } ifelse
} def

/offsetline {
dup def_vin dup def_vout
dup 1 tail 2 head spill exch 2 last exch printelement
dup 2 get spill
vin vout p+v norm perp n v*n p+v
%vin norm perp n v*n p+v
%vout norm perp n v*n p+v
%vin mag 0 ne vout mag 0 ne and {vin ang dup =only ( )print vout ang dup =only ( )print exch sub =} if
2 aa
exch 1 get 2 last fixline 1 index printelement / =
} def

/fixline {
spill vin vout p+v norm perp n v*n p+v 2 aa
} def

/offsetcurve {
dup def_vin dup def_vout dup def_venter dup def_vexit
dup 1 tail 2 head spill exch 2 last exch printelement
dup 2 get spill
6 4 roll
%vin norm perp n v*n p+v
vin venter p+v norm perp n v*n p+v
6 4 roll
%vout norm perp n v*n p+v
vout vexit p+v norm perp n v*n p+v
6 4 roll
%vout norm perp n v*n p+v
vout vexit p+v norm perp n v*n p+v
6 aa
exch 1 get 2 last fixcurve 1 index printelement / =
} def

/fixcurve {
spill vin venter p+v norm perp n v*n p+v 2 aa
} def

/def_vin {
dup 1 get 2 last spill 2 index 2 get 2 head spill 4 2 roll p-p
2 copy mag .05 lt {
pop pop dup 1 get length 2 eq {
dup 0 get 2 last spill 2 index 1 get spill 4 2 roll p-p
}{ dup 1 get 4 last spill 4 2 roll p-p } ifelse
} if
2 aa exch pop cvx /vin exch def
} def

/def_vout {
dup 2 get 2 last spill 2 index 3 get 2 head spill 4 2 roll p-p
2 aa exch pop cvx /vout exch def
} def

/def_venter { % quad
dup 1 get dup length 6 eq { % q q1
%dup ==
4 last spill % q x1 y1 x2 y2
4 2 roll p-p % q dx dy
3 2 roll pop
}{ % q q1
spill % q x1 y1
3 2 roll % x1 y1 q
0 get 2 last % x1 y1 q0{..2}
spill % x1 y1 x2 y2
p-p
} ifelse
2 aa cvx /venter exch def
%/venter load == / =
} def

/def_vexit {
2 get 4 last spill 4 2 roll p-p
2 aa cvx /vexit exch def
} def

/offsetpath {
/n exch def
closedsubpaths
%dup printlengths
%dup drawpoints
offsetbyvectors %dup ==
newpath
drawpath
} def

/drawpathpoints {
closedsubpaths drawpoints
} def

/circ {
gsave
0 setgray
newpath 1 0 360 arc stroke
grestore
} def

/drawpoints {
{ { 2 2 { spill circ } fortuplem } forall } forall
} def



/xtt {
x3 x2 3 mul sub
x1 3 mul add x0 sub tt 3 exp mul
x2 3 mul x1 6 mul neg add
x0 3 mul add tt dup mul mul add
x1 3 mul x0 3 mul neg add tt mul add x0 add
} def % x coefficients as f(t)

/ytt {
y3 y2 3 mul sub
y1 3 mul add y0 sub tt 3 exp mul
y2 3 mul y1 6 mul neg add
y0 3 mul add tt dup mul mul add
y1 3 mul y0 3 mul neg add tt mul add y0 add
} def % y coefficients as f(t)

/bezierlength {
/oldx x0 def /oldy y0 def /blength 0 def
0 1 numpoints 1 sub div 1.0001 {
/tt exch def
xtt ytt /newy exch def /newx exch def
newx oldx sub dup mul
newy oldy sub dup mul add sqrt
blength add /blength exch def
/oldy newy def /oldx newx def
} for
}def % approximate curve with line segments

/numpoints 37 def

/printelement {
dup ==
2 copy printelementlength
printelementangles
} def

/printelementangles {
dup length 2 eq {
vin ang ==
}{
vin ang == vexit ang ==
vexit ang vin ang a-a ==
} ifelse
pop pop
} def

/a-a {
2 copy 270 gt exch 90 lt and { exch 360 add exch } if
sub
} def

/printlengths {
{ printsubpathlengths } forall
} def

/printsubpathlengths {
dup 1 last spill 2 last 1 index 0 get printelementlength
1 2 { spill exch 2 last printelementlength }fortuplem
} def

/printelementlength {
dup length 2 eq { printlinelength }{ printcurvelength } ifelse
} def

/printlinelength {
exch spill 3 2 roll
spill 4 2 roll p-p mag ==
} def

/printcurvelength {
exch spill 3 2 roll
spill {y3 x3 y2 x2 y1 x1 y0 x0}{exch def}forall
bezierlength blength ==
} def


3 3 scale
/Calibri-Bold 600 selectfont
100 100 moveto (9) true charpath
gsave stroke grestore
gsave drawpathpoints grestore
%gsave 20 setlinewidth strokepath 1 setlinewidth stroke grestore

%flattenpath
1 0 0 setrgbcolor
gsave 8 offsetpath stroke grestore
%gsave 7 offsetpath 3 offsetpath stroke grestore
%gsave 7 offsetpath 3 offsetpath 3 offsetpath stroke grestore
%gsave 10 offsetpath stroke grestore
%gsave -20 offsetpath stroke grestore

showpage quit

luser droog

unread,
Aug 11, 2019, 4:33:38 PM8/11/19
to
[snip]
[snip]

Using this output was enough to isolate that bad corner. This is the
bottom left corner of the '9' where it turns from about 90 degrees
(North) to about 342 degrees (ENE). I translated and scaled and got
some smaller numbers, then copied them 3 places in my notebook and
plotted the points by hand. And then, I found something.

The shapes of the control polygons of the curves in the input curve
are all disjoint and "normal". But each of these maps to a strange
shape and several of them overlap each other. One of them is "twisted"
where the incoming tangent vector intersects the outgoing one. Two of
them look like a "thorny triangle", where the vector from the first
control point to the second is almost perpendicular to the vector from
the start point the final point. And one of them is "inverted", where
the vector from the first control point to the second is much longer
than the vector from the start point to the final point.

So, the new strategy is: run through the original curve and the new
generated curve and discover where a "normal, disjoint" curve maps
to a "strange, overlapping" curve, and replace sequences of these
with a new curve built from the incoming tangent vector of the first
curve and the outgoing tangent of the last curve. Checking for overlap
is going to be a little painful, so hopefully I can get away with
just checking for strangeness. This involves computing an intersection
for the "twisted" case, but for the other cases it's simple vector
operations.

I'm thinking about using a PostScript trick to keep all this data
organized. I have a path array of subpath arrays of element arrays of
coordinates. I can make associations between the element arrays and
other useful data by making definitions in a dictionary, using the
element arrays as keys. Then I can use simple 'forall' loops and load
associated data back out of the dict, having the element to hand.

This should help clean up a lot of the crazy loops, I think. I just
need one crazy loop to collect all of the contextual vectors, then
just 'forall' loops after that.

luser droog

unread,
Aug 13, 2019, 7:03:58 PM8/13/19
to
On Sunday, August 11, 2019 at 3:33:38 PM UTC-5, luser droog wrote:
> On Thursday, August 8, 2019 at 2:22:52 AM UTC-5, luser droog wrote:
> > On Wednesday, August 7, 2019 at 12:05:51 AM UTC-5, luser droog wrote:

> > > At long last I've implemented all of these ideas without errors this time.
> > > And it still makes ears. But I think this is good progress.
> > >
> > > I found some code in Don Lancaster's pages to measure the length of a curve.
> > > I think I can use that to determine if my new offset curve is bigger or
> > > smaller than the original. And that should tell me whether we're inside or
> > > outside of a bend.
> > >
> > > But a numerical/conceptual problem keeps cropping up. Whenever I try to
> > > run through a series of line segments to get the angular change, I don't
> > > know how to bound the output to be 0-360. My modular arithmetic skills
> > > keep crapping out. Maybe pen and paper are the tool for that too.
> > > This problem of getting a usable figure for angular change is exactly
> > > what I need to solve to find out how "tight" the bend is in the curve.
> > >
Mostly there. Everything works. Ear detection seems to be working just
by checking two vectors from each curve, the "control vector" that goes
from the first control point to the second, and the "transit vector"
that goes straight from the first point to the final point. If the
control vector is bigger than the transit vector, that's an "inverted
curve" by my definition. And if the angular difference between the
control vector and the transit vector is more than about 80 degrees,
then that's either a thorny triangle or a twisted curve if it's even
bigger, closer to 180 degrees.

When drawing an offset curve 10 points away, I get 2 ears which matches
the visual results. At 5 points away, I get this "ear report":

[(00000000000000011000000000000000000000000) (000000000000000)]

which matches the visual results. At only 2 points away, no ears are
found, which also looks right.

So, now for the chore of actually snipping the ears. I think the same
PostScript trick of using a dict as an associative array will help.
I can associate each string of the ear report with the subpath array
it belongs to. Then I can use a plain 'forall' loop instead of a weird
'for' loop to access parallel arrays.

I expect to have some pretty output soon. Wish me luck!

droog

luser droog

unread,
Aug 20, 2019, 1:46:24 AM8/20/19
to
On Tuesday, August 13, 2019 at 6:03:58 PM UTC-5, luser droog wrote:

> Mostly there. Everything works. Ear detection seems to be working just
> by checking two vectors from each curve, the "control vector" that goes
> from the first control point to the second, and the "transit vector"
> that goes straight from the first point to the final point. If the
> control vector is bigger than the transit vector, that's an "inverted
> curve" by my definition. And if the angular difference between the
> control vector and the transit vector is more than about 80 degrees,
> then that's either a thorny triangle or a twisted curve if it's even
> bigger, closer to 180 degrees.
>
> When drawing an offset curve 10 points away, I get 2 ears which matches
> the visual results. At 5 points away, I get this "ear report":
>
> [(00000000000000011000000000000000000000000) (000000000000000)]
>
> which matches the visual results. At only 2 points away, no ears are
> found, which also looks right.
>
> So, now for the chore of actually snipping the ears. I think the same
> PostScript trick of using a dict as an associative array will help.
> I can associate each string of the ear report with the subpath array
> it belongs to. Then I can use a plain 'forall' loop instead of a weird
> 'for' loop to access parallel arrays.
>
> I expect to have some pretty output soon. Wish me luck!
>

Sigh. Same story again. It all works, but it doesn't "work". I tried
snipping the ears by replacing the subsequence of curves with a single
curve. And I tried grabbing earlier and/or later curves as well.

I think it might work if I use 2 curves. I have a sketch of how to
do it, but nothing typed in yet.

luser droog

unread,
Aug 22, 2019, 2:44:47 AM8/22/19
to
With two curves, the output still isn't right. But it is interesting.
The whole thing sorta halfway works. I can snip the ears, but I still
need to devise a suitable replacement fragment.

On the other hand, checking for self-intersection seems like a more
correct method. Then I'd have a point of intersection and I could
just chop it at that point.

jdaw1

unread,
Oct 10, 2019, 6:07:59 PM10/10/19
to
This is an important and difficult problem. I want this to work, but I want this to work when applied to complicated path returned by charpath, and to work with total reliability. Which is a big ask.

E.g., a different route to this answer was requested in 2005: https://groups.google.com/forum/#!msg/comp.lang.postscript/VgN-zQSklVI/ (“What is wanted is to do the partial stroke of this path that is ≤6pt from the path, but >5pt.”)

Currently the equivalent is achieved by the ugly-tastic likes of 1 setlinejoin 1 setlinecap gsave 0 setgray 12 setlinewidth stroke grestore 1 setgray 10 setlinewidth stroke.

jdaw1

unread,
Oct 10, 2019, 6:09:52 PM10/10/19
to

luser droog

unread,
Oct 13, 2019, 4:39:46 AM10/13/19
to
Now that you mention it, I pulled up the code I have and took a look at
where I had left off. Just like all the other versions, it *almost*
works. That is the code all functions correctly but it still *doesn't
quite look right*. It looks like my last several messages were all talk,
so here's what I've got. The descriptive comments were added just now
because I didn't bother with anything like that while writing it.

%!
%errordict/typecheck{countexecstack array execstack == quit}put
/args-begin { dup length dict begin { exch def } forall } def
/map { [ 3 1 roll forall ] } def
/spill { aload pop } def
/head { 0 exch getinterval } def
/tail { 1 index length 1 index sub getinterval } def
/last { 1 index length 1 index sub exch getinterval } def
/droplast { 1 index length exch sub 0 exch getinterval } def
/fortuplem { {p m n a} args-begin
0 n /a load length m sub
[ /a load /exch cvx m /getinterval cvx /p load /exec cvx ] cvx
end for } def
/p-p { exch 3 1 roll sub 3 1 roll sub exch } def
/p+v { exch 3 1 roll add 3 1 roll add exch } def
/v*n { 3 2 roll 1 index mul 3 1 roll mul } def
/a-a { 2 copy 270 gt exch 90 lt and { exch 360 add exch } if sub } def
/perp { exch neg } def
/mag { dup mul exch dup mul exch add sqrt } def
/norm { 2 copy mag dup 0 eq { }{ 1 exch div } ifelse v*n } def
/ang { exch atan } def
/aa { array astore } def
/cat { 2 aa {spill} map } def
/median { p+v .5 v*n } def
/max { 2 copy lt { exch } if pop } def
/min { 2 copy gt { exch } if pop } def
/sign { dup 0 ne { 0 gt { 1 }{ -1 } ifelse } if } def
/ps {pstack / =} def /pq {ps quit} def

% The path data structure is an array of subpaths.
% A subpath is an array of segments.
% A segment is a length 2 array for a moveto (if first in the subpath) or lineto (general position)
% or a length 6 array for a curveto segment.
% For this application (operating upon the result of charpath), subpaths are assumed to be closed,
% with the last point coincident with the first. Except the very last subpath which will be a single
% moveto which positions the current point for the next glyph.
/closedsubpaths { [ {[ 3 1 roll 2 aa} {2 aa} {6 aa} {]} pathforall ] {{]}stopped{exit}if}loop } def
/drawpath {
{ dup length 1 eq { spill spill moveto }{
dup 1 head spill spill moveto
1 tail { dup length 2 eq { spill lineto }{ spill curveto } ifelse } forall
closepath
} ifelse
} forall
} def


% The intersection algorithm here has been copied from online refs,
% but also modified to consider coincident points not be an intersection.
% Or at least I tried. Consequently these are no longer in the
% general form. Although they also don't quite succeed in letting
% coincident points to pass. This leads to extra logic in the double-loop
% in find-intersections not to try comparing segments that are within
% a distance of two from each other.
/orientation {
{p3y p3x p2y p2x p1y p1x} args-begin
p2y p1y sub p3x p2x sub mul
p2x p1x sub p3y p2y sub mul sub
sign
end
} def
/colinear { orientation 0 eq } def
/cw { orientation 0 gt } def
/ccw { orientation 0 lt } def
%0 0 4 4 1 2 orientation = 0 0 4 4 1 2 ccw = 0 0 4 4 1 1 orientation =

/onsegment {
{ry rx qy qx py px} args-begin
rx qx eq ry qy eq and px qx eq py qy eq and or {
false
}{
qx px rx max le qx px rx min ge and
qy py ry max le and qy py ry min ge and
} ifelse
end
} def

/intersect? {
{q2y q2x p2y p2x q1y q1x p1y p1x} args-begin
/o1 p1x p1y q1x q1y p2x p2y orientation def
/o2 p1x p1y q1x q1y q2x q2y orientation def
/o3 p2x p2y q2x q2y p1x p1y orientation def
/o4 p2x p2y q2x q2y q1x q1y orientation def
o1 o2 ne o3 o4 ne and { true }{
o1 0 eq p1x p1y p2x p2y q1x q1y onsegment and { true }{
o2 0 eq p1x p1y q2x q2y q1x q1y onsegment and { true }{
o3 0 eq p2x p2y p1x p1y q2x q2y onsegment and { true }{
o4 0 eq p2x p2y q1x q1y q2x q2y onsegment and { true }{
false
} ifelse} ifelse} ifelse} ifelse} ifelse
end
} def
%1 1 10 1 1 2 10 2 intersect? = 10 0 0 10 0 0 10 10 intersect? = -5 -5 0 0 1 1 10 10 intersect? =


% The primary workhorse function.
% After capturing the current path as an array, we first create a dict called 'context'.
% The context keys are the segments in the path, the values are an array holding an array
% of points which includes all the points in the segment as well as extra surrounding points.
%
% Next we use all these points to populate a dict called 'vectors', the keys of which are
% segments, and the values are vectors between adjacent points in the context, as well as
% the 'transit vector' which goes straight from the start point to the endpoint of a curve.
%
% Next, we use the context and the vectors to produce the offset of each point along the normal.
% This process yields the new path on the stack, but we all populate a dict called 'parents'.
% The parents keys are the segments of the new path, the values are the corresponding segments
% in the original path.
%
% Next, we run through the new path twice to populate context and vectors with new association.
%
% Then, use all the information gathered about the path to find any ears and snip them.
%
% Finally, install the path back into the graphics state.
/offsetpath {
/n exch def
%flattenpath
closedsubpaths
/context 1 dict def
gathercontext
/vectors 1 dict def
computevectors
/parents 1 dict def
computeoffset
gathercontext
computevectors
snipears
newpath
drawpath
} def


% collect 2 earlier and 1 later points for all elements
/gathercontext {
dup
{ dup length 1 ne {subpathcontext}{pop} ifelse } forall
} def
/subpathcontext {
dup dup 2 last exch 2 head cat elementcontext
dup dup 1 last exch 3 head cat elementcontext
dup 1 4 { elementcontext } fortuplem
dup 3 last exch 1 head cat elementcontext
} def
/elementcontext {dup 2 get length 2 eq { linecontext }{ curvecontext } ifelse} def
/linecontext {
%dup == / =
dup 2 get spill 2 index 1 get 2 last spill p-p
mag .05 gt { dup 1 get 2 last spill }{
dup 1 get length 2 gt { dup 1 get 4 last 2 head spill }{
dup 0 get 2 last spill
} ifelse
} ifelse % c0
2 index 2 get spill % c1
4 index 3 get 2 head spill % c2
6 aa exch 2 get exch context 3 1 roll put
} def
/curvecontext {
%dup == / =
dup 1 get length 2 eq {
dup 0 get 2 last spill % c0
2 index 1 get spill % c1
}{
dup 1 get 4 last spill % c0 c1
} ifelse
4 index 2 get spill % c2 c3 c4
10 index 3 get 2 head spill % c5
12 aa exch 2 get exch context 3 1 roll put
} def


% calc vectors between context points
/computevectors {
dup { dup length 1 ne {subpathvectors}{pop} ifelse } forall
} def
/subpathvectors { { elementvectors } forall } def
/elementvectors { dup length 2 eq { linevectors }{ curvevectors } ifelse } def
/linevectors {
context 1 index get
dup 4 head spill 4 2 roll p-p
3 2 roll 4 last spill 4 2 roll p-p
4 aa vectors 3 1 roll put
} def
/curvevectors {
context 1 index get
dup 0 4 getinterval spill 4 2 roll p-p % v01
2 index 2 4 getinterval spill 4 2 roll p-p % v12
4 index 4 4 getinterval spill 4 2 roll p-p % v23
6 index 6 4 getinterval spill 4 2 roll p-p % v34
8 index 8 4 getinterval spill 4 2 roll p-p % v45
10 index 8 2 getinterval spill
12 index 2 2 getinterval spill p-p % v14 the 'transit vector'
12 aa exch pop vectors 3 1 roll put
} def


% use vectors to calc normals and offset elements' points
/computeoffset { { dup length 1 ne { subpathoffset } if } map } def
/subpathoffset { { elementoffset } map } def
/elementoffset { dup length 2 eq { lineoffset }{ curveoffset } ifelse } def
/lineoffset {
vectors 1 index get spill p+v norm perp n v*n
2 index spill p+v 2 aa
dup 3 2 roll parents 3 1 roll put
} def
/curveoffset {
vectors 1 index get
1 index 0 2 getinterval spill 2 index 0 4 getinterval spill p+v norm perp n v*n p+v
3 index 2 2 getinterval spill 4 index 6 4 getinterval spill p+v norm perp n v*n p+v
5 index 4 2 getinterval spill 6 index 6 4 getinterval spill p+v norm perp n v*n p+v 6 aa
exch pop
dup 3 2 roll parents 3 1 roll put
} def


/snipears {
find-intersections
} def

% O(N**2) Double-loop through the points, comparing segments that are not
% the same or adjacent. Populate a dict called 'intersections' for any that are found.
% The keys and values are indices of the found intersections. It is expected that
% each intersection will show up twice, ie. if segment 23 intersects with 32 then the dict
% will have << 23 32 >> and << 32 23 >> in there.
/find-intersections {
1 dict begin
{
dup length 1 ne {
/path exch def
/intersections 1 dict def
2 1 path length 1 sub {
context path 2 index get get
p-q-from-context % i p1x p1y q1x q1y
2 1 path length 1 sub {
dup 6 index sub dup 2 ge exch -2 le or %0 ne %abs 1 gt
{
context path 2 index get get
p-q-from-context % i p1x p1y q1x q1y j p2x p2y q2x q2y
8 index 8 index 8 index 8 index
intersect? % i p1x p1y q1x q1y j bool
%{ (X) print } if %==
%{ dup 6 index =only ( )print = } if
{
dup 6 index
2 copy =only ( )print =
intersections 3 1 roll put
} if
} if
pop
} for
pop pop pop pop pop
} for
path
intersections {
2 copy min 3 1 roll max 1 index sub % i n
chopsubpath exch
dup length 1 eq { transformear-line }{ transformear-curve } ifelse
exch joinsubpath
exit
} forall
} if
} map
end
/ =
} def

/chopsubpath { 3 1 roll headtail 3 2 roll headtail } def
/headtail { 2 copy head 3 1 roll tail } def
/joinsubpath { cat cat } def


% What to replace the ear with?
/transformear-line { 1 last 0 get 2 last 1 aa } def

/transformear-curve { % [1 1] -> [0]
dup 0 get exch 1 last 0 get % ^[] $[]
/f exch def /s exch def
context s get 4 head 2 headtail cvx /sc1 exch def cvx /sc0 exch def
vectors s get 2 head cvx /sv0 exch def
context f get 4 last 2 headtail cvx /fc5 exch def cvx /fc4 exch def
vectors f get 8 2 getinterval cvx /fv4 exch def
fc4 sc1 p-p 2 aa cvx /nv5 exch def
sv0 norm nv5 mag .05 mul v*n 2 aa cvx /nv1 exch def
sc1 nv1 p+v 2 aa cvx /nc2 exch def
fv4 norm nv5 mag .05 mul v*n 2 aa cvx /nv3 exch def
fc4 nv3 p-p 2 aa cvx /nc3 exch def
[ nc2 nc3 fc4 ]
context 1 index [ sc0 sc1 nc2 nc3 fc4 fc5 ] put
vectors 1 index [ sv0 nv1 nc3 nc2 p-p nv3 fv4 nv5 ] put
1 aa
} def

/p-q-from-context {
dup length 6 eq {
0 4 getinterval spill
}{
dup 2 2 getinterval spill 3 2 roll 8 2 getinterval spill
} ifelse
} def


% The main script
% Use charpath to produce the outline of the number 9 in
% Calibri 600pt. Scale up to make the problem spot more visible.
% Attempt various combinations of offsets and offsets of offsets.

8 dup dup scale currentlinewidth exch div setlinewidth
/Calibri-Bold 600 selectfont
10 10 moveto (9) true charpath
gsave stroke grestore
%gsave 20 setlinewidth strokepath 1 setlinewidth stroke grestore

1 0 0 setrgbcolor
gsave 5 offsetpath stroke grestore
gsave 10 offsetpath stroke grestore
%gsave 5 offsetpath 5 offsetpath stroke grestore
gsave 5 offsetpath 5 offsetpath 5 offsetpath stroke grestore

showpage quit

luser droog

unread,
Oct 13, 2019, 4:51:57 AM10/13/19
to
On Sunday, October 13, 2019 at 3:39:46 AM UTC-5, luser droog wrote:
>
> Now that you mention it, I pulled up the code I have and took a look at
> where I had left off. Just like all the other versions, it *almost*
> works. That is the code all functions correctly but it still *doesn't
> quite look right*. It looks like my last several messages were all talk,
> so here's what I've got. The descriptive comments were added just now
> because I didn't bother with anything like that while writing it.
>
> %!
> %errordict/typecheck{countexecstack array execstack == quit}put
[...]
> showpage quit

Since I copied straight out of emacs instead of xterm, it didn't show the
filename. FWIW this one is 'stroke9.ps'.

jdaw1

unread,
Jun 4, 2020, 6:15:48 AM6/4/20
to
As a marker, this is still wanted. But only if it works with total reliability, even on the nasty paths made by charpath, whether the font be a Helvetica, a Garamond, or a Blackletter monstrosity.

luser droog

unread,
Jul 9, 2020, 1:31:22 PM7/9/20
to
On Thursday, June 4, 2020 at 5:15:48 AM UTC-5, jdaw1 wrote:
> As a marker, this is still wanted. But only if it works with total reliability, even on the nasty paths made by charpath, whether the font be a Helvetica, a Garamond, or a Blackletter monstrosity.

The path forward appears to me to be in that Bezier curve classification
paper. My code has all the infrastructure to run through the path,
gather lots of context points for each curve, calculate vectors and
normals and offsets of the control points. But it's missing some
heuristics to detect problem curves and deal with them, probably by
chopping into 2 simpler curves.

But if we can analyze the curve and detect whether there's a cusp
in the middle and *where* on the curve it is, then we can chop the
curve into two easier pieces and a few recursive steps should yield
an offset curve with better fine tracking. ... Or maybe better not
to chop and find an intersection between offsets of the two vectors.
I'm not sure which track will yield better results.

jdaw1

unread,
Feb 13, 2021, 12:51:03 PM2/13/21
to
0 new messages