Simple and fast high quality antialiased lines with OpenGL

The standard way

glEnable(GL_LINE_SMOOTH);
glEnable(GL_BLEND);
glDepthMask(false);
glLineWidth(lw);
glDrawArrays(GL_LINE_STRIP, offset, count);
  1. Enable line smoothing by calling glEnable(GL_LINE_SMOOTH). In hardware it works by adjusting pixels’ alpha values. That’s why you have to do the next step.
  2. Enable blending with glEnable(GL_BLEND);
  3. Disable writing to the depth buffer with glDepthMask(false). When rendering lines, especially thick ones, one pixel can be drawn several times with different alpha and z values. And sometimes a more dark pixel may appear below a lighter pixel because of the Z fighting. And if the GL_DEPTHTEST is enabled (usually you want to have it be enabled) the dark pixel won’t be drawn and the final image will have a visible gap in this place. That’s why you have to disable writing to the depth buffer.
  4. Set the line width by calling glLineWidth(lw).
  5. Draw the lines.

Problems with the standard way

If you follow these steps, the quality of AA lines at least on NVidia is pretty decent, but there are some problems.

  1. The quality depends on the hardware implementation. So it might be good on NVidia, bad on Intel, you never know.
  2. Poor performance on latest NVidia GeForce cards. It might be a marketing trick to make people buy professional Quadro cards, but it is how it is, the time of rendering antialiased lines with NVidia GeForce OpenGL is dramatically longer (50-100 times maybe) than one of the alised lines.

The cool shader way

There are plenty of ways to draw lines with OpenGL. One popular approach is to draw degenerate quad strips instead of line strips, dynamically thicken them in the vertex shader and smooth in the fragment shader (e.g. http://jcgt.org/published/0002/02/08/). It’s a very flexible approach but not very simple and it requires considerably more memory for the vertex and attribute buffers compared to the standard way of drawing lines.

But there is a much simpler way. I borrowed the idea from this article http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter22.html and simplified it even further so almost nothing has to be changed in the client OpenGL code. The idea is to draw ‘fat’ aliased lines and filter them in the fragment shader to produce nice antialiased lines.

Here is the fragment shader code

uniform float uLineWidth;
uniform vec4 uColor;
uniform float uBlendFactor; //1.5..2.5
varying vec2 vLineCenter;
void main(void)
{
      vec4 col = uColor;        
      double d = length(vLineCenter-gl_FragCoord.xy);
      double w = uLineWidth;
      if (d>w)
        col.w = 0;
      else
        col.w *= pow(float((w-d)/w), uBlendFactor);
      gl_FragColor = col;
};

and the vertex shader code

uniform vec2 uViewPort; //Width and Height of the viewport
varying vec2 vLineCenter;
void main(void)
{
  vec4 pp = gl_ModelViewProjectionMatrix * gl_Vertex;
  gl_Position = pp;
  vec2 vp = uViewPort;
  vLineCenter = 0.5*(pp.xy + vec2(1, 1))*vp;
};

Filtering is made in the fragment shader by adjusting the fragment’s alpha value based on the distance to the drawn line. To calculate the distance from a fragment to the line I introduce the interpolated line center point attribute vLineCenter. I calculate it in the vertex shader by simply transforming the normalized projected vertex position pp to the viewport space. The rest of the work does the hardware rasterizer by interpolating vLineCenter between the first and the second vertexes of the line the same way it interpolates other attributes, like color (see gradient lines for example).

To calculate the fragment’s alpha value I just compare the  fragment’s distance to the line d with the current linewidth uLineWidth and apply the power function to the normalized difference between those two to achieve desired blurriness of the line.

Results

The rendering performance on my NVidia GTX 650 is on par with the performance of rendering simple aliased lines (100x speedup over the standard GeForce antialiasing). The quality of the image is decent too. No gaps, no jags, smooth lines in the range of 0.5..10 pixels width.

image00

Simple and fast high quality antialiased lines with OpenGL

Data types and structures for Simple CSS-like styles

unit CSSTypes;

interface

uses
  System.Types, System.UITypes;

type

  TControlState =
    (
      csDF,
      csDS,
      csSL,
      csHL,
      csSLHL,
      csOwnerHL,
      csOwnerHL_SL,
      csOwnerSL,
      csOwnerSL_SL
    );

  TStyleAttributeType =
    (
      saVisualColor,
      saVisualAlpha,
      saVisualMultAlpha,
      saBoxPaddingLeft,
      saBoxPaddingRight,
      saBoxPaddingTop,
      saBoxPaddingBottom,
      saBoxMarginLeft,
      saBoxMarginRight,
      saBoxMarginTop,
      saBoxMarginBottom,
      saBoxHAlign,
      saBoxVAlign,
      saBoxHAutoSize,
      saBoxVAutoSize,
      saBoxMinWidth,
      saBoxMinHeight,
      saBoxMaxWidth,
      saBoxMaxHeight,
      saLayoutType,
      saLayoutResizeChildren,
      saLayoutFitLastChild,
      saFontName,
      saFontStyle,
      saFontUnderline,
      saFontSize,
      saFontWhite,
      saFrameName,
      saFrameBorderColor,
      saFrameBorderAlpha
      //+etc
    );

  TStyleAttribute = record
    CS: TControlState;
    Typ: TStyleAttributeType;
    Value: Variant;
  end;

  TStyleDefinition = class
  public
    Name: string;
    ParentName: string;
    ParentStyle: TStyleDefinition;
    ContainerStyle: TStyleDefinition;
    Attributes: array of TStyleAttribute;
    ContainedStyles: array of TStyleDefinition;
  end;

  TControlStyle=class;

  TComputedStyle = class
  public
    Definition: TStyleDefinition;
    ParentStyle: TComputedStyle;
    ContainerStyle: TComputedStyle;
    StateStyles: array[TControlState] of TControlStyle;
    ContainedStyles: array of TComputedStyle;
  end;

  TControlVisual = class;
  TControlBox = class;
  TControlLayout = class;
  TControlFrame = class;

  TControlStyle = class
  public
    Visual: TControlVisual;
    Box: TControlBox;
    Layout: TControlLayout;
    Frame: TControlFrame;
  end;

  TControlVisual = class
  public
    Color: integer;
    Alpha: single;
    MultAlpha: single;
    Cursor: TCursor;
  end;

  direction = (horz, vert);

  TAlign =
    (
      alBeg,
      alCen,
      alEnd
    );

  TControlBox = class
  public
    Margins: TRect;
    Padding: TRect;
    Align: array[direction] of TAlign;
    AutoSize: array[direction] of boolean;
    MinSize: array[direction] of integer;
    MaxSize: array[direction] of integer;
  end;

  TLayoutType =
    (
      loNone,
      loHorz,
      loVert,
      loStack
    );

  TControlLayout = class
  public
    Typ: TLayoutType;
    ResizeChildren: boolean;
    FitLastChild: boolean;
  end;

  TControlFrame = class
  public
    Name: string;
    BorderColor: integer;
    BorderAlpha: single;
    BackColor: integer;
    BackAlpha: single;
  end;

  TControlFont = class
  public
    Name: string;
    Style: TFontStyles;
    IsWhite: boolean;
  end;

  TIconStyle = class(TControlStyle)
  public
    //
  end;

  TLabelStyle = class(TControlStyle)
  public
    Font: TControlFont;
    Underlined: boolean;
  end;

  TEditStyle = class(TLabelStyle)
  public
    //...
  end;

  TBezierSegment = array[0..3] of array[direction] of single;

  TEffectStyle=class
  public
    Name: string;
    Duration: single;
    Delay: single;
    EaseFunction: TBezierSegment;
  end;

  TPanelStyle = class(TControlStyle)
  public
    ShowEffect: TEffectStyle;
    HideEffect: TEffectStyle;
  end;

  TComboBoxStyle = class(TControlStyle)
  public
    //
  end;

  TScrollBoxStyle = class(TControlStyle)
  public
    //
  end;

  TListViewStyle = class(TScrollBoxStyle)
  public
    //
  end;

  TTableViewStyle = class(TScrollBoxStyle)
  public
    //
  end;

  TTreeViewStyle = class(TScrollBoxStyle)
  public
    //
  end;

implementation

end.

Data types and structures for Simple CSS-like styles

Affine transformation, matrices and scope violations

One of commonly used ways of representing affine transformations such as rotations and translations in geometric software is utilizing of 4×4 matrices. The main problem with matrices is that they are often confusing both to newbie and even to experienced programmers. And there is an objective reason behind this. A matrix is a subject of analytical algebra, while geometric software deals with geometric scope. If you use low level constructs such as matrices math in your high level logic do not be surprised your code becomes complicated.

I call this situation a scope violation. In my opinion a good code should look like a SADT/IDEF0 diagram. Every subroutine like an IDEF0 block generates some output data from an input data based on its business logic. There is a rule in IDEF0 that the decomposition of a block can not have more than 7 other blocks. This restriction guarantees that every diagram is consistent in respect to its scope, i.e. every activity is described on the same level of detail. Using algebra matrices in your geometric code is like trying to insert a low level block into a top level IDEF0 diagram. Obviously this is wrong.

So, what geometric sense does a 4×4 transformation matrix have? From the geometric point of view a 4×4 transformation matrix is just a local coordinate system with its origin, X, Y, and Z axes. You might be surprised but a transformation matrix can be easily represented with the following structure.

  T3DMatrix = array[1..4,1..4] of STFloat;

  THmgMatrix3d = record
  case byte of
   0: (vX : T3DPoint; A : STFloat;
       vY : T3DPoint; B : STFloat;
       vZ : T3DPoint; C : STFloat;
       vT : T3DPoint; D : STFloat);
   1: (M : T3DMatrix);
  end;

Here vX, vY, vZ are directions of the local coordinate system axes, vT is the origin of the LCS. A, B, C are some stupid perspective transformation coefficients. They are used mainly in computer graphics, in CAD/CAM applications they are always equal to zero. The D coefficient is the scale coefficient. In geometric software it always equals to 1. Furthermore in geometric software in 99.99% of cases only orthogonal coordinate systems are used. This is enough to represent such transformations as translations, rotations, mirroring and the combinations.

All operations involving points, vectors and matrices also have a simple geometric sense. By multiplying a point onto a matrix we just compute coordinates of a point defined in a local coordinate system, in the global coordinate system. So we should replace the function Point_x_Matrix(const p: T3DPoint; const M: T3DMatrix) with the method TransformPoint of our THmgMatrix3d record.

function THmgMatrix3d.TransformPoint(const lp: T3DPoint): T3DPoint;
begin
  result :=
    lp.x*self.vX+
    lp.y*self.vY+
    lp.z*self.vZ+
    self.vT;
end;

Analogously multiplication of a vector onto a matrix is just computing of a global vector.

function THmgMatrix3d.TransformVector(const lv: T3DPoint): T3DPoint;
begin
  result :=
    lv.x*self.vX+
    lv.y*self.vY+
    lv.z*self.vZ;
end;

Multiplication of two matrices is the most confusing operation, because the result of multiplication varies of the order of multiplication and it’s easy to get matrices multiplied a wrong order. However from the geometric point of view multiplication of two matrices is just computation of a LCS2 defined in the LCS1 in the global coordinate system. So by replacing the function Matr_x_Matr(const M1, M2: T3DMatrix): T3DMatrix with the method THmgMatrix3d.TransformMatrix we once for all eliminate all misunderstandings.

function THmgMatrix3d.TransformMatrix(const lm: THmgMatrix3d): THmgMatrix3d;
begin
  result.Init;
  reuslt.vX := self.TransformVector(lm.vX);
  result.vY := self.TransformVector(lm.vY);
  result.vZ := self.TransformVector(lm.vZ);
  result.vT := self.TransformPoint(lm.vT);
end;

Even more misunderstandings introduce inverse matrices. Whilst a matrix itself has a simple geometric sense, an inverse matrix has no geometric sense at all. From the linear algebra point of view this is just a solution of a system of 4 linear equations. However the use of inverse matrices has an easily understandable geometric sense. Multiplication of a point, a vector or another matrix onto an inverse matrix results into computation of a point, a vector, or an lcs defined in the global coordinate system in the local coordinate system, the matrix of which we have inverted.

Fortunately the 99.99% of matrices used in geometric software are orthogonal so we can easily compute local vectors, points, and coordinate systems without computation of an inverse matrix.

function THmgMatrix3d.GetLocalVector(const gv: T3DPoint): T3DPoint; begin result.x := Vec_mul_Vec(gv, self.vX); result.y := Vec_mul_Vec(gv, self.vY); result.z := Vec_mul_Vec(gv, self.vZ); end; function THmgMatrix3d.GetLocalPoint(const gp: T3DPoint): T3DPoint; begin result := GetLocalVector(gp-self.vT); end; function THmgMatrix3d.GetLocalMatrix(const gm: THmgMatrix3d): THmgMatrix3d; begin result.Init; reuslt.vX := self.GetLocalVector(gm.vX); result.vY := self.GetLocalVector(gm.vY); result.vZ := self.GetLocalVector(gm.vZ); result.vT := self.GetLocalPoint(gm.vT); end;

function Vec_mul_Vec(const v1, v2: T3DPoint): T3DPoint; begin result := v1.x*v2.x+v1.y*v2.y+v1.z*v2.z; end;

So, by replacing all those messy T3DMatrix, Matr_x_Matr and InverseMatrix functions with THmgMatrix3d you can avoid scope violations and thus make your code much more easy to read and understand.

Affine transformation, matrices and scope violations

Efficient memory management with Delphi

The commonly used way of dealing with large amounts of data in non manageable programming languages like C/C++ and Delphi is using of custom memory managers with the following API.

  TItemStorage = class
  public
    constructor Create(ItemSize:integer);
    function GetNewItem: pointer;
    property Top: integer read GetTop;
    property Item[i: integer]: pointer read GetItem; default;
    procedure Clear;
  end;

What it actually is just a dynamic array, but a very efficient dynamic array. And unlike standard dynamic arrays this class guarantees that if you put an item in it the pointer to the item stays the same all the time no matter how many items you add after this. The trick is to allocate new memory in fixed sized blocks which can reside sufficient amount of items.

function TItemStorage.GetNewItem: pointer;
var
  i, bi, ii: integer;
begin
  i := AtomicAdd(1, fItemCnt);
  bi := i div fBlockCapacity;
  ii := i mod fBlockCapacity;
  if bi>fTopBlock then begin
    ReallocMem(fBlocks, (bi+1)*SizeOf(pointer));
    fBlocks[bi] := AllocMem(fBlockSize);;
    inc(fTopBlock);
  end;
  STIntPtr(result) := STIntPtr(fBlocks[bi]) + ii*fItemSize;
end;

function TItemStorage.GetItem(i: integer): pointer;
var
  bi, ii: integer;
begin
  bi := i div fBlockCapacity;
  ii := i mod fBlockCapacity;
  STIntPtr(result) := STIntPtr(fBlocks[bi])+ii*fItemSize;
end;

The important moment which often lacks of attention here is the AllocMem function. Everybody just use the standard GetMem(var p: pointer; Size: NativeInt) function for this. But I discovered that the standard delphi memory manager deals very bad with large blocks of memory. It tends to not free the virtual address space (VAS) after you call FreeMem. For example, In SprutCAM when a user generated a toolpath on a complex part and a massive amounts of memory were allocated for temporal data structures needed in the calculation process and than the user hit the reset button, the VAS which was allocated during the calculation process was not freed. The software just consumed more and more memory with every new toolpath generation.

The solution is to use the Win32 API VirtualAlloc/VirtualFree functions. Unfortunately the minimum size of a block which can allocate the VirtualAlloc function is 64kbytes, and this is too much for our needs. So we need a memory manager for a memory manager) which will split 64kbytes blocks into smaller chunks. I chose the size of 1Kb, which is big enough to reside such data as points, triangles etc, and small enough to not waste too much space if the number of used items in an array is small.

So I wrote such a class.

  TSTMemoryManager = class
  protected type
    TFreeBlockArray = array of pointer;
  protected
    fLock: integer;
    fFreeBlocks: TFreeBlockArray;
    fTopFreeBlock: integer;
    CntBlocks: integer;
    function AddFreeBlock(p: pointer): integer;
    function ExtractFreeBlock(p: pointer): integer;
    const BlockSize=64*1024;
    const PageSize=1024;
    function BlockInfo(p: pointer): PInt64;
    function ChunkInfo(p: pointer): PByte;
  public
    const ChunkSize=PageSize-SizeOf(int64);
    constructor Create;
    function AllocMem: pointer;
    function FreeMem(p: pointer): boolean;
  end;

function TSTMemoryManager.AllocMem: pointer;
var
  Block: pointer;
  bi: PInt64;
  i: integer;
begin
  Lock(fLock);
  try
    if fTopFreeBlock<0 then begin
      Block := VirtualAlloc(nil, BlockSize, MEM_RESERVE OR MEM_COMMIT, 
        PAGE_READWRITE);
      BlockInfo(Block)^ := -1;
      AddFreeBlock(Block);
      inc(CntBlocks);
    end else
      Block := fFreeBlocks[fTopFreeBlock];
    ASSERT((STIntPtr(Block) AND $FFFF)=0);
    bi := BlockInfo(Block);
    i := FetchFreeChunk(bi);
    ASSERT((i>=0) and (i<=63));
    if bi^=0 then
      dec(fTopFreeBlock);
    result := pointer(STIntPtr(Block)+i*PageSize);
  finally
    UnLock(fLock);
  end;
end;

function TSTMemoryManager.FreeMem(p: pointer): boolean;
var
  CI: byte;
  Block: pointer;
  bi: PInt64;
begin
  result := p<>nil;
  if result then begin
    Lock(fLock);
    try
      {$IFDEF CPUX64}
      Block := pointer(STIntPtr(p) AND $FFFFFFFFFFFF0000);
      {$ELSE}
      Block := pointer(STIntPtr(p) AND $FFFF0000);
      {$ENDIF}
      ci := (STIntPtr(p)-STIntPtr(Block)) div PageSize;
      ASSERT(ci<=63);
      ASSERT((STIntPtr(Block) AND $FFFF)=0);
      bi := BlockInfo(Block);
      if bi^=0 then
        AddFreeBlock(Block);
      FreeChunk(bi, ci);
      if bi^=-1 then begin
        ExtractFreeBlock(Block);
        VirtualFree(Block, 0, MEM_RELEASE);
        dec(CntBlocks);
      end;
    finally
      UnLock(fLock);
    end;
  end;
end;

function TSTMemoryManager.BlockInfo(p: pointer): PInt64;
begin
  result := pointer(NativeInt(P)+BlockSize-SizeOf(Int64));
end;

function FetchFreeChunk(bi: PInt64): integer;
asm
{$IFDEF CPUX64}
  mov rdx, [bi]
  BSF rax, rdx
  JZ @End;
  BTR[rcx], rax;
@End:
{$ELSE}
  mov edx, [eax]
  BSF ecx, edx
  JNZ @End32
  add eax, 4;
  mov edx, [eax];
  BSF ecx, edx
  JZ @End64
  BTR[eax], ecx;
@End64:
  mov eax, ecx;
  add eax, 32;
  ret
@End32:
  BTR[eax], ecx;
  mov eax, ecx;
{$ENDIF}
end;

function TSTMemoryManager.AddFreeBlock(p: pointer): integer;
begin
  fTopFreeBlock := fTopFreeBlock+1;
  result := fTopFreeBlock;
  if result>=Length(fFreeBlocks) then
    SetLength(fFreeBlocks, result+10);
  fFreeBlocks[result] := p;
end;

function TSTMemoryManager.ChunkInfo(p: pointer): PByte;
begin
  result := pointer(NativeInt(p)+PageSize-9);
end;

function TSTMemoryManager.ExtractFreeBlock(p: pointer): integer;
begin
  result := fTopFreeBlock;
  while (result>=0) and (fFreeBlocks[result]<>p) do
    dec(result);
  if fFreeBlocks[result]=p then begin
    fFreeBlocks[result] := fFreeBlocks[fTopFreeBlock];
    dec(fTopFreeBlock);
  end;
end;

procedure FreeChunk(Block: Pint64; iChunk: integer);
asm
  BTS [Block], iChunk;
end;

The class works the following way. With VirtualAlloc we allocate a block of 64kbytes and put it into fFreeBlocks, in the end of the block we store a 64bit mask every bit of which says which one of 64 block pages are currently free (we initialize it with Int64(-1)) . If the bit mask becomes zero in the AllocMem function it means the whole block is used and we remove it from the fFreeBlocks array, if the int64(mask) becomes –1 in the FreeMem function it means the whole block is free and we release it using the VirtualFree function.

After replacing the standard GetMem function with this class AllocMem routine SprutCAM now frees all the memory it uses during toolpath generation. Here is the source code. http://dl.dropbox.com/u/45498379/PageStorage.pas

Efficient memory management with Delphi

Delphi against the crazy C world

One thing drives me crazy. Why in the today world the ugly C-style syntax became so popular and widespread while beautiful pascal syntax is one on the latest places of popularity. Moreover today many new programming languages appear, and it seems to me the authors of those languages are competing with each other for the price in the category who figures out the most weird way of coding.

Let’s do some math about the SprutCAM project. Today the sources base of SprutCAM is about one and a half million lines of code, and it took 10 years to develop. As I know the effective development team never exceeded 5 men. 1’500’000 lines of code divided by 10 years of 50 weeks with 40 work hours multiplied at 5 men equals to 15 lines of code an hour.

What it looks like:

  result := false;
  at := self.GetArcType;
  case at of
    atL_L: begin
        Result := CalcArcLine_Line(ln1.p, ln1.tt, ln2.p, ln2.tt, s1, s2, fInitRc,
          ap1, ap2, ac, aR);
        if not result and
          IsEqD(1, abs(Vec_mul_Vec(ln1.tt, ln2.tt)), Increment)
        then begin
          result := true;
          ap1 := ProjectPointRay(self.pc, ln1.p, ln1.tt);
          ap2 := ProjectPointRay(self.pc, ln2.p, ln2.tt);
          ac := 0.5*(ap1+ap2);
          aR := VecLen(ap1, ac);
        end;

You can see there are a lot of spaces in each line. Your grandma can type 15 lines of code an hour! This example makes obvious one simple thing. Programming is not about typing. I can approve that programming is much more about reading a code and debugging it than typing it. However the authors of C-style languages think different! They think that by replacing human language words begin, end by curly braces {} and omitting then in the if statement they save hours of programmers lives. Here is an example of a C code.

   stbtt_uint16 format = ttUSHORT(data + index_map + 0);
   if (format == 0) { // apple byte encoding
      stbtt_int32 bytes = ttUSHORT(data + index_map + 2);
      if (unicode_codepoint < bytes-6)
         return ttBYTE(data + index_map + 6 + unicode_codepoint);
      return 0;
   } else if (format == 6) {
      stbtt_uint32 first = ttUSHORT(data + index_map + 6);
      stbtt_uint32 count = ttUSHORT(data + index_map + 8);
      if ((stbtt_uint32) unicode_codepoint >= first && (stbtt_uint32) unicode_codepoint < first+count)
         return ttUSHORT(data + index_map + 10 + (unicode_codepoint - first)*2);
      return 0;
   } else if (format == 2) {
      STBTT_assert(0); // @TODO: high-byte mapping for japanese/chinese/korean
      return 0;
   } else if (format == 4) { // standard mapping for windows fonts: binary search collection of ranges

Just look at those two code snippets! Which one do you prefer?

Delphi against the crazy C world

Steve Jobs, a man who approached the speed of light

My ramblings on the speed of light and Steve Jobs.

Everybody knows that nothing can move faster than light. But it is also known that time slows down for you when you approach the light speed. So imagine, you are flying on a space ship approaching the speed of light. For you speed is a distance divided by the time you spend moving this distance. Thus it seems to you, that you are continuously accelerating and moving much faster than light.

Exactly the same thing is happening to you when you are working on something you love and are continuously improving it. From a side view it may seem that you are stuck in place but from your point of view even minor improvements in the result make a huge difference.

Steve Jobs was a person who saw things not as they are in real life, he saw things as they are intended to be in an ideal world. And he devoted his entire life striving to make our world a better place.

So, next time you want to say that there is nothing exciting in an iPhone, an iPod, or an iPad, remember that you are just a mediocre crawling at a snail’s pace while Steve Jobs was someone who approached the light speed.

Steve Jobs, a man who approached the speed of light

Watch driven development

Everybody heard about the test driven development. But have you ever heard about the watch driven development?

If you are involved in development of some kind of graphic or geometric software, e.g. cad/cam/2d/3d modeler, editor, etc., you should know how difficult it can be to debug such applications. The main problem is that the data structures you use in your code are mainly of geometric nature (points, curves, surfaces, meshes, etc.) while the debugging environment is focused strictly on representing textual information. Thus debugging turns into a big headache: you write out the coordinates of points on a piece of paper, than try to imagine how those points are positioned in the space, connect them, of course you are not a talented painter, and you never can draw straight lines and perfect circles, so you do not understand your draft and thus the sources of errors in your algorithm, and finally you got tired and leave your task unsolved… forever.

This is the real life example.

One such unhappy day I decided to put an end to this madness, and I’ve come up with the idea of GeWatch. Gewatch is a standalone application which has the COM interface for receiving a tree of geometric entities, and it can represent that tree in two forms: as a feature tree on the left pane and as a tree of visual objects in the OpenGL canvas in the middle.  The most important word here is STANDALONE. As GeWatch is a standalone process it doesn’t depend on the application you are currently debugging, so you can stop your debugged application while still being able to watch the actual state of all the geometric data structures live in GeWatch, and it is even possible to watch how your algorithms work live.

The interface of Gewatch looks like that.

  ISTGeWatch = interface(IUnknown)
    ['{13C5118F-1364-4D77-99D0-1AA652A4787A}']
    function OpenObject(const Id: WideString; ReWrite: WordBool): WordBool;
    procedure CloseObject;
    procedure StartCurve(x, y: Double);
    procedure StopCurve(IsClosed: WordBool);
    procedure CutTo(x, y: Double);
    procedure ArcTo(x, y, xc, yc, r: Double);
    procedure Point(x, y: Double);
    procedure Arrow(x, y: Double; tx, ty: Double);
    procedure Box(xMin, yMin, xMax, yMax: Double);
    procedure SetProperty(const Name: WideString; Value: Double);
    procedure SetFormat(Attribute: GeFormatAttribute; Value: Integer);
    procedure Clear;
    procedure ClearImmediate;
    procedure Line(x1, y1, x2, y2: Double);
    procedure Circle(xc, yc, r: Double);
    procedure Arc(x1, y1, x2, y2, xc, yc, r: Double);
    procedure LineColor(Value: Integer);
    procedure LineWidth(Value: Integer);
    procedure StartCurve3d(x, y, z: Double);
    procedure StopCurve3d(IsClosed: WordBool);
    procedure CutTo3d(x, y, z: Double);
    procedure Point3d(x, y, z: Double);
    procedure Arrow3d(x, y, z, tx, ty, tz: Double);
    procedure StartTriangles;
    procedure AddTriangleVertex(x, y, z: Double);
    procedure SetVertexNormal(x, y, z: Double);
    procedure CloseTriangles;
    procedure Cube(xMin, yMin, zMin, xMax, yMax, zMax: Double);
  end;

To use it in our code I created the module AbstractGeWatch with the following stuff.

  IWatchableObject = interface
    ['{DF9FA5EF-3F5F-4E86-B0F0-395C908C2610}']
    procedure Watch(const w: ISTGeWatch);
  end;

  function agw: IAbstractGeWatch;
  begin
    result := fAgw;
  end;

  procedure isdf(const Unk: IUnknown; const ObjName: WideString = '');
  var w: IAbstractGeWatch;
      wobj: ISTGeWatch;
  begin
    try
      if unk=nil then Exit;
      if Cast(unk, IWatchableObject, wobj) then begin
        w := agw;
        if w=nil then Exit;
        if ObjName='' then begin
          wobj.Watch(w);
        end else begin
          w.OpenObject(ObjName);
          wobj.Watch(w);
          w.CloseObject;
        end;
      end;
    except
    end;
  end;

So you can either use agw function to directly draw in GeWatch, or you can implement the IWatchableObject interface in your geometric data structures and watch them with the isdf function. When you stop your debugged application by breakpoint or whatever you have access to both of those functions in the Evaluate/Modify dialog.

Gewatch saved months of my life and turned development of geometric algorithms into fun which it is intended to be.

Watch driven development