Task139 Editing
Path needs to include control points along with the standard point to point API.
I once wrote a QuartzPath IDL file, based on the Quartz 2D API from Apple, http://developer.apple.com/techpubs/macosx/CoreTechnologies/graphics/Quartz2D/Quartz_2D_Ref/qref_main/index.html I'm attaching Quartz.idl. Anyone looking into an API for complex paths may want to look at this.
I'm, uh, *attaching* Quartz.idl.
The attached idl looks like a very good start. But some things remain a bit unclear (at least to me): what exactly is a path ? How much 'style' and similar state is associated with it ? What attributes belong to a path, what belong to the DK ? I'm still not convinced Path has to be an interface. It could well be a struct (yeah, it's a bit ugly, but surely much more efficient).
Hey Nick, update your status here :)
Patch. Breaks ToolKit Diamond and Gauge, as well as daVinci and the LogoDemo. Moves most of Fresco from old Path structure to new Path API. The LibArtDK and PSDK are known to work, the GLDK is still in question.
Attaching screenshot from GLDK.
Attaching screenshot from PSDK.
neiljp was following along and astutely noticed dots in the GLDK image. Those have been fixed (off by one error with the GL evaluators.) Also, he demanded to see curved paths in the LADK.
Stefan points out that the patch is missing PathImpl.cc and PathImpl.hh. Zut!
Ok, attaching new patch. This includes all sorts of good stuff! Everything but a fixed daVinci, basically. Please test, comment on, review, etc.
ok, here is a quite different 'path' data structure. It's all a bit rough, but it should be enough to transmit the general idea: A path is a sequence of 'segments', each of a specific type. Segments are then implemented by means of (CORBA) unions, so their exact substructure can depend on the type. That means the data layout can be optimal for the given type. No need to cary two control points per vertex if this is going to be used as a polygon, etc... Here you go: struct QuadricCurve { // whatever it takes... }; struct BezierCurve { unsigned long order; sequence<Vertex> controls; }; struct NurbsCurve { unsigned long order; sequence<Vertex> controls; sequence<double> weights; sequence<double> knots; }; typedef sequence<Vertex> PolygonCurve; enum PathSegmentType { polygon, quadric, bezier, nurbs}; union PathSegment switch (PathSegmentType) { case polygon: PolygonCurve polygon; case quadric: QuadricCurve quadric; case bezier: BezierCurve bezier; case nurbs: NurbsCurve nurbs; }; struct Path { sequence<PathSegment> segments; boolean closed; } The 'Path' type itself in this example is a struct, but it could well be an interface if that's *really* necessary. If it is an interface, we can endow it with a convenient interface to add segments intuitively, if it's a struct we have to provide convenience *functions* to all sides (server and client runtimes). Also, using an interface would provide us with the means to control modifiability, and thus cacheability. Food for thought...
sorry, didn't mean to steal the task from you (nicholas). Just wanted to add me. Hmm, will have to do it over the cli.
ok, I don't mind using the union. But your Path struct implies a couple very important differences from my interface. You're implying random access to any path segment. In my Path, once you add a segment, you can't rewind and replace it, or change what's already been written. Secondly, you're making it thoroughly easy to create paths that can't be rendered by postscript or libart by allowing an arbitrary number of control points to exist in BezierCurve.
stefan, wasn't the idea here to allow us to save memory storage in the PathImpl? Could you convince me that your complex IDL types are going to be more memory efficient than: enum PathSegmentType { move, line, conic, cubic }; struct PathSegment { Vertex to, cp1, cp2; PathSegmentType type; }; in straight C++ (not burdened with CORBA anything)?
Nick Lewycky wrote: > Nick Lewycky <nicholas@fresco.org> added the comment: > > stefan, wasn't the idea here to allow us to save > memory storage in the PathImpl? Could you convince > me that your complex IDL types are going to be > more memory efficient than: > > enum PathSegmentType { move, line, conic, cubic }; > struct PathSegment > { > Vertex to, cp1, cp2; > PathSegmentType type; > }; > > in straight C++ (not burdened with CORBA anything)? > > ________________________________________ > Fresco issue tracker <issues@fresco.org> > http://issues.fresco.org/task139 > ________________________________________ > > Two things: * my suggested path is more flexible than yours, as it allows more than one curve type, so there is more than memory-efficiency * the path idl as I suggested it doesn't involve *any* CORBA, it is mapped to straight C++ Please consider the extreme (but most frequent) cases, where you have a path with a single segment which is a 'polyline'. The data you'll effectively see is: +------------+ | Path | |+----------+| || Polyline || ||+--------+|| |||Vertices||| ||| ||| ||+--------+|| |+----------+| | closed | +------------+ i.e. there isn't a single control point. In your case you'd carry 2n redundant vertices with you for a polyline consisting of n vertices, and if we would generalize your case to support nurbs you'd also get weights and knots that wouldn't serve anything. So I'm trying to make the data types as specific as possible for a given curve type, and then use a union to get the necessary flexibility. Also (in reply to your other message), the (de)composition I show merely reflects the conceptual separation of items that make up a path. I'm not suggesting that we have to keep the Path API as transparent as that. I can see your point of wanting to restrict access in some way or another (though I can see cases where I want to modify vertices or other data in place). Let's first iron out the underlying path *model*, and then refine the API to access and modify it, ok ?
Taking Stefan's design ideas into account, I'm attaching the latest Path.idl.
Nicholas: I wanted to make clear which 'path' we are talking about: FigureKit generates a Path which is a Graphic. I'm *not* talking about that. I.e. I'm talking about the low level Path that is exchanged between Graphics and DrawingKits. For this I don't see much need for convenience. But a lot of need for flexibility and speed. Read: I expect users to use the FigureKit graphic that let them (interactively, may be) manipulate shapes (i.e. paths). But the path we are talking here about is basically a data structure to be used to actually draw. Do we agree on that ?
You want one interface for the FigureKit's Path and one for the Path that the DrawingKit draw_path function takes? I'm not sure I can disagree more. :-) I wanted the clients to get a Path_var from the FigureKit, and a Very Dumb wrapper Graphic which takes the Path_var from the client and calls DrawingKit draw_path with it when traversed. Part of the reason is to keep only one true Path API around for consistency and clarity. If you don't value that, then we can break it into two pieces, one of which is a secret between the FigureKit and the DrawingKit. Oh, and any other client-side Graphics who might care, but we can safely ignore them by saying "don't do that then" in the documentation. Actually, your PathSegment in a struct makes a whole lot more sense when considered as part of approach. :-) One thing we should keep in mind is that some DrawingKits will have accelerated draw_rectangle implementations which wouldn't be accelerated in draw_path. The LibArtDrawingKit is currently such an example. We could work around this by allowing a new PathSegment of type Rectangle so that the backend could Do The Right Thing. Stefan, if this seperation is what you intended, one Figure::Path and one Fresco::Path, then I'll happily use your Path internally and expose only my original API from Figure::Path. Does that satisfy both of our design requirements?
Nick Lewycky wrote: > You want one interface for the FigureKit's Path and one for the Path > that the DrawingKit draw_path function takes? well, 'interface' in an abstract sense... > I'm not sure I can disagree more. :-) I wanted the clients to get a > Path_var from the FigureKit, and a Very Dumb wrapper Graphic which takes > the Path_var from the client and calls DrawingKit draw_path with it when > traversed. Part of the reason is to keep only one true Path API around > for consistency and clarity. I still think that there are conceptually two things, which may or may not be lumped into a single interface: There is a low level 'war' path that is handed over to the DK for rendering. Assuming that the DK has only a single method to draw paths (polylines, curves, all through the same call), it means the path passed as argument needs to be pretty versatile, and easy/fast to access. This means we should do everything we can to avoid unnecessary dynamic memory allocations (such as used in method calls a la PathSegment get_segment(in long i) ... That's the kind of thing which kills performance... (One reason I'm interested into this path API is that I'd like to understand how we can get CORBA *out* of the scene graph, i.e. make the CORBA exposed interface (i.e. the calls that are routed through the ORB) more granular.) And I think that Path is a good topic to study this. > If you don't value that, then we can break it into two pieces, one of > which is a secret between the FigureKit and the DrawingKit. Oh, and any > other client-side Graphics who might care, but we can safely ignore them > by saying "don't do that then" in the documentation. Well, basically all graphics that render use paths (images are the only exception). Though the vast majority will render simple things such as rectangles or polygons. For example the 'beveled frame' (WidgetKit / ToolKit) will store a path internally and pass it down to the DK when being drawn. I don't mind the implementer of a Bevel to mess with path internals, if that is what we need to be fast. And even then we could provide non-corba functions (inside libBerlin) as convenience... Not so the FigureKit graphic that exposes a Path API: there I expect all those nice methods like 'add_circle_segment', 'add_bezier', 'add_polyline', etc. which *application* code will hook into (such as daVinci). > Actually, your PathSegment in a struct makes a whole lot more sense when > considered as part of approach. :-) I hope so :-) > One thing we should keep in mind is > that some DrawingKits will have accelerated draw_rectangle > implementations which wouldn't be accelerated in draw_path. The > LibArtDrawingKit is currently such an example. We could work around this > by allowing a new PathSegment of type Rectangle so that the backend > could Do The Right Thing. While I agree in general (that's why I still suggest a bezier segment, even though it could be entirely represented as a nurbs segment), I think that it is simple enough for renderers to detect whether a polyline is a rectangle. Also don't forget that whether or not it is a rectangle depends on the accumulated trafo, too... > Stefan, if this seperation is what you intended, one Figure::Path and > one Fresco::Path, then I'll happily use your Path internally and expose > only my original API from Figure::Path. Does that satisfy both of our > design requirements? yes, I believe we are getting closer :-)
Stefan Seefeld wrote: > This means we should do everything we can to avoid unnecessary dynamic > memory allocations (such as used in method calls a la > > PathSegment get_segment(in long i) > > ... > That's the kind of thing which kills performance... Oh is that what you're worried about. Well, if it helps, the CORBAized version of the Java Shape interface[1] I modelled this after would actually be: PathSegmentType currentSegment(out Vertices); Where what they actually use in Java is a double[6] or float[6]. For the DrawingKit, this would mean only one dynamic allocation of the Vertices with a length of three per draw_path API call, not per path segment. And if that's still too nasty for you, we could allocate that Vertices *once* when the DrawingKit is cloned and use it evermore. Well, actually, we'd have a problem with your arbitrary length NURBS, wouldn't we, not fitting in Vertices.length(3). And doubtless you'll complain again about the two wasted spaces when it's only a line segment and not using the control points. I suppose then what we would do is create the PathSegment in the add_* call, and return a reference to that when get_segment is called. We can even allow them to modify the Path through the reference we returned. void get_segment(out PathSegment ps); should be appropriate, yes? [1] - http://java.sun.com/j2se/1.4.1/docs/api/java/awt/geom/PathIterator.html > (One reason I'm interested into this path API is that I'd like to understand > how we can get CORBA *out* of the scene graph, i.e. make the CORBA exposed > interface (i.e. the calls that are routed through the ORB) more granular.) > And I think that Path is a good topic to study this. The granularity isn't the issue I think we should be focusing on, it's the *usability*. Especially for programmers with little or no CORBA experience. My opinion is, the CORBA sequence interface is bad and I don't want to force client authors to use it any more than I must. >>If you don't value that, then we can break it into two pieces, one of >>which is a secret between the FigureKit and the DrawingKit. Oh, and any >>other client-side Graphics who might care, but we can safely ignore them >>by saying "don't do that then" in the documentation. > > Well, basically all graphics that render use paths (images are the only exception). > Though the vast majority will render simple things such as rectangles or polygons. > For example the 'beveled frame' (WidgetKit / ToolKit) will store a path internally > and pass it down to the DK when being drawn. I don't mind the implementer of > a Bevel to mess with path internals, if that is what we need to be fast. And even > then we could provide non-corba functions (inside libBerlin) as convenience... > > Not so the FigureKit graphic that exposes a Path API: there I expect all those > nice methods like 'add_circle_segment', 'add_bezier', 'add_polyline', etc. > which *application* code will hook into (such as daVinci). No offense, but I'd rather you didn't try to spoil the architecture here by making this Path the one and only place where we don't use CORBA pervasively. If you want to decree that we should start removing CORBA between the scene graph and renderer, then you can do that later. Please don't start hacking up an otherwise perfectly good design. This curved path work is absolutely essential for Fresco to be used seriously as a GUI toolkit. It is not a toy for you to try alternate approaches and architectures. The good news is that if we do decide to start such work, we can do so easily since both C++ calls and cross-CORBA calls look the same in terms of the code syntax. That said, if I'm not mistaken, my Beveller implementation already does use direct C++ calls: PathImpl path = new PathImpl(); path->add_lines(v); is not a cross-CORBA call. Only when I call "drawing->draw_path( path->_this());" does CORBA get involved, which is exactly what our design currently mandates. How is this different from your suggested "non-corba [convenience] functions (inside libBerlin)"? >>One thing we should keep in mind is >>that some DrawingKits will have accelerated draw_rectangle >>implementations which wouldn't be accelerated in draw_path. The >>LibArtDrawingKit is currently such an example. We could work around this >>by allowing a new PathSegment of type Rectangle so that the backend >>could Do The Right Thing. > > While I agree in general (that's why I still suggest a bezier segment, even though > it could be entirely represented as a nurbs segment), I think that it is simple > enough for renderers to detect whether a polyline is a rectangle. Also don't > forget that whether or not it is a rectangle depends on the accumulated trafo, too... The DrawingKit will have to check that itself (and LibArtDrawingKit::draw_rectangle() already does). The point is that we'd add a Rectangle segment type instead of adding four lines.
as you'v certainly noted, I'v checked in some nurbs code: it's basically a set of headers. The actual nurbs code is parametrized for the Point type and the number of parameters. A special 'Vertex.hh' header is provided adding the needed API so all this can be instantiated for fresco's Vertex type. the test/nurbs/ directory contains five examples demonstrating its use. I checked in a Makefile.in, which should be easy to either integrate into the build system, or manually derive a Makefile from. 'make' will build five examples showing what can be done with nurbs. I'll try to work further on a Path API, so we can come back to that discussion, too...
here's some followup on my meddling with nurbs: I'v defined the following Path IDL: typedef sequence<Vertex> Polyline; struct Nurbs { unsigned short degree; sequence<Vertex> controls; sequence<double> weights; sequence<double> knots; }; enum PathSegmentType { polyline_t, nurbs_t}; union PathSegment switch (PathSegmentType) { case polyline_t: Polyline polyline_a; case nurbs_t: Nurbs nurbs_a; }; typedef sequence<PathSegment> Path; Based on this, the code to define a path looks so: Path_var path = new Path; path->length(4); { Polyline lines; //... set vertices ... path[0].polyline_a(lines); } { Nurbs nurbs; nurbs.degree = 3; nurbs.controls.length(10); //... set control points ... nurbs.weights.length(10); //... set weights ... nurbs.knots.length(10 + 3 + 1); //... set knots ... path[1].nurbs_a(nurbs); } { Polyline lines; // and again ... path[2].polyline_a(lines); } { Nurbs nurbs; //... and again ... path[3].nurbs_a(nurbs); } which doesn't look that bad (IMO). All these calls have to be made eventually, and there are basically only two methods to access the sequences: 'length()' and 'operator []'. How can you get simpler than that ? Also note that the real work is the definition of the individual segments. In this example I picked the two extreme cases: a Polyline is the simplest path you can concieve, and the nurbs is the most complex (read: parameter-rich). So if you would make Path an interface, you'd basically have to replace 'operator []' by real accessor/manipulator methods (at least an attribute). As we discussed, there are arguments pro interface, or better, pro object. These are related to possibly adding a 'cachable' interface, so we can track when the path was last modified. But just to be able to identify the path we could probably use a simple hash. The real reason why I'm very reluctant to make Path an interface is that the Path is one of the most fine-grained types (beside basic types) we (will) use. And as I'm generally tending to think we should push CORBA out of the APIs, I think this is the level to start with. Location transparency is good for things such as models, callbacks, and to a certain extend graphics (though that's debatable), access to the inner structure of a path should be completely local. CORBA has 'value types' offering an object model that is quite a bit more lightweight than that associated with 'CORBA objects', but it has other drawbacks. Not sure I'd like to start using yet another CORBA feature... --- Some more arguments to the discussion related to the CS, i.e. the connection between adjacent segments: PS uses a number of 'segment types' (to speak in fresco jargon): lines, arcs, curves. the definition of the 'arc' operator is such that it implicitely constructs a line from the current point to the starting point of the arc, thus breaking the simple 'moveto'/'lineto' paradigm you seem so fond of. Further, there is a 'lineto' and a 'rlineto', where 'rlineto' uses relative coordinates, so you can effectively append a line using the CS of the current point. If we decide to use a similar approach, we could stipulate three possible 'connection methods': * a new segment is added 'relatively' to the current point, i.e. make the starting point of the new segment coincide with the ending point of the last segment) (which seems to me semantically equivalent to 'rlineto') * use an 'absolute CS' but implicitely connect the adjacent end points ('lineto') * don't connect the segments ('moveto') My impression is that postscript supports all three ways, so I don't see us 'breaking the rules' in any major way. Oh, and I can see all three connection methods being useful, so I'd suggest that we add the ability to define that (per segment) to the path. here's some followup on my meddling with nurbs: I'v defined the following Path IDL: typedef sequence<Vertex> Polyline; struct Nurbs { unsigned short degree; sequence<Vertex> controls; sequence<double> weights; sequence<double> knots; }; enum PathSegmentType { polyline_t, nurbs_t}; union PathSegment switch (PathSegmentType) { case polyline_t: Polyline polyline_a; case nurbs_t: Nurbs nurbs_a; }; typedef sequence<PathSegment> Path; Based on this, the code to define a path looks so: Path_var path = new Path; path->length(4); { Polyline lines; //... set vertices ... path[0].polyline_a(lines); } { Nurbs nurbs; nurbs.degree = 3; nurbs.controls.length(10); //... set control points ... nurbs.weights.length(10); //... set weights ... nurbs.knots.length(10 + 3 + 1); //... set knots ... path[1].nurbs_a(nurbs); } { Polyline lines; // and again ... path[2].polyline_a(lines); } { Nurbs nurbs; //... and again ... path[3].nurbs_a(nurbs); } which doesn't look that bad (IMO). All these calls have to be made eventually, and there are basically only two methods to access the sequences: 'length()' and 'operator []'. How can you get simpler than that ? Also note that the real work is the definition of the individual segments. In this example I picked the two extreme cases: a Polyline is the simplest path you can concieve, and the nurbs is the most complex (read: parameter-rich). So if you would make Path an interface, you'd basically have to replace 'operator []' by real accessor/manipulator methods (at least an attribute). As we discussed, there are arguments pro interface, or better, pro object. These are related to possibly adding a 'cachable' interface, so we can track when the path was last modified. But just to be able to identify the path we could probably use a simple hash. The real reason why I'm very reluctant to make Path an interface is that the Path is one of the most fine-grained types (beside basic types) we (will) use. And as I'm generally tending to think we should push CORBA out of the APIs, I think this is the level to start with. Location transparency is good for things such as models, callbacks, and to a certain extend graphics (though that's debatable), access to the inner structure of a path should be completely local. CORBA has 'value types' offering an object model that is quite a bit more lightweight than that associated with 'CORBA objects', but it has other drawbacks. Not sure I'd like to start using yet another CORBA feature... --- Some more arguments to the discussion related to the CS, i.e. the connection between adjacent segments: PS uses a number of 'segment types' (to speak in fresco jargon): lines, arcs, curves. the definition of the 'arc' operator is such that it implicitely constructs a line from the current point to the starting point of the arc, thus breaking the simple 'moveto'/'lineto' paradigm you seem so fond of. Further, there is a 'lineto' and a 'rlineto', where 'rlineto' uses relative coordinates, so you can effectively append a line using the CS of the current point. If we decide to use a similar approach, we could stipulate three possible 'connection methods': * a new segment is added 'relatively' to the current point, i.e. make the starting point of the new segment coincide with the ending point of the last segment) (which seems to me semantically equivalent to 'rlineto') * use an 'absolute CS' but implicitely connect the adjacent end points ('lineto') * don't connect the segments ('moveto') My impression is that postscript supports all three ways, so I don't see us 'breaking the rules' in any major way. Oh, and I can see all three connection methods being useful, so I'd suggest that we add the ability to define that (per segment) to the path.
I committed some further refinements to the nurbs API, and a demo showing how to align path segments relative to each other. Of course, I'm using *my* definition of 'segment' here, as I try to show how I imagine the Path API to look like. I use functions nurbs::evaluate_at() to compute the first and last point of a nurbs curve so I can compute a translation vector with it. Using that, I draw a polyline, a nurbs, another polyline, and another nurbs such that they are connected. That works beautifully. As I suggested, two other 'connection styles' are imaginable, both using absolute positioning (well, within the current CS of course): a disconnected (i.e. with a logical 'moveto'), and a connected (i.e. with a logical 'lineto'). We may offer all three by adding an enum to the segment that specifies how to connect to the next segment. If there is no next segment, this may just express whether or not the path is to be closed. Any thoughts ?
Ok, here is a slightly refined Path API, as exposed through IDL: typedef sequence<Vertex> Polyline; struct Quadric {/*...*/}; struct Bezier { unsigned short order; sequence<Vertex> controls; }; struct Nurbs { unsigned short order; sequence<Vertex> controls; sequence<double> weights; sequence<double> knots; }; enum PathSegmentType { polylinetype, arctype, beziertype, nurbstype}; union PathSegmentContent switch (PathSegmentType) { case polylinetype: Polyline lines; case arctype: Quadric arc; case beziertype: Bezier bezier; case nurbstype: Nurbs nurbs; }; //. moveto: the next segment's CS is the same as this, //. end points are not connected //. //. lineto: the next segment's CS is the same as this, //. end points are connected by a line //. //. align: the next segment's CS is this plus a translation //. that aligns the last point of this segment with //. the first point of the next segment enum PathSegmentConnection { moveto, lineto, align}; struct PathSegment { PathSegmentContent segment; //. connection defines the connection to the next //. segment. If this is the last one, moveto implies //. an open path, lineto a closed path. Align is //. undefined... PathSegmentConnection connection; }; typedef sequence<PathSegment> Path;