// GLExtrusion
{: Egg<p>

	Extrusion objects for GLScene. Extrusion objects are solids defined by the
   surface described by a moving curve.<p>

	<b>Historique : </b><font size=-1><ul>
      <li>02/11/01 - Egg - TGLPipe.BuildList now has a "persistent" cache
      <li>25/11/01 - Egg - TGLPipe nodes can now be colored
      <li>19/07/01 - Egg - Fix in TGLRevolutionSolid due to RotateAround change
      <li>29/03/01 - Uwe - Added a TGLExtrusionSolid based on TMultiPolygonBase,
                           Fixes in TGLRevolutionSolid
      <li>26/02/01 - Egg - Minor update to RenderSides by Michael Schuricht
      <li>21/02/01 - Egg - New RenderSides code by Michael Schuricht,
                           now XOpenGL based (multitexture)
      <li>10/01/01 - Egg - Better aspect when nodeN = NodeN-2 in lines mode
                           (should have only beend encountered when editing)
	   <li>06/08/00 - Egg - Creation (from split of GLObjects), Added TGLPipe
	</ul></font>
}
{  TODO

   ur:
     Suggestion:
       All extrusion objects use actually the same kind of "parts",
       one common type should do.
}
unit GLExtrusion;

interface

uses Classes, OpenGL1x, GLObjects, GLScene, GLMisc, GLTexture, GLMultiPolygon;

type

   TExtrusionSolidPart = (espOutside, espInside, espStartPolygon, espStopPolygon);
   TExtrusionSolidParts = set of TExtrusionSolidPart;

   // TRevolutionSolidParts
   //
   TRevolutionSolidPart = (rspOutside, rspInside, rspStartPolygon, rspStopPolygon);
   TRevolutionSolidParts = set of TRevolutionSolidPart;

   // TGLRevolutionSolid
   //
   {: A solid object generated by rotating a curve along the Y axis.<p>
      The curve is described by the Nodes and SplineMode properties, and it is
      rotated in the trigonometrical direction (CCW when seen from Y->INF).<p>
      The TGLRevolutionSolid can also be used to render regular helicoidions, by
      setting a non-null YOffsetPerTurn, and adjusting start/finish angles to
      make more than one revolution.<p>
      If you want top/bottom caps, just add a first/last node that will make
      the curve start/finish on the Y axis. }
   TGLRevolutionSolid = class(TGLPolygonBase)
      private
			{ Private Declarations }
         FSlices : Integer;
         FStartAngle, FStopAngle : Single;
         FNormals : TNormalSmoothing;
         FYOffsetPerTurn : Single;
         FTriangleCount : Integer;
         FNormalDirection : TNormalDirection;
         FParts : TRevolutionSolidParts;

		protected
			{ Protected Declarations }
         procedure SetStartAngle(const val : Single);
         procedure SetStopAngle(const val : Single);
         function  StoreStopAngle : Boolean;
         procedure SetSlices(const val : Integer);
         procedure SetNormals(const val : TNormalSmoothing);
         procedure SetYOffsetPerTurn(const val : Single);
         procedure SetNormalDirection(const val : TNormalDirection);
         procedure SetParts(const val : TRevolutionSolidParts);

      public
			{ Public Declarations }
         constructor Create(AOwner: TComponent); override;
         destructor Destroy; override;
         procedure Assign(Source: TPersistent); override;
         procedure BuildList(var rci : TRenderContextInfo); override;

         {: Number of triangles used for rendering. }
         property TriangleCount : Integer read FTriangleCount;

      published
			{ Published Declarations }
         {: Parts of the rotation solid to be generated for rendering.<p>
            rspInside and rspOutside are generated from the curve and make the
            inside/outside as long as NormalDirection=ndOutside and the solid
            is described by the curve that goes from top to bottom.<p>
            Start/StopPolygon are tesselated from the curve (considered as closed). }
         property Parts : TRevolutionSolidParts read FParts write SetParts default [rspOutside];

         property StartAngle : Single read FStartAngle write SetStartAngle;
         property StopAngle : Single read FStopAngle write SetStopAngle stored StoreStopAngle;
         {: Y offset applied to the curve position for each turn.<p>
            This amount is applied proportionnally, for instance if your curve
            is a small circle, off from the Y axis, with a YOffset set to 0 (zero),
            you will get a torus, but with a non null value, you will get a
            small helicoidal spring.<p>
            This can be useful for rendering, lots of helicoidal objects from
            screws, to nails to stairs etc. }
         property YOffsetPerTurn : Single read FYOffsetPerTurn write SetYOffsetPerTurn;
         {: Number of slices per turn (360°). }
         property Slices : Integer read FSlices write SetSlices default 16;

         property Normals : TNormalSmoothing read FNormals write SetNormals default nsFlat;
         property NormalDirection : TNormalDirection read FNormalDirection write SetNormalDirection default ndOutside;
   end;

   // TGLExtrusionSolid
   //
   { Extrudes a complex Polygon into Z direction.<p>
     For contour description see TMultiPolygonBase.<br>
     properties Parts, Height (or should we better cal it Depth, because its in Z?),
     Stacks, Normals and NormalDirection are equivalent to TGLRevolutionSolid.<p>

     If Normals=nsSmooth and the angle between two consecutive normals along the
     contour is less than MinSmoothAngle, smoothing is done, otherweise flat normals
     are used. This makes it possible to have smooth normals on sharp edged contours.
   }
   TGLExtrusionSolid = class(TMultiPolygonBase)
   private
      { Private Declarations }
      FStacks : Integer;
      FNormals : TNormalSmoothing;
      FTriangleCount : Integer;
      FNormalDirection : TNormalDirection;
      FParts : TExtrusionSolidParts;
      FHeight: TGLFloat;
      FMinSmoothAngle: Single;
      FMinSmoothAngleCos:Single;
      procedure SetHeight(const Value: TGLFloat);
      procedure SetMinSmoothAngle(const Value: Single);

   protected
      { Protected Declarations }
      procedure SetStacks(const val : Integer);
      procedure SetNormals(const val : TNormalSmoothing);
      procedure SetNormalDirection(const val : TNormalDirection);
      procedure SetParts(const val : TExtrusionSolidParts);

   public
      { Public Declarations }
      constructor Create(AOwner: TComponent); override;
      destructor Destroy; override;
      procedure Assign(Source: TPersistent); override;
      procedure BuildList(var rci : TRenderContextInfo); override;

      {: Number of triangles used for rendering. }
      property TriangleCount : Integer read FTriangleCount;

   published
     { Published Declarations }
      property Parts : TExtrusionSolidParts read FParts write SetParts default [espOutside];

      property Height: TGLFloat read FHeight write SetHeight;
      property Stacks : Integer read FStacks write SetStacks default 1;

      property Normals : TNormalSmoothing read FNormals write SetNormals default nsFlat;
      property NormalDirection : TNormalDirection read FNormalDirection write SetNormalDirection default ndOutside;
      property MinSmoothAngle:Single read FMinSmoothAngle write SetMinSmoothAngle;
   end;

	// TGLPipeNode
	//
	TGLPipeNode = class (TGLNode)
	   private
	      { Private Declarations }
         FRadiusFactor : Single;
         FColor : TGLColor;

	   protected
	      { Protected Declarations }
         function GetDisplayName : String; override;
         procedure SetRadiusFactor(const val : Single);
         function StoreRadiusFactor : Boolean;
         procedure SetColor(const val : TGLColor);
	      procedure ColorChanged(sender : TObject);

      public
	      { Public Declarations }
	      constructor Create(Collection : TCollection); override;
	      destructor Destroy; override;
	      procedure Assign(Source: TPersistent); override;

	   published
	      { Published Declarations }
         property RadiusFactor : Single read FRadiusFactor write SetRadiusFactor stored StoreRadiusFactor;
         property Color : TGLColor read FColor write SetColor;

	end;

	// TGLPipeNodes
	//
	TGLPipeNodes = class (TGLLinesNodes)
	   protected
	      { Protected Declarations }
         procedure SetItems(index : Integer; const val : TGLPipeNode);
	      function GetItems(index : Integer) : TGLPipeNode;

      public
	      { Public Declarations }
	      constructor Create(AOwner : TComponent); 
         function Add: TGLPipeNode;
	      function FindItemID(ID: Integer): TGLPipeNode;
	      property Items[index : Integer] : TGLPipeNode read GetItems write SetItems; default;
   end;

   // TPipeParts
   //
   TPipePart = (ppOutside, ppInside, ppStartDisk, ppStopDisk);
   TPipeParts = set of TPipePart;

   // TPipeNodesColorMode
   //
   TPipeNodesColorMode = (pncmNone, pncmEmission, pncmAmbient, pncmDiffuse,
                          pncmAmbientAndDiffuse);

   // TGLPipe
   //
   {: A solid object generated by extruding a circle along a trajectory.<p>
      Texture coordinates NOT supported yet. }
   TGLPipe = class(TGLPolygonBase)
      private
			{ Private Declarations }
         FSlices : Integer;
         FParts : TPipeParts;
         FTriangleCount : Integer;
         FRadius : Single;
         FNodesColorMode : TPipeNodesColorMode;

		protected
			{ Protected Declarations }
         procedure CreateNodes; override;
         procedure SetSlices(const val : Integer);
         procedure SetParts(const val : TPipeParts);
         procedure SetRadius(const val : Single);
         function StoreRadius : Boolean;
         procedure SetNodesColorMode(const val : TPipeNodesColorMode);

      public
			{ Public Declarations }
         constructor Create(AOwner: TComponent); override;
         destructor Destroy; override;
         procedure Assign(Source: TPersistent); override;
         procedure BuildList(var rci : TRenderContextInfo); override;

         {: Number of triangles used for rendering. }
         property TriangleCount : Integer read FTriangleCount;

      published
			{ Published Declarations }
         property Parts : TPipeParts read FParts write SetParts default [ppOutside];
         property Slices : Integer read FSlices write SetSlices default 16;
         property Radius : Single read FRadius write SetRadius;
         property NodesColorMode : TPipeNodesColorMode read FNodesColorMode write SetNodesColorMode default pncmNone;
   end;

// ------------------------------------------------------------------
// ------------------------------------------------------------------
// ------------------------------------------------------------------
implementation
// ------------------------------------------------------------------
// ------------------------------------------------------------------
// ------------------------------------------------------------------

uses SysUtils, VectorGeometry, Spline, VectorLists, XOpenGL;

// ------------------
// ------------------ TGLRevolutionSolid ------------------
// ------------------

// Create
//
constructor TGLRevolutionSolid.Create(AOwner: TComponent);
begin
   inherited Create(AOwner);
   FStartAngle:=0;
   FStopAngle:=360;
   FSlices:=16;
   FNormals:=nsFlat;
   FNormalDirection:=ndOutside;
   FParts:=[rspOutside];
end;

// Destroy
//
destructor TGLRevolutionSolid.Destroy;
begin
   inherited Destroy;
end;

// SetStartAngle
//
procedure TGLRevolutionSolid.SetStartAngle(const val : Single);
begin
   if FStartAngle<>val then begin
      FStartAngle:=val;
      if FStartAngle>FStopAngle then FStopAngle:=FStartAngle;
      StructureChanged;
   end;
end;

// SetStopAngle
//
procedure TGLRevolutionSolid.SetStopAngle(const val : Single);
begin
   if FStopAngle<>val then begin
      FStopAngle:=val;
      if FStopAngle<FStartAngle then FStartAngle:=FStopAngle;
      StructureChanged;
   end;
end;

// StoreStopAngle
//
function TGLRevolutionSolid.StoreStopAngle : Boolean;
begin
   Result:=(FStopAngle<>360);
end;

// SetSlices
//
procedure TGLRevolutionSolid.SetSlices(const val : Integer);
begin
   if (val<>FSlices) and (val>0) then begin
      FSlices:=val;
      StructureChanged;
   end;
end;

// SetNormals
//
procedure TGLRevolutionSolid.SetNormals(const val : TNormalSmoothing);
begin
   if FNormals<>val then begin
      FNormals:=val;
      StructureChanged;
   end;
end;

// SetYOffsetPerTurn
//
procedure TGLRevolutionSolid.SetYOffsetPerTurn(const val : Single);
begin
   if FYOffsetPerTurn<>val then begin
      FYOffsetPerTurn:=val;
      StructureChanged;
   end;
end;

// SetNormalDirection
//
procedure TGLRevolutionSolid.SetNormalDirection(const val : TNormalDirection);
begin
   if FNormalDirection<>val then begin
      FNormalDirection:=val;
      StructureChanged;
   end;
end;

// SetParts
//
procedure TGLRevolutionSolid.SetParts(const val : TRevolutionSolidParts);
begin
   if FParts<>val then begin
      FParts:=val;
      StructureChanged;
   end;
end;

// Assign
//
procedure TGLRevolutionSolid.Assign(Source: TPersistent);
begin
   if Source is TGLRevolutionSolid then begin
      FStartAngle:=TGLRevolutionSolid(Source).FStartAngle;
      FStopAngle:=TGLRevolutionSolid(Source).FStopAngle;
      FSlices:=TGLRevolutionSolid(Source).FSlices;
      FNormals:=TGLRevolutionSolid(Source).FNormals;
      FYOffsetPerTurn:=TGLRevolutionSolid(Source).FYOffsetPerTurn;
      FNormalDirection:=TGLRevolutionSolid(Source).FNormalDirection;
      FParts:=TGLRevolutionSolid(Source).FParts;
   end;
   inherited Assign(Source);
end;

// BuildList
//
procedure TGLRevolutionSolid.BuildList(var rci : TRenderContextInfo);
var
   deltaAlpha, startAlpha, stopAlpha, alpha : Single;
   deltaS : Single;
   deltaYOffset, yOffset, startYOffset : Single;
   lastNormals : PAffineVectorArray;
   firstStep, gotYDeltaOffset : Boolean;

   procedure CalcNormal(const ptTop, ptBottom : PAffineVector; var normal : TAffineVector);
   var
      tb : TAffineVector;
      mx, mz : Single;
   begin
      mx:=ptBottom[0]+ptTop[0];
      mz:=ptBottom[2]+ptTop[2];
      VectorSubtract(ptBottom^, ptTop^, tb);
      normal[0]:=-tb[1]*mx;
      normal[1]:=mx*tb[0]+mz*tb[2];
      normal[2]:=-mz*tb[1];
      NormalizeVector(normal);
   end;

   procedure BuildStep(ptTop, ptBottom : PAffineVector; invertNormals : Boolean;
                       topT, bottomT : Single);
   var
      i : Integer;
      topBase, topNext, bottomBase, bottomNext, normal, topNormal, bottomNormal : TAffineVector;
      topTPBase, topTPNext, bottomTPBase, bottomTPNext : TTexPoint;
      nextAlpha : Single;
      ptBuffer : PAffineVector;

      procedure SetLocalNormals;
      begin
        if (FNormals=nsFlat) or FirstStep then begin
          topNormal:=normal;
          bottomNormal:=normal;
          if (FNormals=nsSmooth) then lastNormals[i]:=normal;
        end else if (FNormals=nsSmooth) then begin
          if invertNormals then begin
            topNormal:=normal;
            bottomNormal:=lastNormals[i];
          end else begin
            topNormal:=lastNormals[i];
            bottomNormal:=normal;
          end;
          lastNormals[i]:=normal;
        end;
      end;

   begin
      // to invert normals, we just need to flip top & bottom
      if invertNormals then begin
         ptBuffer:=ptTop;
         ptTop:=ptBottom;
         ptBottom:=ptBuffer;
      end;
      // generate triangle strip for a level
      // TODO : support for triangle fans (when ptTop or ptBottom is on the Y Axis)
      alpha:=startAlpha;
      i:=0;
      yOffset:=startYOffset;
      topTPBase.S:=0;         bottomTPBase.S:=0;
      topTPBase.T:=topT;      bottomTPBase.T:=bottomT;
      VectorRotateAroundY(ptTop^, alpha, topBase);
      VectorRotateAroundY(ptBottom^, alpha, bottomBase);
      if gotYDeltaOffset then begin
         topBase[1]:=topBase[1]+yOffset;
         bottomBase[1]:=bottomBase[1]+yOffset;
      end;
      CalcNormal(@topBase, @bottomBase, normal);
      SetLocalNormals;
      inc(i);
      topTPNext:=topTPBase;
      bottomTPNext:=bottomTPBase;
      glBegin(GL_TRIANGLE_STRIP);
      glNormal3fv(@topNormal);
      xglTexCoord2fv(@topTPBase);
      glVertex3fv(@topBase);
      while alpha<stopAlpha do begin
         glNormal3fv(@bottomNormal);
         xglTexCoord2fv(@bottomTPBase);
         glVertex3fv(@bottomBase);
         nextAlpha:=alpha+deltaAlpha;
         topTPNext.S:=topTPNext.S+deltaS;
         bottomTPNext.S:=bottomTPNext.S+deltaS;
         VectorRotateAroundY(ptTop^, nextAlpha, topNext);
         VectorRotateAroundY(ptBottom^, nextAlpha, bottomNext);
         if gotYDeltaOffset then begin
            topNext[1]:=topNext[1]+yOffset;
            bottomNext[1]:=bottomNext[1]+yOffset;
            yOffset:=yOffset+deltaYOffset
         end;
         CalcNormal(@topNext, @bottomNext, normal);
         SetLocalNormals;
         inc(i);
         xglTexCoord2fv(@topTPNext);
         glNormal3fv(@topNormal);
         glVertex3fv(@topNext);
         alpha:=nextAlpha;
         topBase:=topNext;          topTPBase:=topTPNext;
         bottomBase:=bottomNext;    bottomTPBase:=bottomTPNext;
      end;
      glNormal3fv(@bottomNormal);
      xglTexCoord2fv(@bottomTPBase);
      glVertex3fv(@bottomBase);
      glEnd;
      firstStep:=False;
   end;

var
   i, nbSteps, nbDivisions : Integer;
   splinePos, lastSplinePos, bary, polygonNormal : TAffineVector;
   f : Single;
   spline : TCubicSpline;
   invertedNormals : Boolean;
   polygon : TGLNodes;
begin
   if (Nodes.Count>1) and (FStopAngle>FStartAngle) then begin
      startAlpha:=FStartAngle*cPIdiv180;
      stopAlpha:=FStopAngle*cPIdiv180;
      nbSteps:=Round(((stopAlpha-startAlpha)/(2*PI))*FSlices);
      // drop 0.1% to slice count to care for precision losses
      deltaAlpha:=(stopAlpha-startAlpha)/(nbSteps*0.999);
      deltaS:=(stopAlpha-startAlpha)/(2*PI*nbSteps);
      gotYDeltaOffset:=FYOffsetPerTurn<>0;
      if gotYDeltaOffset then
         deltaYOffset:=(FYOffsetPerTurn*(stopAlpha-startAlpha)/(2*PI))/nbSteps
      else deltaYOffset:=0;
      startYOffset:=YOffsetPerTurn*startAlpha/(2*PI);
      invertedNormals:=(FNormalDirection=ndInside);
      FTriangleCount:=0;
      // generate sides
      if (rspInside in FParts) or (rspOutside in FParts) then begin
         // allocate lastNormals buffer (if smoothing)
         if FNormals=nsSmooth then begin
            GetMem(lastNormals, (FSlices+2)*SizeOf(TAffineVector));
            firstStep:=True;
         end;
         // start working
         if rspInside in Parts then begin
           firstStep:=True;
           if (Division<2) or (SplineMode=lsmLines) then begin
              // standard line(s), draw directly
              for i:=0 to Nodes.Count-2 do with Nodes[i] do begin
                 BuildStep(PAffineVector(Nodes[i].AsAddress),
                           PAffineVector(Nodes[i+1].AsAddress), not invertedNormals,
                           i/(Nodes.Count-1), (i+1)/(Nodes.Count-1));
              end;
              FTriangleCount:=nbSteps*Nodes.Count*2;
           end else begin
              // cubic spline
              Spline:=Nodes.CreateNewCubicSpline;
              Spline.SplineAffineVector(0, lastSplinePos);
              f:=1/Division;
              nbDivisions:=(Nodes.Count-1)*Division;
              for i:=1 to nbDivisions do begin
                 Spline.SplineAffineVector(i*f, splinePos);
                 BuildStep(@lastSplinePos, @splinePos, not invertedNormals,
                           (i-1)/nbDivisions, i/nbDivisions);
                 lastSplinePos:=splinePos;
              end;
              Spline.Free;
              FTriangleCount:=nbSteps*nbDivisions*2;
           end;
         end;
         if rspOutside in Parts then begin
           firstStep:=True;
           if (Division<2) or (SplineMode=lsmLines) then begin
              // standard line(s), draw directly
              for i:=0 to Nodes.Count-2 do with Nodes[i] do begin
                 BuildStep(PAffineVector(Nodes[i].AsAddress),
                           PAffineVector(Nodes[i+1].AsAddress), invertedNormals,
                           i/(Nodes.Count-1), (i+1)/(Nodes.Count-1));
              end;
              FTriangleCount:=nbSteps*Nodes.Count*2;
           end else begin
              // cubic spline
              Spline:=Nodes.CreateNewCubicSpline;
              Spline.SplineAffineVector(0, lastSplinePos);
              f:=1/Division;
              nbDivisions:=(Nodes.Count-1)*Division;
              for i:=1 to nbDivisions do begin
                 Spline.SplineAffineVector(i*f, splinePos);
                 BuildStep(@lastSplinePos, @splinePos, invertedNormals,
                           (i-1)/nbDivisions, i/nbDivisions);
                 lastSplinePos:=splinePos;
              end;
              Spline.Free;
              FTriangleCount:=nbSteps*nbDivisions*2;
           end;
         end;
         if (rspInside in FParts) and (rspOutside in FParts) then
            FTriangleCount:=FTriangleCount*2;
         xglTexCoord2fv(@NullTexPoint);
         // release lastNormals buffer (if smoothing)
         if FNormals=nsSmooth then
            FreeMem(lastNormals);
      end;
      // tessellate start/stop polygons
      if (rspStartPolygon in FParts) or (rspStopPolygon in FParts) then begin
         bary:=Nodes.Barycenter; bary[1]:=0;
         NormalizeVector(bary);
         // tessellate start polygon
         if rspStartPolygon in FParts then begin
            polygon:=Nodes.CreateCopy(nil);
            with polygon do begin
               RotateAroundY(RadToDeg(startAlpha));
               Translate(AffineVectorMake(0, startYOffset, 0));
               if invertedNormals then
                  alpha:=startAlpha+PI/2
               else alpha:=startAlpha+PI+PI/2;
               polygonNormal:=VectorRotateAroundY(bary, alpha);
               if SplineMode=lsmLines then
                  RenderTesselatedPolygon(False, @polygonNormal, 1)
               else RenderTesselatedPolygon(False, @polygonNormal, Division);
               Free;
            end;
            // estimated count
            FTriangleCount:=FTriangleCount+Nodes.Count+(Nodes.Count shr 1);
         end;
         // tessellate stop polygon
         if rspStopPolygon in FParts then begin
            polygon:=Nodes.CreateCopy(nil);
            with polygon do begin
               RotateAroundY(RadToDeg(stopAlpha));
               Translate(AffineVectorMake(0, startYOffset+(stopAlpha-startAlpha)*YOffsetPerTurn/(2*PI), 0));
               if invertedNormals then
                  alpha:=stopAlpha+PI+PI/2
               else alpha:=stopAlpha+PI/2;
               polygonNormal:=VectorRotateAroundY(bary, alpha);
               if SplineMode=lsmLines then
                  RenderTesselatedPolygon(False, @polygonNormal, 1)
               else RenderTesselatedPolygon(False, @polygonNormal, Division);
               Free;
            end;
            // estimated count
            FTriangleCount:=FTriangleCount+Nodes.Count+(Nodes.Count shr 1);
         end;
      end;
   end;
end;

// ------------------
// ------------------ TGLPipeNode ------------------
// ------------------

// Create
//
constructor TGLPipeNode.Create(Collection : TCollection);
begin
	inherited Create(Collection);
   FRadiusFactor:=1.0;
   FColor:=TGLColor.CreateInitialized(Self, clrBlack, ColorChanged);
end;

// Destroy
//
destructor TGLPipeNode.Destroy;
begin
   FColor.Free;
	inherited Destroy;
end;

// Assign
//
procedure TGLPipeNode.Assign(Source: TPersistent);
begin
	if Source is TGLPipeNode then begin
      FRadiusFactor:=TGLPipeNode(Source).FRadiusFactor
	end;
	inherited;
end;

// GetDisplayName
//
function TGLPipeNode.GetDisplayName : String;
begin
	Result:=Format('%s / rf = %.3f', [inherited GetDisplayName, RadiusFactor]);;
end;

// SetRadiusFactor
//
procedure TGLPipeNode.SetRadiusFactor(const val : Single);
begin
   if FRadiusFactor<>val then begin
      FRadiusFactor:=val;
      Changed(false);
      //(Collection as TGLNodes).NotifyChange;
   end;
end;

// StoreRadiusFactor
//
function TGLPipeNode.StoreRadiusFactor : Boolean;
begin
	Result:=(FRadiusFactor<>1.0);
end;

// SetColor
//
procedure TGLPipeNode.SetColor(const val : TGLColor);
begin
   FColor.Assign(val);
end;

// ColorChanged
//
procedure TGLPipeNode.ColorChanged(sender : TObject);
begin
   TGLPipeNodes(Collection).NotifyChange;
end;

// ------------------
// ------------------ TGLPipeNodes ------------------
// ------------------

constructor TGLPipeNodes.Create(AOwner : TComponent);
begin
	inherited Create(AOwner, TGLPipeNode);
end;

procedure TGLPipeNodes.SetItems(index : Integer; const val : TGLPipeNode);
begin
	inherited Items[index]:=val;
end;

function TGLPipeNodes.GetItems(index : Integer) : TGLPipeNode;
begin
	Result:=TGLPipeNode(inherited Items[index]);
end;

function TGLPipeNodes.Add: TGLPipeNode;
begin
	Result:=(inherited Add) as TGLPipeNode;
end;

function TGLPipeNodes.FindItemID(ID: Integer): TGLPipeNode;
begin
	Result:=(inherited FindItemID(ID)) as TGLPipeNode;
end;

// ------------------
// ------------------ TGLPipe ------------------
// ------------------

// Create
//
constructor TGLPipe.Create(AOwner: TComponent);
begin
   inherited Create(AOwner);
   FSlices:=16;
   FParts:=[ppOutside];
   FRadius:=1.0;
   FTriangleCount:=0;
end;

// CreateNodes
//
procedure TGLPipe.CreateNodes;
begin
   FNodes:=TGLPipeNodes.Create(Self);
end;

// Destroy
//
destructor TGLPipe.Destroy;
begin
   inherited Destroy;
end;

// SetSlices
//
procedure TGLPipe.SetSlices(const val : Integer);
begin
   if (val<>FSlices) and (val>0) then begin
      FSlices:=val;
      StructureChanged;
   end;
end;

// SetParts
//
procedure TGLPipe.SetParts(const val : TPipeParts);
begin
   if FParts<>val then begin
      FParts:=val;
      StructureChanged;
   end;
end;

// SetRadius
//
procedure TGLPipe.SetRadius(const val : Single);
begin
   if FRadius<>val then begin
      FRadius:=val;
      StructureChanged;
   end;
end;

// StoreRadius
//
function TGLPipe.StoreRadius : Boolean;
begin
   Result:=(FRadius<>1);
end;

// SetNodesColorMode
//
procedure TGLPipe.SetNodesColorMode(const val : TPipeNodesColorMode);
begin
   if val<>FNodesColorMode then begin
      FNodesColorMode:=val;
      StructureChanged;
   end;
end;

// Assign
//
procedure TGLPipe.Assign(Source: TPersistent);
begin
   if Source is TGLPipe then begin
      FSlices:=TGLPipe(Source).FSlices;
      FParts:=TGLPipe(Source).FParts;
      FRadius:=TGLPipe(Source).FRadius;
      FNodesColorMode:=TGLPipe(Source).FNodesColorMode;
   end;
   inherited Assign(Source);
end;

// BuildList
//
var
   vSinCache, vCosCache : array of Single;
procedure TGLPipe.BuildList(var rci : TRenderContextInfo);
type
   TNodeData = record
      pos : TAffineVector;
      normal : TAffineVector;
   end;
   TRowData = record
      node : array of TNodeData;
      color : TColorVector;
   end;
   PRowData = ^TRowData;
const
   cPNCMtoEnum : array [pncmEmission..pncmAmbientAndDiffuse] of TGLEnum =
      (GL_EMISSION, GL_AMBIENT, GL_DIFFUSE, GL_AMBIENT_AND_DIFFUSE);

   procedure CalculateRow(row : PRowData;
                          const center, normal : TAffineVector; radius : Single);
   var
      i : Integer;
      vx, vy : TAffineVector;
   begin
      // attempt to use object's Z as Y vector
      VectorCrossProduct(ZVector, normal, vx);
      if VectorNorm(vx)<1e-7 then begin
         // bad luck, the X vector will do (unless it's or normal that was null)
         if VectorNorm(normal)<1e-7 then begin
            SetVector(vx, XVector);
            SetVector(vy, ZVector);
         end else begin
            VectorCrossProduct(XVector, normal, vx);
            NormalizeVector(vx);
            VectorCrossProduct(normal, vx, vy);
         end;
      end else begin
         NormalizeVector(vx);
         VectorCrossProduct(normal, vx, vy);
      end;
      NormalizeVector(vy);
      ScaleVector(vx, FRadius);
      ScaleVector(vy, FRadius);
      // generate the circle
      for i:=0 to High(row.node) do begin
         row.node[i].normal:=VectorCombine(vx, vy, vCosCache[i], vSinCache[i]);
         row.node[i].pos:=VectorCombine(PAffineVector(@center)^,
                                        row.node[i].normal, 1, radius);
      end;
   end;

   procedure RenderDisk(row : PRowData;
                        const center : TVector; const normal : TAffineVector;
                        invert : Boolean);
   var
      i : Integer;
   begin
      if NodesColorMode<>pncmNone then
         glColor4fv(@row.color);
      glBegin(GL_TRIANGLE_FAN);
         glNormal3fv(@normal);
         glVertex3fv(@center);
         if invert then
            for i:=High(row.node) downto 0 do glVertex3fv(@row.node[i].pos)
         else for i:=0 to High(row.node) do glVertex3fv(@row.node[i].pos);
      glEnd;
   end;

   procedure RenderSides(curRow, prevRow : PRowData);
   var
      j, k, kp, m, n, kx, ky, kz, kpx, kpy, kpz : Integer;
      deltaNormal, deltaPos : array of Double;
   begin
      kx:=1;   ky:=1;   kz:=1;
      kpx:=1;  kpy:=1;  kpz:=1;
      for j:=0 to Slices do begin
         if Sign(curRow.node[j].normal[0])<>Sign(prevRow.node[j].normal[0]) then Inc(kx);
         if Sign(curRow.node[j].normal[1])<>Sign(prevRow.node[j].normal[1]) then Inc(ky);
         if Sign(curRow.node[j].normal[2])<>Sign(prevRow.node[j].normal[2]) then Inc(kz);
         if Sign(curRow.node[j].pos[0])<>Sign(prevRow.node[j].pos[0]) then Inc(kpx);
         if Sign(curRow.node[j].pos[1])<>Sign(prevRow.node[j].pos[1]) then Inc(kpy);
         if Sign(curRow.node[j].pos[2])<>Sign(prevRow.node[j].pos[2]) then Inc(kpz);
      end;
      // k is a Value, which indicate the similarity of the normal vectors
      // between curRow and PrevRow
      k:=kx*ky*kz;
      // kp is a Value, which indicate the similarity of the positon vectors
      // between curRow and PrevRow
      kp:=kpx*kpy*kpz;
      // I hope, so i can distinguish, if the curRow must rotate
      if (k>Sqr(Slices div 2)) or (kp>Sqr(Slices div 2)) then begin
         SetLength(deltanormal, Slices);
         SetLength(deltapos, Slices);
         for k:=0 to Slices-1 do begin    //rotate index for curRow
            deltanormal[k]:=0;             //sum of difference for normal vector
            deltapos[k]:=0;                //sum of difference for pos vector
            for j:=0 to Slices-1 do begin  //over all places
               n:=(j+k) mod Slices;
               deltanormal[k]:=deltanormal[k]+VectorSpacing(curRow.node[n].normal, prevRow.node[j].normal);
               deltapos[k]   :=deltapos[k]   +VectorSpacing(curRow.node[n].pos,    prevRow.node[j].pos);
            end;
         end;
         //Search minimum
         // only search in deltapos, if i would search in deltanormal,
         // the same index of minimum would be found
         m:=0;
         for k:=1 to Slices-1 do
            if deltapos[m]>deltapos[k] then m:=k;
         // rotate count
         for k:=1 to m do begin
            // rotate the values of curRow
            curRow.node[Slices]:=curRow.node[0];
            System.Move(curRow.node[1], curRow.node[0], SizeOf(TNodeData)*Slices);
            curRow.node[Slices]:=curRow.node[0];
         end;
      end;
      // go on
      glBegin(GL_TRIANGLE_STRIP);
         if NodesColorMode<>pncmNone then
            glColor4fv(@prevRow.color);
         glNormal3fv(@prevRow.node[0].normal);
         glVertex3fv(@prevRow.node[0].pos);
         for j:=0 to Slices-1 do begin
            if NodesColorMode<>pncmNone then
               glColor4fv(@curRow.color);
            glNormal3fv(@curRow.node[j].normal);
            glVertex3fv(@curRow.node[j].pos);
            if NodesColorMode<>pncmNone then
               glColor4fv(@prevRow.color);
            glNormal3fv(@prevRow.node[j+1].normal);
            glVertex3fv(@prevRow.node[j+1].pos);
         end;
         if NodesColorMode<>pncmNone then
            glColor4fv(@curRow.color);
         glNormal3fv(@curRow.node[Slices].normal);
         glVertex3fv(@curRow.node[Slices].pos);
      glEnd;
   end;

{   procedure RenderSides(var curRow, prevRow : array of TNodeData);
   var
      j : Integer;
   begin
      glBegin(GL_TRIANGLE_STRIP);
         glNormal3fv(@prevRow[0].normal);
         glVertex3fv(@prevRow[0].pos);
         for j:=0 to Slices-1 do begin
            glNormal3fv(@curRow[j].normal);
            glVertex3fv(@curRow[j].pos);
            glNormal3fv(@prevRow[j+1].normal);
            glVertex3fv(@prevRow[j+1].pos);
         end;
         glNormal3fv(@curRow[Slices].normal);
         glVertex3fv(@curRow[Slices].pos);
      glEnd;
   end;}

var
   i, curRow, nbDivisions, k : Integer;
   normal, splinePos : TAffineVector;
   rows : array [0..1] of TRowData;
   ra : PFloatArray;
   posSpline, rSpline : TCubicSpline;
   f, t : Single;
begin
   FTriangleCount:=0;
   if Nodes.Count=0 then Exit;
   SetLength(rows[0].node, Slices+1);
   SetLength(rows[1].node, Slices+1);
   if (Length(vSinCache)<>Slices+1) or (Length(vCosCache)<>Slices+1) then begin
      SetLength(vSinCache, Slices+1);
      SetLength(vCosCache, Slices+1);
      PrepareSinCosCache(vSinCache, vCosCache, 0, 360);
   end;
   if (SplineMode=lsmCubicSpline) and (Nodes.Count>1) then begin
      // create position spline
      posSpline:=Nodes.CreateNewCubicSpline;
      // create radius spline
      GetMem(ra, SizeOf(TGLFloat)*Nodes.Count);
      for i:=0 to Nodes.Count-1 do
         ra[i]:=TGLPipeNode(Nodes[i]).RadiusFactor;
      rSpline:=TCubicSpline.Create(ra, nil, nil, nil, Nodes.Count);
      FreeMem(ra);
      normal:=posSpline.SplineSlopeVector(0);
   end else begin
      normal:=Nodes.Vector(0);
      posSpline:=nil;
      rSpline:=nil;
   end;
   if NodesColorMode<>pncmNone then begin
      glPushAttrib(GL_ENABLE_BIT);
      glEnable(GL_COLOR_MATERIAL);
      glColorMaterial(GL_FRONT_AND_BACK, cPNCMtoEnum[NodesColorMode]);
   end;
   CalculateRow(@rows[0], PAffineVector(@Nodes[0].AsVector)^, normal,
                TGLPipeNode(Nodes[0]).RadiusFactor);
   rows[0].color:=TGLPipeNodes(Nodes)[0].Color.Color;
   if ppStartDisk in Parts then begin
      NegateVector(normal);
      RenderDisk(@rows[0], Nodes[0].AsVector, normal, True);
      FTriangleCount:=FTriangleCount+Slices+1;
   end;
   if (Nodes.Count>1) then begin
      if SplineMode=lsmCubicSpline then begin
         f:=1/Division;
         nbDivisions:=(Nodes.Count-1)*Division;
         for i:=1 to nbDivisions do begin
            t:=i*f;
            posSpline.SplineAffineVector(t, splinePos);
            normal:=posSpline.SplineSlopeVector(t);
            NormalizeVector(normal);
            curRow:=(i and 1);
            CalculateRow(@rows[curRow], splinePos, normal,
                         rSpline.SplineX(t));
            if NodesColorMode<>pncmNone then begin
               k:=Trunc(t);
               if k<Nodes.Count-1 then
                  rows[curRow].color:=VectorLerp(TGLPipeNodes(Nodes)[k].Color.Color,
                                                 TGLPipeNodes(Nodes)[k+1].Color.Color,
                                                 Frac(t))
               else rows[curRow].color:=TGLPipeNodes(Nodes)[k].Color.Color;
            end;
            //rows[curRow].color:=TGLPipeNodes(Nodes)[curRow].Color.Color;
            if ppOutside in Parts then
               RenderSides(@rows[curRow xor 1], @rows[curRow]);
            if ppInside in Parts then
               RenderSides(@rows[curRow], @rows[curRow xor 1]);
         end;
         i:=nbDivisions*(Slices+1)*2;
         if ppOutside in Parts then Inc(FTriangleCount, i);
         if ppInside in Parts then Inc(FTriangleCount, i);
      end else begin
         for i:=1 to Nodes.Count-1 do begin
            curRow:=(i and 1);
            CalculateRow(@rows[curRow], PAffineVector(@Nodes[i].AsVector)^,
                         Nodes.Vector(i), TGLPipeNode(Nodes[i]).RadiusFactor);
            rows[curRow].color:=TGLPipeNodes(Nodes)[i].Color.Color;
            if ppOutside in Parts then
               RenderSides(@rows[curRow xor 1], @rows[curRow]);
            if ppInside in Parts then
               RenderSides(@rows[curRow], @rows[curRow xor 1]);
         end;
         i:=Nodes.Count*(Slices+1)*2;
         if ppOutside in Parts then Inc(FTriangleCount, i);
         if ppInside in Parts then Inc(FTriangleCount, i);
      end;
   end;
   if ppStopDisk in Parts then begin
      i:=Nodes.Count-1;
      if SplineMode=lsmCubicSpline then
         normal:=posSpline.SplineSlopeVector(Nodes.Count-1)
      else normal:=Nodes.Vector(i);
      CalculateRow(@rows[0], PAffineVector(@Nodes[i].AsVector)^, normal,
                   TGLPipeNode(Nodes[i]).RadiusFactor);
      rows[0].color:=TGLPipeNodes(Nodes)[i].Color.Color;
      RenderDisk(@rows[0], Nodes[i].AsVector, normal, False);
      FTriangleCount:=FTriangleCount+Slices+1;
   end;
   if SplineMode=lsmCubicSpline then begin
      posSpline.Free;
      rSpline.Free;
   end;
   if (NodesColorMode<>pncmNone) then
      glPopAttrib;
end;

// ------------------
// ------------------ TGLExtrusionSolid ------------------
// ------------------

// Assign
//
procedure TGLExtrusionSolid.Assign(Source: TPersistent);
begin
   if Source is TGLExtrusionSolid then begin
      FStacks:=TGLExtrusionSolid(Source).FStacks;
      FNormals:=TGLExtrusionSolid(Source).FNormals;
      FNormalDirection:=TGLExtrusionSolid(Source).FNormalDirection;
      FParts:=TGLExtrusionSolid(Source).FParts;
   end;
   inherited;
end;

// BuildList
//
procedure TGLExtrusionSolid.BuildList(var rci: TRenderContextInfo);
var
   deltaS, deltaZ : Single;
   lastNormal : TAffineVector;

   procedure CalcNormal(const Top, Bottom : TAffineVector; var normal : TAffineVector);
   { extrusion is in Z direction, so the Z component of the normal vector is
     always zero. }
   {var
     p : TAffineVector;}
   begin
      normal[0]:=Bottom[1] - Top[1];
      normal[1]:=Top[0] - Bottom[0];
      normal[2]:=0;
      NormalizeVector(normal);
      if FHeight<0 then NegateVector(normal);
      (*
      p:=Top; p[2]:=p[2] + FHeight;
      CalcPlaneNormal(top,bottom,p,normal);
      *)
   end;

   procedure BuildStep(ptTop, ptBottom : TAffineVector; invertNormals : Boolean;
                       topT, bottomT : Single);
   var
      step : Integer;
      topBase, topNext, bottomBase, bottomNext, normal, normTop, normBottom : TAffineVector;
      topTPBase, topTPNext, bottomTPBase, bottomTPNext : TTexPoint;
      ptBuffer : TAffineVector;
      angle: Double;
   begin
      // to invert normals, we just need to flip top & bottom
      if invertNormals then begin
         ptBuffer:=ptTop;
         ptTop:=ptBottom;
         ptBottom:=ptBuffer;
      end;
      // generate triangle strip for a level
      // TODO : support for triangle fans (when ptTop or ptBottom is on the Y Axis)
      topTPBase.S:=0;         bottomTPBase.S:=0;
      topTPBase.T:=topT;      bottomTPBase.T:=bottomT;
      topBase:=ptTop;
      bottomBase:=ptBottom;
      //topBase[2]:=FHeight;
      //bottomBase[2]:=FHeight;
      CalcNormal(topBase,bottomBase,normal);
      if (FNormals=nsFlat) then
         lastNormal:=normal
      else if (FNormals=nsSmooth) then begin
         angle:=VectorDotProduct(normal,lastNormal);
         if (angle<FMinSmoothAngleCos) then begin
            lastNormal:=normal;
         end;
      end;
      if invertNormals then begin
         normTop:=Normal;
         normBottom:=lastnormal;
      end else begin
         normTop:=lastNormal;
         normBottom:=normal;
      end;
      lastNormal:=normal;
      topNext:=topBase;
      bottomNext:=bottomBase;
      topTPNext:=topTPBase;
      bottomTPNext:=bottomTPBase;
      glBegin(GL_TRIANGLE_STRIP);
      glNormal3fv(@normTop);
      xglTexCoord2fv(@topTPBase);
      glVertex3fv(@topBase);
      for step:=1 to FStacks do begin
         glNormal3fv(@normBottom);
         xglTexCoord2fv(@bottomTPBase);
         glVertex3fv(@bottomBase);
         topTPNext.S:=step*deltaS;
         bottomTPNext.S:=step*deltaS;
         topNext[2]:=step*DeltaZ;
         bottomNext[2]:=topNext[2];
         xglTexCoord2fv(@topTPNext);
         glNormal3fv(@normTop);
         glVertex3fv(@topNext);
         topBase:=topNext;          topTPBase:=topTPNext;
         bottomBase:=bottomNext;    bottomTPBase:=bottomTPNext;
      end;
      glNormal3fv(@normBottom);
      xglTexCoord2fv(@bottomTPBase);
      glVertex3fv(@bottomBase);
      glEnd;
   end;

var
   n, i: Integer;
   invertedNormals : Boolean;
   normal : TAffineVector;
begin
   if Outline.Count<1 then Exit;
   deltaZ:=FHeight/FStacks;
   deltaS:=1/FStacks;
   invertedNormals:=(FNormalDirection=ndInside);
   FTriangleCount:=0;
   // generate sides
   if (FHeight<>0) and ((espInside in FParts) or (espOutside in FParts)) then begin
      for n:=0 to Outline.Count-1 do begin
         with Outline.List[n] do if count>1 then begin
            if espInside in Parts then begin
               CalcNormal(List[count-1],List[0],lastNormal);
               if not InvertedNormals then
                  NegateVector(lastNormal);
               for i:=0 to Count-2 do begin
                  BuildStep(List[i], List[i+1], not invertedNormals,
                            i/(Count-1), (i+1)/(Count-1));
               end;
               BuildStep(List[count-1], List[0], not invertedNormals, 1, 0);
            end;
            if espOutside in Parts then begin
               CalcNormal(List[count-1], List[0], lastNormal);
               if InvertedNormals then
                  NegateVector(lastNormal);
               for i:=0 to Count-2 do begin
                  BuildStep(List[i],List[i+1], invertedNormals,
                            i/(Count-1), (i+1)/(Count-1));
               end;
               BuildStep(List[count-1],List[0], invertedNormals, 1, 0);
            end;
         end;
      end;
      xglTexCoord2fv(@NullTexPoint);
   end;
   // tessellate start/stop polygons
   if (espStartPolygon in FParts) or (espStopPolygon in FParts) then begin
      normal:=ContoursNormal;
      // tessellate stop polygon
      if espStopPolygon in FParts then begin
         glPushMatrix;
         glTranslatef(0, 0, FHeight);
         RenderTesselatedPolygon(true, @normal, invertedNormals);
         glPopMatrix;
      end;
      // tessellate start polygon
      if espStartPolygon in FParts then begin
         NegateVector(normal);
         RenderTesselatedPolygon(true, @normal, not invertedNormals);
      end;
   end;
end;

// Create
//
constructor TGLExtrusionSolid.Create(AOwner : TComponent);
begin
   inherited;
   FHeight:=1;
   FStacks:=1;
   FNormals:=nsFlat;
   FNormalDirection:=ndOutside;
   FParts:=[espOutside];
   MinSmoothAngle:=5;
end;

// Destroy
//
destructor TGLExtrusionSolid.Destroy;
begin
   inherited;
end;

// SetHeight
//
procedure TGLExtrusionSolid.SetHeight(const Value : TGLFloat);
begin
   if (Value<>FHeight) then begin
      FHeight:=Value;
      StructureChanged;
   end;
end;

// SetMinSmoothAngle
//
procedure TGLExtrusionSolid.SetMinSmoothAngle(const Value : Single);
var
   s, c : Single;
begin
   FMinSmoothAngle:=Value;
   SinCos(Value*cPidiv180,s,c);
   FMinSmoothAngleCos:=c;
end;

// SetNormalDirection
//
procedure TGLExtrusionSolid.SetNormalDirection(const val : TNormalDirection);
begin
   if FNormalDirection<>val then begin
      FNormalDirection:=val;
      StructureChanged;
   end;
end;

// SetNormals
//
procedure TGLExtrusionSolid.SetNormals(const val : TNormalSmoothing);
begin
   if FNormals<>val then begin
      FNormals:=val;
      StructureChanged;
   end;
end;

// SetParts
//
procedure TGLExtrusionSolid.SetParts(const val : TExtrusionSolidParts);
begin
   if FParts<>val then begin
      FParts:=val;
      StructureChanged;
   end;
end;

// SetStacks
//
procedure TGLExtrusionSolid.SetStacks(const val : Integer);
begin
   if (val<>FStacks) and (val>0) then begin
      FStacks:=val;
      StructureChanged;
   end;
end;

// ------------------------------------------------------------------
// ------------------------------------------------------------------
// ------------------------------------------------------------------
initialization
// ------------------------------------------------------------------
// ------------------------------------------------------------------
// ------------------------------------------------------------------

	// class registrations
   RegisterClasses([TGLRevolutionSolid, TGLExtrusionSolid, TGLPipe]);

end.

