The Universe of Discourse


Thu, 23 Nov 2006

Linogram: The EaS as a component
In an earlier article, I discussed the definition of an Etch-a-Sketch picture as a component in the linogram drawing system. I then digressed for a day to rant about the differences between specification-based and WYSIWYG systems. I now return to the main topic.

Having defined the EAS component, I can use it in several diagrams. The typical diagram looks like this:

Here's the specification for that figure:

        require "eas";

        number WIDTH = 2;

        EAS the_eas(w = WIDTH);

        gear3 gears(width=WIDTH, r1=1/4, r3=1/12);

        constraints { 
            the_eas.left = gears.g1.c;
            the_eas.right = gears.g3.c; 
        }
The eas.lino file defines the EAS component and also the gear3 component, which describes what the three gears should look like. The diagram itself just chooses a global width and communicates it to the EAS component and the gear3 component. It also communicates two of the three selected gear sizes to the gear3, which generates the three gears and their axles. Finally, the specification constrains the left reference point of the Etch-a-Sketch to lie at the center of gear 1, and the right reference point to lie at the center of gear 3.

This sort of thing was exactly what I was hoping for when I started designing linogram. I wanted it to be easy to define a new component type, like EAS or gear3, and to use it as if it were a built-in type. (Indeed, as noted before, even linogram's "built-in" types are not actually built in! Everything from point on up is defined by a .lino file just like the one above, and if the user wants to redefine point or line, they are free to do so.)

(It may not be clear redefining point or line is actually a useful thing to do. But it could be. Consider, for example, what would happen if you were to change the definition of point to contain a z coordinate, in addition to the x and y coordinates it normally has. The line and box definitions would inherit this change, and become three-dimensional objects. If provided with a suitably enhanced rendering component, linogram would then become a three-dimensional diagram-drawing program. Eventually I will add this enhancement to linogram.)

I am a long-time user of the AT&T Unix tool pic, which wants to allow you to define and use compound objects in this way, but gets it badly wrong, so that it's painful and impractical. Every time I suffered through pic's ineptitude, I would think about how it might be done properly. I think linogram gets it right; pic was a major inspiration.

Slanty lines

Partway through drawing the Etch-a-Sketch diagrams, I had a happy idea. Since I was describing Etch-a-Sketch configurations that would draw lines with various slopes, why not include examples?

Optimally, one would like to say something like this:

  line L(slope=..., start=..., ...);

In general, though, this is too hard to handle. The resulting equations are quadratic, or worse, trigonometric, and linogram does not know how to solve those kinds of equations.

But if the slope is required to be constant, then the quadratic equations become linear, and there is no problem. And in this case, I only needed constant slopes. Once I realized this, it was easy to define a constant-slope line type:

        require "line";

        define sline extends line {
          param number slope;
          constraints {
            end.y - start.y = slope * (end.x - start.x);
          }
        }
An sline is just like a line, but it has an additional parameter, slope (which must be constant, and specified at compile time, not inferred from other values) and one extra constraint that uses it. Normally, all four of start.x, end.x, start.y, and end.y are independent. The constraint removes some of the independence, so that any three, plus the slope, are sufficient to determine the fourth.

The diagram above was generated from this specification:


        require "eas";
        require "sline";

        number WIDTH = 2;

        EAS the_eas(w = WIDTH);

        gear3 gears(width=WIDTH, r1=1/4, r3=1/12);
        sline L1(slope=1/3, start=the_eas.screen.ne);
        sline L2(slope=3, start=the_eas.screen.ne);

        constraints { 
            the_eas.left = gears.g1.c;
            the_eas.right = gears.g3.c; 
            L1.end.x = the_eas.screen.w.x;
            L2.end.y = the_eas.screen.s.y;
        }

The additions here are the sline items, named L1 and L2. Both lines start at the northeast corner of the screen. Line L1 has slope 1/3, and its other endpoint is constrained to lie somewhere on the west edge of the screen. The y-coordinate of that endpoint is not specified, but is implicitly determined by the other constraints. To locate it, linogram must solve some linear equations. The complete set of constraints no the line is:

L1.start.x = the_eas.screen.ne.x
L1.start.y = the_eas.screen.ne.y
L1.end.x = the_eas.screen.w.x
L1.end.y - L1.start.y = L1.slope × (L1.end.x - L1.start.x);
The L1.slope is required to be specified before the equations are solved, and in the example above, it is 1/3, so the last of these equations becomes:

L1.end.y - L1.start.y = 1/3 × (L1.end.x - L1.start.x);
So the resulting system is entirely linear, and is easily solved.

The other line, L2, with slope 3, is handled similarly; its endpoint is constrained to lie somewhere on the south side of the screen.

Surprise features

When I first planned linogram, I was hardly thinking of slopes at all, except to dismiss them as being intractable, along with a bunch of other things like angles and lengths. But it turned out that they are a little bit tractable, enough that linogram can get a bit of a handle on them, thanks to the param feature that allows them to be excluded from the linear equation solving.

One of the signs that you have designed a system well is that it comes out to be more powerful than you expected when you designed it, and lends itself to unexpected uses. The slines are an example of that. When it occurred to me to try doing them, my first thought was "but that won't work, will it?" But it does work.

Here's another technique I hadn't specifically planned for, that is already be supported by linogram. Suppose Fred Flooney wants to use the eas library, but doesn't like the names of the reference points in the EAS component. Fred is quite free to define his own replacement, with whatever names for whatever reference points he likes

        require "eas";

        define freds_EAS {
          EAS it;
          point middle = it.screen.c;
          point bernard = (it.body.e + 2*it.body.se)/3;
          line diag(start=it.body.nw, end=it.body.se);
          draw { it; }
        }
The freds_EAS component is essentially the same as the EAS component defined by eas.lino. It contains a single Etch-a-Sketch, called it, and a few extra items that Fred is interested in. If E is a freds_EAS, then E.middle refers to the point at the center of E's screen; E.bernard is a point two-thirds of the way between the middle and the bottom corner of its outer edge, and E.diag is an invisible line running diagonally across the entire body, equipped with the usual E.diag.start, E.diag.center, and the like. All the standard items are still available, as E.it.vknob, E.it.screen.left.center, and so on.

The draw section tells linogram that only the component named it—that is, the Etch-a-Sketch itself—should be drawn; this suppresses the diag line. which would otherwise have been rendered also.

If Fred inherits some other diagram or component that includes an Etch-a-Sketch, he can still use his own aliases to refer to the parts of the Etch-a-Sketch, without modifying the diagram he inherited. For example, suppose Fred gets handed my specification for the diagram above, and wants to augment it or incorporate it in a larger diagram. The diagram above is defined by a file called "eas4-3-12.lino", which in turn requires EAS.lino. Fred does not need to modify eas4-3-12.lino; he can do:

        require "eas4-3-12.lino";
        require "freds_eas";

        freds_EAS freds_eas(it = the_eas);

        constraints { 
          freds_eas.middle = ...;
          freds_eas.bernard = ...;
        }
        ...
Fred has created one of his extended Etch-a-Sketch components, and identified the Etch-a-Sketch part of it with the_eas, which is the Etch-a-Sketch part of my original diagram. Fred can then apply constraints to the middle and bernard sub-parts of his freds_eas, and these constraints will be propagated to the corresponding parts the the_eas in my original diagram. Fred can now specify relations in terms of his own peronal middle and bernard items, and they will automatically be related to the appropriate parts of my diagram, even though I have never heard of Fred and have no idea what bernard is supposed to represent.

Why Fred wants these names for these components, I don't know; it's just a contrived example. But the important point is that if he does want them, he can have them, with no trouble.

What next?

There are still a few things missing from linogram. The last major missing feature is arrays. The gear3 construction I used above is clumsy. It contains a series of constraints that look like:

  g1.e = g2.w;
  g2.e = g3.w;
The same file also has an analogous gear2 definition, for diagrams with only two gears. Having both gear2 and gear3, almost the same, is silly and wasteful. What I really want is to be able to write:

  define gears[n] open {
    gear[n] g;
    constraints {
      g[i].e = g[i+1].w;
    }
  }
and this would automatically imply components like gear2 and gear3, with 2 and 3 gears, but denoted gear[2] and gear[3], respectively; it would also imply gear[17] and gear[253] with the analogous constraints.

For gear[3], two constraints are generated: g[0].e = g[1].w, and g[1].e = g[2].w. Normally, arrays are cyclic, and a third constraint, g[2].e = g[0].w, would be generated as well. The open keyword suppresses this additional constraint.

This feature is under development now. I originally planned it to support splines, which can have any number of control points. But once again, I found that the array feature was going to be useful for many other purposes.

When the array feature is finished, the next step will be to create a spline type: spline[4] will be a spline with 4 control points; spline[7] will be a spline with 7 control points, and so on. PostScript will take care of drawing the splines for me, so that will be easy. I will also define a regular polygon type at that time:

        define polygon[n] closed {
          param number rotation = 0;
          number radius;
          point v[n], center;
          line  e[n];
          constraints {
            v[i] = center + radius * cis(rotation + i * 360/n);
            e[i].start = v[i];
            e[i].end   = v[i+1];
          }
        } 
polygon[3] will then be a rightward-pointing equilateral triangle; constraining any two of its vertices will determine the position of the third, which will be positioned automatically. Note the closed keyword, which tells linogram to include the constraint e[2].end = v[0], which would have been omitted had open been used instead.

More complete information about linogram is available in Chapter 9 of Higher-Order Perl; complete source code is available from the linogram web site.


[Other articles in category /linogram] permanent link