Copyright © 2003 Jaime Soffer
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".
Kfandango is a tridimensional CAD application coded in C/C++ and extensible by Python.
Table of Contents
Kfandango is a tridimensional CAD application coded in C/C++ and extensible by Python. Please report any problems or feature requests directly to the author.
This application is currently useless for anything remotely serious except development. Do not attempt to build bridges, skyscrappers or nuclear installations based on data output by it. Should be considered a feature-incomplete (somewhat) stable alpha version. It's very useful for squiggling, programming fractal sequences and to describe static geometry, nonetheless.
The purpose of this application, once finished, should be similar to the one of Autodesk's Autocad (TM). The basic architecture is based on it and on GNU emacs: creating an optimized core and allowing a high level language (AutoLISP or elisp) in this case Python to perform operations with its low level interfaces.
Currently what's ready are the basic primitives line, triangle, line strip and triangle strip. All of them are accesible from the external scripting, making possible to draw about anything with moderate effort.
What's lacking is the capability to modify these drawings. An entity, once drawn, will stay the way it is (can't be hidden or deleted) until the whole drawing is destroyed. The only editing feature already coded in is the ability to move an already placed point.
As for the auxiliar functions, currently it's possible to snap only to the end of a line or to an intersection.
Creates a new document
Calls the dotfile again. If the dotfile doesn't include a "clear()" command its effect will accumulate.
Saves the document (Unimplemented)
Quits Kfandango
When active, draw lines clicking the left mouse button anywhere on the GL area. A continous line will be drawn from the first point until the action is deactivated.
When active, draw triangles clicking the left mouse button anywhere on the GL area. A triangle will be drawn for each three clicks, facing toward if the clicks were done in counterclockwise order.
When active, selects the intersection between two line segments clicking the left mouse button close to them.
When active, a left mouse button click selects the nearest entity.
Extending Kfandango is a joy to behold. Just read through the next chapter to learn how!
Kfandango architecture is structured the following way. The main KApplication is a instance of a class derived of KMainWindow. It constructs a GL widget and a KLineEdit inside of a QVBox. All this is usual on a KDE application.
The most important technical difference between this and a conventional KDE application is the presence of a Python interpreter. This is implemented as a QPyObject derived from a QObject class. The constructor of this derived class runs Py_Initialize and the init* functions defined on primitives.h. The destructor, of course, runs Py_Finalize.
The Qt magic of this object works the following way: it has an slot called processExternalCommand who receives a QString instance as its argument. It simply runs PyRun_SimpleString on it. Connecting the slot of qpo, the standard Python interpreter, to a signal from any KDE object who produces a QString allows transparent Python execution.
There is one "call to the tooth fairy" in this application. It does mean that I have broke a fundamental design rule in exactly one place: the GL widget, an instance of KFandangoWidget is a global pointer. Therefore it should be dealed with special care.
The purpose of taking such a bold measure is to allow a direct participation between the Python extension and the core. Since all the functions in primitives.cc have direct access to the api using a live pointer declared extern the interfacing is very simple. For example, to create a line in the core the extension should call
gw->pushLine(x1, y1, z1, x2, y2, z2);
The disadvantage of this method is an increased risk. gw (the name of the global pointer) should be fully constructed before of qpo (the Python interpreter), since any call to Python can run a GL function. gw should be last to be destroyed, too, and this may be hard to predict; one should trust on the Qt/KDE autodeleting method. Of course any irresponsible usage of the pointer by a programmer will present undefined behavior, and by "undefined" I mean the application will most probably crash.
Here has to be placed a prototype for the new expanded function. It has the basic structure
PyObject* name(PyObject *self, PyObject *args);
where name is the internal name of the C function that will provide the funcionality.
Here is where the extension is done. Three things are required: an entry on a PyMethodDef array, the function implementation and optionally an initialization function (which has to be run on the QPyObject::QPyObject constructor) containing a Py_InitModule instance, if a new module is created.
The entry on the PyMethodDef array has the following structure:
{
"pyfunction",
name,
METH_VARARGS,
"Example of a Python extension. My hovercraft is full of eels!"
},
where the first, quoted value is the name the function will have when called in Python, the second one is the internal name as defined on primitives.hpp, the third one never needs to be changed unless keywords are added to the arguments (look at the line_from function implementation, and at the 1.8 chapter of the Python extension manual, "Keyword Parameters for Extension Functions") and the last one is a description which will be stored as modulename.pyfunction.__doc__ inside Python.
The function implementation has a specific structure. First, define the variables which will hold the parameters of the python function. If some parameter requires to be optional and with a default value define it here.
float x, y; float side = 3.f;
The following piece of code is where Python does its job. The quoted string is a code for python to understand which parameters will the function receive. Any parameter following the pipe character is defined as optional. The addresses following it are the variables where Python will save the values it receives from the function.
if (!PyArg_ParseTuple(args, "ff|f", &x, &y, &side)){
return NULL;
}
On this example the values x and y are mandatory floating point values and the parameter side is optional and defaults to 3.0; if the user inputs something that doesn't correspond to the "ff|f" string, PyArg_ParseTupleAndKeywords will return false, the function will send a NULL back to Python, it will emit an exception and everybody will be happy and safe. Of course, the call will fail.
Look in (Some appendix: TODO) for a full list of keywords to use on the coded parameters string.
The next part is the only one who will be coded as a trivial C function. Use the variables which were just filled by Python to do some processing. For example, draw an square with center x, y and side length side.
gw->newStrip(x-(side/2.0), y-(side/2.0), 0.f, 1); // begin a line strip gw->pushToStrip(x+(side/2.0), y-(side/2.0), 0.f); // line to bottom, right gw->pushToStrip(x+(side/2.0), y+(side/2.0), 0.f); // top, right gw->pushToStrip(x-(side/2.0), y+(side/2.0), 0.f); // top, left gw->pushToStrip(x-(side/2.0), y-(side/2.0), 0.f); // close it at bottom, left
Next, the return value. Here are two options: return nothing or return a value. To return nothing:
Py_INCREF(Py_None); return(Py_None);
It's very important to do Py_INCREF instead of returning directly, or Python will lose count of its references, overflow an stack and die after a few dozens of calls.
To return a value, using the same coded string system of before:
return Py_BuildValue("[fff]", x, y, side);
returns a tuple containing the input values. Be careful of selecting the right types.
Table of Contents
Drawing primitives ? Functions that create lines or triangles
idorudraw.beginLinesEntity(l);
int l;
idorudraw.beginTrianglesEntity(l);
int l;
idorudraw.line(x1, y1, x2, y2, z1, z2);
float x1;
float y1;
float x2;
float y2;
float z1;
float z2;
idorudraw.triangle(x1, y1, x2, y2, x3, y3, z1, z2, z3);
float x1;
float y1;
float x2;
float y2;
float x3;
float y3;
float z1;
float z2;
float z3;
idorudraw.line_from(x, y, z, l);
float x;
float y;
float z;
int l;
idorudraw.triangle_from(x1, y1, x2, y2, z1, z2, l);
float x1;
float y1;
float x2;
float y2;
float z1;
float z2;
int l;
idorudraw.line_to(x, y, z);
float x;
float y;
float z;
idorudraw.triangle_to(x, y, z);
float x;
float y;
float z;
The begin*Entity, line and triangle functions draw unconnected primitives.
The line_from, line_to, triangle_from and triangle_to functions draw line and triangle strips.
To draw unconnected primitives call first a begin*Entity function passing a layer index to it; then call as many related primitives (lines or triangles) as required for the entity. Call any drawing initialization function to finish the entity and start a new one.
To draw a connected primitive (a strip) call first a *_from function passing to it a point to begin a line strip, a line to begin a triangle strip, and, in both cases, the layer to which the strip will belong. To suspend a strip call any drawing initialization function.
Originally it was possible to use nested primitive initialization (for example, calling beginTrianglesEntity, then line_from, triangle and later line_to on that order) but it's currently not possible and will generate an error. This must be fixed in the future.
mousebutton ? Callback called each time a mouse button is pressed
float
fandango_input.mousebutton();
The mousebutton function does nothing by default. It is automatically called by KFandangoWidget::mousePressEvent and should be overriden by an user defined function.
Create a function to call each time a mouse button is pressed, then assignate it to fandango_input.mousebutton. The following example explains how to set the mouse to change the light position every click, and how to restore it.
### Defined on click_aux.py
def clp(l=0):
position = cadget.clicks()
lpos(position[2][0], position[2][1], position[2][2], l)
### Every click from now on will set the light position to the
### current cursor position
fandango_input.mousebutton = clp
### Restore the default click action (none)
fandango_input.mousebutton = None
Currently this function reacts to the left button clicks only; future versions will have left, middle and right button handlers.
clearcalled ? Callback called each time the drawing is cleared
fandango_input.clearcalled();
The clearcalled function does nothing by default. It is automatically called by KFandangoWidget::clear and should be overriden by an user defined function.
Define a function just as on fandango_input.mousebutton. This will be called after all the memory pages are deleted and recreated on KFandangoWidget::clear. The following example explains how to set up the application to zoom automatically after calling clear().
def rezoom():
qzoom(15)
fandango_input.mousebutton = rezoom
I don't remember what was it implemented for.
clicks ? Returns a tuple containing the last clicked points
[[fff][fff][fff]]
cadget.clicks();
Returns a [[xyz][xyz][xyz]] tuple containing the coordinates of the last three clicked points.
Assignate the return of this function to a variable, then use its contents to perform any desired operation. The following example displays how to draw a triangle from the three latest clicks.
### Defined on click_aux.py
import idorudraw; D = idorudraw
def ct(l = 1):
D.beginTrianglesEntity(l)
corners = cadget.clicks()
D.triangle( corners[0][0], corners[0][1], # x1, y1,
corners[1][0], corners[1][1], # x2, y2,
corners[2][0], corners[2][1], # x3, y3,
corners[0][2], corners[1][2], corners[2][2] ) # z1, 2, 3
The clicks are stored in order. corners[0] stores a point older than corners[1]. The initial state of the array is all zeroes.
select_entity ? Finds the closer entity to a point
int
cadget.select_entity(x, y, z);
float x;
float y;
float z;
Finds an entity given a point near to it. The return value is the internal index of the entity.
Find a point using cadget.clicks or input one manually. Call the function with it as parameter and assign the output to a variable. Use the variable as a handler to perform operations over the found entity.
### Defined on click_aux.py
# After clicking on a point call this function and the closest
# line based entity and its closest vertex will be selected
def cs():
select(1)
c = cadget.clicks()
cadget.set_selected(cadget.select_entity(c[2][0], c[2][1], c[2][2]))
set_selected ? Sets an entity as active
Selects the entity. Setting an entity as active sets its active vertex too, allowing to move it with fandangoedit.move.
Find an entity by using cadget.select_entity, then call this function to make it active.
### Defined on click_aux.py
# After clicking on a point call this function and the closest
# line based entity and its closest vertex will be selected
def cs():
select(1)
c = cadget.clicks()
cadget.set_selected(cadget.select_entity(c[2][0], c[2][1], c[2][2]))
# Will move the currently selected point to the last click
def cm():
c = cadget.clicks()
fandangoedit.move(c[2][0], c[2][1], c[2][2])
findint ? Attempts to find an intersection
glstate.findint(x, y, z);
float x;
float y;
float z;
[fff]
glstate.getint();
glstate.getint returns a [xyz] tuple containing the coordinates of the intersection between two lines. glstate.findint should have been called before with a point as input, which should be close to the expected intersection. If it is not, findint may either do nothing or find an undefined intersection somewhere else.
getint may be called as much times as needed, and its return will only change after a sucessful call to findint.
Retrieve a point by using cadget.clicks and pass its coordinates. Receive the resulting point on a variable. The following example is a crude attempt of drawing lines between intersection snaps.
### Get the first point (assumes the screen is filled with ### intersecting lines). Click near an intersection before calling ### the following: c = cadget.clicks() findint(c[2][0], c[2][1], c[2][2]) c = getint() line_from(c[0], c[1], c[2]) ### Continue the strip, clicking near to intersections before calling ### the following. c = cadget.clicks() findint(c[2][0], c[2][1], c[2][2]) c = getint() line_to(c[0], c[1], c[2]) ### The whole process can be trivially automated (see, for example, ### cint() in fandango.py).
This function should return the found intersection's coordinates, but doesn't. To retrieve them glstate.getint should be called. This functionality is expected to improve.
axis ? Determines the rotation axis of the view
Sets a rotation axis with origin in the center of the physical widget and pointing to center+[x,y,z]. Holding the left mouse button and pulling up and down rotates the view around this axis.
There are currently two rotation modes, to be set with glstate.rot_style(); passing to it 1 selects axial rotation and this function will set the axis. Passing to it 0 changes to a more free style rotation, and the selected axis will be ignored.
reset ? Resets all the rotation angles
glstate.reset();
The rotation angle of the view can be changed by holding the left mouse button and pulling around. This function resets the angles to zero.
Calling this function resets the view simultaneously for both rotation styles.
zoom ? Sets the values of the viewport's borders
glstate.zoom(l, t, b);
float l;
float t;
float b;
The GL widget viewport has two metrics, one in pixels and another in GL units. This function allows to match the GL metrics with the real screen pixels.
Setting the GL value who should be exactly on the left, top and bottom pixels, regardless of the resizing of the widget, allows full flexibility for the geometry. The right side value is automatically calculated on the call to this function based on the width/height ratio.
Resizing the widget causes the following effect: vertical resizing changes the scale of the view preserving the top and bottom values, and horizontal resizing crops the view by the sides, preserving the center.
This is mostly a low level function, and should be used to build upon it more user friendly ones. The basical usage is to determine first the top and bottom limits of the required viewport, then either calculate the center and substract enough value to determine the left side or simply give a good guess.
The following example is the actual implementation of qzoom, which resolves the border values for a view of exactly n*2 units of height with center on a given point.
### Defined on fandango.py
def qzoom(n, x = 0.0, y = 0.0):
zoom((-n * cadget.ratio())+x, n+y, -n+y)
cadget.ratio determines the current ratio of the widget; it simply is substracted from the center. The other values are passed literally.
It may be useful to implement a mode to allow width-resizing to be pinned on a border instead of the center.
color, clearcolor, lcolor ? Color manipulation functions
glstate.color(r, g, b, layer, a);
float r;
float g;
float b;
int layer;
float a;
glstate.clearcolor(r, g, b);
float r;
float g;
float b;
glstate.lcolor(r, g, b, light, a);
float r;
float g;
float b;
int light;
float a;
color defines the color of all the entities on a determined layer.
clearcolor determines the color of the background.
lcolor sets the color of a determined light.
Any time a drawing entity is created, be it by line_from, triangle_from, beginLineEntity or beginTrianglesEntity, a layer must be assignated to them. If it is not, they belong to layer 1.
Calling color with a determined layer as argument changes the color of all the entities belonging to that layer.
### From my personal $(HOME)/.fandango
def emacs_colors():
clearcolor(0.1843, 0.3098, 0.3098)
lcolor(0.92, 0.95, 1.000000, 0)
color(0.9607, 0.8705, 0.7019, 1, 0.5) ## Translucent
color(1, 0.4983, 0.1411, 2)
color(0.5294, 0.8078, 0.9803, 3)
color(0, 1, 1, 4)
color(0.8549, 0.4392, 0.8392, 255, 0.5) ## Crosshair; translucent
emacs_colors()
Crosshair
It was the in-widget text color; unused
Hilighted items
Default layer
The only active light on the default compilation is light number 0. To activate more, edit KFandangoWidget::initializeGL and duplicate the lines calling setLight and initLight with numbers between 1 and MAX_LIGHTS-1. Recompile.
Employing the last layers to apply colors to UI elements is a mess. Modifying the NUM_LAYERS constant would break a few things.
lpos ? Moves a light
The only active light on the default compilation is light number 0. To activate more, edit KFandangoWidget::initializeGL and duplicate the lines calling setLight and initLight with numbers between 1 and MAX_LIGHTS-1. Recompile.
glEnable/Disable wrappers ? Functions that activate or deactivate GL features
glstate.aa(b);
bool b;
glstate.blend(b);
bool b;
glstate.texture(b);
bool b;
glstate.light(b);
bool b;
glstate.cull(b);
bool b;
getsnap, getsnapvertex ? Finds a close endline vertex
bool
glstate.getsnap(x, y, z);
bool x;
bool y;
bool z;
[fff]
glstate.getsnapvertex();
glstate.getsnap simply finds the nearest line end to the given point. If a line end could not be found the return value is False. The found vertex should be retrieved with glstate.getsnapvertex.
Retrieve a vertex using cadget.clicks. Pass that vertex to glstate.getsnap, and check the return value. If True, use glstate.getsnapvertex to retrieve the ENDline snap.
The names are confuse and must be changed.
snap_ratio ? Modifies the maximum distance to the cursor when finding a snap
The functions who attempt to find an endline vertex or an intersection are too expensive to call them over the full drawing, and some algorithms try to determine if the input point is "close enough" to an entity before performing the test.
snap_ratio modifies the "close" distance. A value too small (1, 2...) requires a very precise input (and is probably faster), and a large value (10, 20, 800...) is more forgiving but requires more time to process. The default value is 5.
The function used to find snaps is not perfectly bounded. Even if an input is not close to a snap that doesn't mean one will not be found, probably not the expected one. It will be needed to check the (currenty unimplemented) return value of the find* function.
write ? Pipes some text to the mock stdout
The conventional Python interpreter sends the messages printed by print to the console. Since this is a GUI application such approach would be cumbersome.
This function replaces the standard output of Python and pipes the text through a Qt signal, making it accesible to all the widgets.
On the current implementation anything printed by Python will be displayed on the status bar.
This function should not be used directly.
The correct way to use it is simply to replace the builtin sys.stdout method. This is already performed on fandango.py and most probably will not need to be modified.
### From fandango.py import fandango_stdout sys.stdout = fandango_stdout
On the current implementation stderr is not being handled. It would be trivial to use the same method for it too; but Python sends its errors on several lines and many are empty, making this approach almost useless.
move ? Replaces the position of the active vertex
fandangoedit.move(x, y, z);
float x;
float y;
float z;
If select mode is on, in a selected entity there is a selected vertex, which is marked by a small yellow square.
To select the entity fandangoedit.move should have been called, and a vertex close enough to the arguments gets selected.
Calling move replaces the position of this selected vertex.
This section is dedicated to the inner workings of the application. It's almost useless for anybody not interested on improving the application's core.
Table of Contents
pushPoint ? Adds a point to the memory page list who calls it.
void
iMem::pushPoint(x, y, z);
float x;
float y;
float z;
pushPoint is the lowest level function on the Kfandango API. It adds a point to the memory structure stored by the head of a iMem linked list.
The actual effect of this function variates depending on the array_type variable on the linked list.
pushPoint should not be used directly. It is instead called from newStrip, newTriStrip, pushLine, pushTriangle, pushToStrip and pushToTriStrip on the KFandangoWidget class.
Its usage adds a point to a memory array apt to be passed to glDrawArrays. The actual effect of the call is derived from the type of array passed as defined on the initialization of a iMem constructor.
The possible values and their effect are listed next.
GL_LINES: The odd indexed points mark the beginning of lines, the even indexed ones complete them.
GL_TRIANGLES: Each point defines a vertex of a triangle. For each three points a triangle is drawn. The front face of the triangle is defined by the winding order of the points who form it.
GL_LINE_STRIP: The first time the function is called after defining a new entity it marks a beginning point. Each time after it a line is drawn between the last point and the current.
GL_TRIANGLE_STRIP: As the GL_LINE_STRIP case, but requires two calls to be initialized. Each call after these draws a triangle between the two latest calls and the current. The front of the strip is determined by the winding order of the first three calls.
In all the cases creating a new entity by a call of pushEntity resets the counts and interrupts any strip (if it applies).
Since the correct amount of calls variates between array types, it's important that the functions which call it do so the right amount of times. The functions that initialize strips and those which complete them call it once. The function that draws a line calls it twice, and the one that draws a triangle calls it three times. A wrong count could result in unsynchronization of the memory page characterized by 3d drawing artifacts (geometry appearing where it should not, lines joining unrelated points, etc).
pushEntity ? Begins a drawing entity from the given data.
void
entityMarshall::pushEntity
(type, data, normals, texture, layer);
glEnum type;
float* data;
float* normals;
float* texture;
unsigned int layer;
One of GL_LINES, GL_TRIANGLES, GL_LINE_STRIP or GL_TRIANGLE_STRIP.
A pointer to a memory array containing the vertex of the entity to initialize.
A pointer to the already calculated normal values for this entity. Should be NULL for unlit entities (line based).
A pointer to the UV texture coordinates for this entity. NULL for line based entities.
The index of a layercard structure to define the color and other properties of this entity (barely implemented).
The pushEntity function should not be used directly, and the values passed to it should not be manually generated either.
This function is currently called by an entity marshall in the iMem::beginEntity function, which is called only by iMem::pushPoint.
What this function actually does it to set the input values to an entry on a entityCard struct entry. The entity will be retrieved from there to be rendered.
Kfandango
Program copyright 2002, 2003 Jaime Soffer <jsoffer@(this site)>
Contributors:
Konqui the KDE Dragon <konqui@kde.org>
Tux the Linux Penguin <tux@linux.org>
Documentation copyright 2003 Jaime Soffer <jsoffer@(this site)>
This documentation is licensed under the terms of the GNU Free Documentation License.
This program is licensed under the terms of the GNU General Public License.
Kfandango is not part of the KDE project; but can be found in the CVS archive and files section of the sourceforge project elcad (oddly enough).
In order to successfully use Kfandango, you need KDE 3.x with support for OpenGL compiled in QT. It also worked correctly on KDE 2.2 and probably can be built for this older version replacing the admin directory and recreating the configure script.
Python 2.x is required.
A 3D video accelerator is highly recommended.
Kfandango uses about 10 megs of memory to begin to run, and will increase its size to hold the drawing entities.
Links to all the required libraries can be found on The Kfandango home page.
In order to compile and install Kfandango on your system, type the following in the base directory of the distribution:
% ./configure % make % make install
Since Kfandango uses autoconf and automake you should have not trouble compiling it. Should you run into problems please report them directly to the author.