1

I'm trying to create a wheel cog in Metapost that I would like fill in with a color. I came up with this code:

vardef sin(expr xx) = sind(xx) enddef;
vardef cos(expr xx) = cosd(xx) enddef;
beginfig(1)
  inc := 15;
  inner_radius := 1.0cm;
  outer_radius := 1.3cm;

for ang = 0 step 2 * inc until 360: draw (inner_radius * sin(ang), inner_radius * cos(ang)).. (inner_radius * sin(ang + inc), inner_radius * cos(ang + inc))-- (outer_radius * sin(ang + inc), outer_radius * cos(ang + inc))-- (outer_radius * sin(ang + 2 * inc), outer_radius * cos(ang + 2 * inc))-- (inner_radius * sin(ang + 2 * inc), inner_radius * cos(ang + 2 * inc)); endfor; endfig;

Which produces the figure I like but is there an easy way to adapt the code to fill in with a color?

maxwell79
  • 59
  • 4
  • 1
    Not by the computer, so just a comment:, Construct a path first with the for loop, do not forget to add --cycle at the end (if not you cannot fill it). Then you can draw and fill that path. – mickep Jul 25 '22 at 11:35
  • We have done this before by the way. I do not think any of the answers there was in MP -- but they could be adapted. – Thruston Jul 25 '22 at 13:36

2 Answers2

2

Let's take this in stages.

To make something you can fill, you need a single closed path. Your approach of drawing a series of independent line segments needs to be adapted. So lets change your code to make a single path, built up one "tooth" at a time:

  path cog; 
  for ang = 0 step 2 * inc until 360:
      cog := if known cog: cog -- fi
      (inner_radius * sin(ang), inner_radius * cos(ang))..
      (inner_radius * sin(ang + inc), inner_radius * cos(ang + inc))--
      (outer_radius * sin(ang + inc), outer_radius * cos(ang + inc))--
      (outer_radius * sin(ang + 2 * inc), outer_radius * cos(ang + 2 * inc))--
      (inner_radius * sin(ang + 2 * inc), inner_radius * cos(ang +  2 * inc));
  endfor
  draw cog;

This produces the same figure -- the clever bit is in the first line of the loop. Note that you use := to update a variable. In MP a=b means a is equal to b; but a := b means assign the value of b to a. So here you are updating the path variable cog each time through the loop. Effectively you are extending the path by doing cog := cog -- tooth each time. Except that this will not work the first time, so you need to protect the very first assignment with the if known trick. The first time round the loop cog has no value so it is not known so the first assignment will be cog := tooth as required. Notice that the if .. fi is expanded in line.

So now you have a single path cog. But in order to fill it, you need to close it, so you need one more step after the loop: cog := cog .. cycle; Like this

  path cog; 
  for ang = 0 step 2 * inc until 360:
      cog := if known cog: cog -- fi
      (inner_radius * sin(ang), inner_radius * cos(ang))..
      (inner_radius * sin(ang + inc), inner_radius * cos(ang + inc))--
      (outer_radius * sin(ang + inc), outer_radius * cos(ang + inc))--
      (outer_radius * sin(ang + 2 * inc), outer_radius * cos(ang + 2 * inc))--
      (inner_radius * sin(ang + 2 * inc), inner_radius * cos(ang +  2 * inc));
  endfor
  cog := cog .. cycle;
  fill cog withcolor 3/4[blue, white];
  draw cog;

which produces this

enter image description here

Ooops! There is one step too many in the loop, because you have gone from 0 to 360, so there is an extra tooth and the & cycle has looped back. Note that this makes no difference to the fill, but looks bad for the draw. It is easy to fix. Just subtract a small amount eps from 360 so we stop earlier:

  path cog; 
  for ang = 0 step 2 * inc until 360 - eps:
      cog := if known cog: cog -- fi
      (inner_radius * sin(ang), inner_radius * cos(ang))..
      (inner_radius * sin(ang + inc), inner_radius * cos(ang + inc))--
      (outer_radius * sin(ang + inc), outer_radius * cos(ang + inc))--
      (outer_radius * sin(ang + 2 * inc), outer_radius * cos(ang + 2 * inc))--
      (inner_radius * sin(ang + 2 * inc), inner_radius * cos(ang +  2 * inc));
  endfor
  cog := cog .. cycle;
  fill cog withcolor 3/4[blue, white];
  draw cog;

enter image description here

Job done? Well not quite, because the cog := cog .. syntax is a bit clunky, especially as it needs the if known trick. And there is an Easier Way. You can put the loop inside the definition of the path:

  path cog; cog = 
  for ang = 0 step 2 inc until 360 - eps:
      (inner_radius * sin(ang), inner_radius * cos(ang)) --
      (inner_radius * sin(ang + inc), inner_radius * cos(ang + inc)) --
      (outer_radius * sin(ang + inc), outer_radius * cos(ang + inc)) --
      (outer_radius * sin(ang + 2 inc), outer_radius * cos(ang + 2 inc)) --
      (inner_radius * sin(ang + 2 inc), inner_radius * cos(ang +  2 inc)) &
  endfor cycle;
  fill cog withcolor 3/4[blue, white];
  draw cog;

which is rather neater.

And having said all that, one of the nice things about MP is that you never have to muck about with sind and cosd. For example, here is a different approach.

\documentclass[border=5mm]{standalone}
\usepackage{luamplib}
\begin{document}
\begin{mplibcode}
beginfig(1)
  path greatc, smallc;
  greatc = fullcircle scaled 2.6 cm;
  smallc = fullcircle scaled 2.0 cm;

numeric teeth; teeth = 12; path cog; cog = for t = 1 upto teeth: subpath (8/teeth)(t-1, t-1/2) of greatc -- subpath (8/teeth)(t-1/2, t) of smallc -- endfor cycle;

fill cog withcolor 3/4[blue, white]; draw cog; endfig; \end{mplibcode} \end{document}

Compile this with lualatex to get:

enter image description here

with curved edges...

Finally, I am no engineer but what we have drawn looks a bit more like a coronavirus than a cog, so you might need to chamfer each tooth a bit. Perhaps like this?

\documentclass[border=5mm]{standalone}
\usepackage{luamplib}
\begin{document}
\mplibtextextlabel{enable}
\begin{mplibcode}
beginfig(1)
  path greatc, smallc;
  greatc = fullcircle scaled 2.6 cm;
  smallc = fullcircle scaled 2.0 cm;

numeric teeth; teeth = 12; numeric r; r = 1/24; path cog; cog = for t = 1 upto teeth: subpath (8/teeth)(t-1+r, t-1/2-r) of greatc -- subpath (8/teeth)(t-1/2+r, t-r) of smallc -- endfor cycle;

fill cog withcolor 3/4[blue, white]; draw cog; endfig; \end{mplibcode} \end{document}

Compile with lualatex to get this:

enter image description here

Thruston
  • 42,268
1

The fill statement needs a cyclic path argument; use the cycle keyword to close a path. You can construct the path, store it in a variable then fill it:

path p;
p := for ang = 0 step 2*inc until 360 - epsilon:
    (inner_radius * sin(ang), inner_radius * cos(ang)) --
    (inner_radius * sin(ang + inc), inner_radius * cos(ang +  inc)) --
    (outer_radius * sin(ang + inc), outer_radius * cos(ang + inc)) --
    (outer_radius * sin(ang + 2 * inc), outer_radius * cos(ang + 2 * inc)) --
    (inner_radius * sin(ang + 2 * inc), inner_radius * cos(ang +  2 * inc)) --
  endfor
  cycle;
fill p withcolor 0.7white;

Another option is to define the path to be filled “inline“, i.e. to put the for loop directly after the fill keyword:

\documentclass[border=2mm]{standalone}
\usepackage{luamplib}

\begin{document} \begin{mplibcode} vardef sin(expr xx) = sind(xx) enddef; vardef cos(expr xx) = cosd(xx) enddef;

beginfig(1); inc := 15; inner_radius := 1.0cm; outer_radius := 1.3cm;

fill for ang = 0 step 2inc until 360 - epsilon: (inner_radius sin(ang), inner_radius * cos(ang)) -- (inner_radius * sin(ang + inc), inner_radius * cos(ang + inc)) -- (outer_radius * sin(ang + inc), outer_radius * cos(ang + inc)) -- (outer_radius * sin(ang + 2 * inc), outer_radius * cos(ang + 2 * inc)) -- (inner_radius * sin(ang + 2 * inc), inner_radius * cos(ang + 2 * inc)) -- endfor cycle withcolor 0.7white; endfig; \end{mplibcode} \end{document}

Note: although this doesn't change anything for filling, I substracted epsilon from 360 because it is cleaner this way (the reason is well explained in Thruston's answer, +1). epsilon is 1/65536, i.e. the smallest positive number MetaPost can handle.

enter image description here

frougon
  • 24,283
  • 1
  • 32
  • 55