# Convert a Bezier to XY wave In order to make a nice smooth illustration, I've drawn a bezier* on my graph.

But now my graph has too many lines, and I want to use "markers" to show the bezier as spheres/triangles etc.  For this I think I need to use waves (I do this often with sparse markers).

Is there any way to convert the Bezier to an XY wave, or any other solution here?

Thanks!

(*CTRL+T, right click Polygon tool, draw Bezier)

In the meantime, here's the "Cubic Bezier Curve Flattening using Forward Differencing" algorithm Igor's is based on:

https://gist.github.com/rlindsay/c55be560ec41144f521f

Is there a way to tweak the number of points in the waves W_PolyX and W_PolyY that DrawAction extractOuline produces?

Say, I'd like to "wave" the following bezier:

Window Graph0() : Graph
Make/O w = nan
Display w
SetAxis left*,128
SetDrawLayer UserFront
SetDrawEnv xcoord= bottom,ycoord= left
DrawBezier 4.7065034842466,7.14085694275653,0.956151,0.921157,{4.7065034842466,7.14085694275653,4.7065034842466,7.14085694275653,23.73305842356,32.2103830643604,45.4585566789422,48.2641594054833,67.1840549343245,64.3179357466063,90.7319706331314,71.1687650784765,90.7319706331314,71.1687650784765,90.7319706331314,71.1687650784765,95.308229338295,79.5737119191777}
DrawBezier/A {108.382023375747,95.8329122606684,121.4558174132,112.092112602159,125.384572602189,114.478647825545,125.384572602189,114.478647825545}
EndMacro

I get pretty much get what I want with:

DrawAction extractOutline
Display W_PolyY vs W_PolyX

but the resulting trace does not look as smooth as the curve itself. I wonder if this could be improved if I specified the number of points, which seems to be fixed to 80 in this case.

Interpolation and Smoothing help a bit, but it's not quite there yet.

Looking at the source code, it appears that we are drawing a bezier by computing 20 points per bezier segment. There isn't any intelligence or user control for that. If you plot the extracted waves on the graph with your draw-object bezier, and set the fill for the bezier to None, I think you will see that the two versions are the same.

It does seem like we should provide some control over the resolution of the bezier to polygon conversion.

Does the bezier represent something in particular? Where did you get the bezier coordinates from?

Thanks John,

johnweeks wrote:

Does the bezier represent something in particular? Where did you get the bezier coordinates from?

The scaling is arbitrary, but the shape is typical for a melting curve of minerals/rocks in temperature vs. pressure space. The discontinuity is a result of a phase change within the solid and must not be smoothed out. I'm compiling literature data, some of which are only shown as drawn curves in papers. I use an IgorThief approach to replicate a given curve with a bezier to extract the numeric values. That works like a charm!

I could not check yet if the apparent non-smoothness of the bezier-to-wave conversion would even show up on a print-out.

johnweeks wrote:

It does seem like we should provide some control over the resolution of the bezier to polygon conversion.

That would be awesome :-)

Igor's BezierToPolygon operation has a /NSEG=(numberOfSegments) parameter:

BezierToPolygon [ /DSTX=destXWave /DSTY=dstYWave /FREE /NSEG=nseg ] bezXWave, bezYWave

The BezierToPolygon operation creates an XY pair of waves approximating the Bezier curves described by bezXWave and bezYWave.

The BezierToPolygon operation was added in Igor Pro 9.00.

Flags

/DSTX=destX    Specifies the X destination wave to be created or overwritten. If you omit /DSTX, destX defaults to W_PolyX.

/DSTY=destY    Specifies the Y destination wave to be created or overwritten. If you omit /DSTY, destY defaults to W_PolyY.

/FREE    Creates output waves as free waves (see Free Waves). /FREE is allowed only in functions. If you use /DSTX or /DSTY then the specified parameter must be either a simple name or a valid wave reference.

/NSEG=nseg    The number of segments used to render each Bezier segment from 1 and 500. The default of 20 is usually sufficient.

Those contain the Bezier control points. You can either compute the control points or draw them and extract the values from the DrawBezier recreation command with something like MakePolyFromDrawnBezier

Window Panel0() : Panel
PauseUpdate; Silent 1       // building window...
NewPanel /W=(150,77,450,277)
SetDrawLayer UserBack
SetDrawEnv linefgc= (65535,0,0)
DrawBezier 42,85,1,1,{42,85,42,44.8876577597368,54.9092779656255,45,77,45,99.0907220343745,45,104,74.8132267551044,104,99,104,123.186773244896,42,85,42,85}
EndMacro

Function MakePolyFromDrawnBezier()
// from DrawBezier command: DrawBezier 42,85,1,1,{42,85,42,44.8876577597368,54.9092779656255,45,77,45,99.0907220343745,45,104,74.8132267551044,104,99,104,123.186773244896,42,85,42,85}
Make/O xy = {42,85,42,44.8876577597368,54.9092779656255,45,77,45,99.0907220343745,45,104,74.8132267551044,104,99,104,123.186773244896,42,85,42,85}
Variable n= numpnts(xy)/2
Make/O/N=(n) wx = xy[0+p*2]
Make/O/N=(n) wy = xy[1+p*2]
Variable xorg = wx
Variable yorg = wy
BezierToPolygon wx,wy // Add /NSEG=num to control precision
WAVE W_PolyX, W_PolyY
DrawPoly /W=Panel0 xorg, yorg, 1, 1, W_PolyX,W_PolyY
End

DisplayHelpTopic "Drawing Polygons and Bezier Curves"

This wasn't as easy as I thought it would be.

This code creates a wave-based DrawPoly object from the "first" drawn Bezier object. "First" means "first in the recreation macro".

Fortunately, you can reorder drawing objects position in the recreation macro by selecting the object and choosing "Send to Back", etc.

"Send To Back" will make the object the first drawn object; just what we need.

The choose MakePolyFromFirstDrawnBezier from the Macros menu. Accept or change the segmentsPerBezier = 20 (experiment with small numbers to see why it matters).

A polygon version of the first Bezier drawn in the top graph, panel, or layout is added to the same window.

None of the Bezier's attributes such as fill or line color are copied to the polygon. You may have to correct the coordinate system if the bezier wasn't drawn with the default coordinate system.

You can comment out the code that adds the DrawPoly object if all you want are the polyX and polyY waves created by MakePolyFromBezierCoordinates().

#pragma IgorVersion=9.0

Macro MakePolyFromFirstDrawnBezier(segmentsPerBezier)
Variable segmentsPerBezier=20

String win = WinName(0,1+4+64) // Graphs, Layouts, Panels
PolyWavesFromFirstDrawnBezier(win,segmentsPerBezier)
End

Function PolyWavesFromFirstDrawnBezier(String win, Variable segmentsPerBezier)

Variable xorg, yorg, hscaling, vscaling, isAbsolute
String coordinates = FirstBezierCoordinates(win, xorg, yorg, hscaling, vscaling, isAbsolute)
if( strlen(coordinates) )
[WAVE polyX, WAVE polyY] = MakePolyFromBezierCoordinates(coordinates, segmentsPerBezier)
String cmd
if( isAbsolute )
DrawPoly/ABS /W=\$win xorg, yorg, hscaling, vscaling, polyX, polyY
else
DrawPoly /W=\$win xorg, yorg, hscaling, vscaling, polyX, polyY
endif
endif
End

Function [WAVE polyX, WAVE polyY] MakePolyFromBezierCoordinates(String coordinates, Variable segmentsPerBezier)

Variable numItems= ItemsInList(coordinates,",")
Variable n= numItems/2
Make/O/D/N=(n)/FREE wx = str2num(StringFromList(0+p*2,coordinates,","))
Make/O/D/N=(n)/FREE wy = str2num(StringFromList(1+p*2,coordinates,","))
BezierToPolygon/NSEG=(segmentsPerBezier) wx,wy
WAVE W_PolyX, W_PolyY
// BezierToPolygon always creates W_PolyX, W_PolyY, we need waves that won't get overwritten.
String xname=UniqueName("Poly4BezX",1,0)
String suffix = xname[strlen("Poly4BezX"),inf]
String yname= "Poly4BezY"+suffix
Duplicate/O W_PolyX, \$xname; WAVE polyX=\$xname
Duplicate/O W_PolyY, \$yname; WAVE polyY=\$yname
End

Function/S FirstBezierCoordinates(String win, Variable &xorg, Variable &yorg, Variable &hscaling, Variable &vscaling, Variable &isAbsolute)

String list = WinRecreation(win,4) // lines end with \r
// look for first DrawBezier or DrawBezier/ABS command,
// and accumulate coordinates from immediately following DrawBezier/A commands.
// Stop accumulating when the next command is NOT DrawBezier/A.
String separator = "\r"
Variable separatorLen = strlen(separator)
Variable numItems = ItemsInList(list, separator)
Variable i, offset = 0
Variable foundBezier= 0
String bezierkey="\tDrawBezier "
String absbezierkey="\tDrawBezier/ABS "
String appendKey="\tDrawBezier/A "
String coordinates=""
for(i=0; i<numItems; i+=1)
String item = StringFromList(0, list, separator, offset) // When using offset, the index parameter is always 0
Variable isDrawBezier = CmpStr(bezierkey,    item[0,strlen(bezierkey)-1]) == 0
Variable isAbsbezier  = CmpStr(absbezierkey, item[0,strlen(absbezierkey)-1]) == 0
Variable isAppend     = CmpStr(appendKey,    item[0,strlen(appendKey)-1]) == 0

if( !foundBezier && (isDrawBezier || isAbsbezier) )
// we have "\tDrawBezier 42,85,1,1,{42,85,...}"
// or "\tDrawBezier/ABS 0,0,1,1,{48,104,...}"
isAbsolute = isAbsbezier
Variable prefixLen = isAbsbezier ? strlen(absbezierkey) : strlen(bezierkey)
sscanf item[prefixLen,strlen(item)-1], "%g,%g,%g,%g,{", xorg, yorg, hscaling, vscaling
SplitString/E=".*\{(.*)\}" item, coordinates

foundBezier= 1
elseif( foundBezier )
if( !isAppend ) // must be a command AFTER DrawBezier and optional DrawBezier/A
break       // this prevents appending coordinates from additional bezier objects.
endif
// we have "\tDrawBezier/A {48,104,48,104}"
String more
SplitString/E=".*\{(.*)\}" item, more
coordinates += ","+more
// keep going, multiple DrawBezier/A commands are allowed.
endif
offset += strlen(item) + separatorLen
endfor
return coordinates
End

Hello Jim,

that's fantastic, another great example of the outstanding support from WM!

I was not aware of WinRecreation (although I wondered if something like this exists) but admittedly I may have choked on the DrawBezier/A issue.

Function PolyWavesFromFirstDrawnBezier(String win, Variable segmentsPerBezier)

Variable xorg, yorg, hscaling, vscaling, isAbsolute
String coordinates = FirstBezierCoordinates(win, xorg, yorg, hscaling, vscaling, isAbsolute)
if( strlen(coordinates) )
[WAVE polyX, WAVE polyY] = MakePolyFromBezierCoordinates(coordinates, segmentsPerBezier)
Make/O W_bez2Poly
Interpolate2/T=1/N=(numPnts(PolyY))/Y=W_Bez2Poly Polyx,Polyy
KillWaves/Z polyX,polyY
endif
End

Function [WAVE W_polyX, WAVE W_polyY] MakePolyFromBezierCoordinates(String coordinates, Variable segmentsPerBezier)

Variable numItems= ItemsInList(coordinates,",")
Variable n= numItems/2
Make/O/D/N=(n)/FREE wx = str2num(StringFromList(0+p*2,coordinates,","))
Make/O/D/N=(n)/FREE wy = str2num(StringFromList(1+p*2,coordinates,","))
BezierToPolygon/NSEG=(segmentsPerBezier) wx,wy
WAVE W_PolyX, W_PolyY
End

which gives me a single scaled wave as output, but this only makes sense with the bezier drawn with:

`SetDrawEnv xcoord= bottom,ycoord= left`

Thanks a lot!