//
// This unit is part of the GLScene Engine, http://glscene.org
//

unit GLExtrusion;

(*
   Extrusion objects for GLScene. Extrusion objects are solids defined by the
   surface described by a moving curve.
  TODO  ur: Suggestion:
       All extrusion objects use actually the same kind of "parts",
       one common type should do.
*)


interface

{$I GLScene.inc}

uses
  Winapi.OpenGL,
  System.Classes,
  System.SysUtils,
  System.Math,

  OpenGLTokens,
  GLContext,
  GLObjects,
  GLScene,
  GLMultiPolygon,
  GLColor,
  GLVectorGeometry,
  GLRenderContextInfo,
  GLNodes,
  GLState,
  GLVectorTypes;

type

  TGLExtrusionSolidPart = (espOutside, espInside, espStartPolygon, espStopPolygon);
  TGLExtrusionSolidParts = set of TGLExtrusionSolidPart;

  TGLRevolutionSolidPart = (rspOutside, rspInside, rspStartPolygon, rspStopPolygon);
  TGLRevolutionSolidParts = set of TGLRevolutionSolidPart;

  (* A solid object generated by rotating a curve along the Y axis.
     The curve is described by the Nodes and SplineMode properties, and it is
     rotated in the trigonometrical direction (CCW when seen from Y->INF).
     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.
     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
    FSlices: Integer;
    FStartAngle, FStopAngle: Single;
    FNormals: TGLNormalSmoothing;
    FYOffsetPerTurn: Single;
    FTriangleCount: Integer;
    FNormalDirection: TGLNormalDirection;
    FParts: TGLRevolutionSolidParts;
    FAxisAlignedDimensionsCache: TVector;
  protected
    procedure SetStartAngle(const val: Single);
    procedure SetStopAngle(const val: Single);
    function StoreStopAngle: Boolean;
    procedure SetSlices(const val: Integer);
    procedure SetNormals(const val: TGLNormalSmoothing);
    procedure SetYOffsetPerTurn(const val: Single);
    procedure SetNormalDirection(const val: TGLNormalDirection);
    procedure SetParts(const val: TGLRevolutionSolidParts);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Assign(Source: TPersistent); override;
    procedure BuildList(var rci: TGLRenderContextInfo); override;
    {Number of triangles used for rendering. }
    property TriangleCount: Integer read FTriangleCount;
    function AxisAlignedDimensionsUnscaled: TVector; override;
    procedure StructureChanged; override;
  published
    (* Parts of the rotation solid to be generated for rendering.
     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.
     Start/StopPolygon are tesselated from the curve (considered as closed). *)
    property Parts: TGLRevolutionSolidParts 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.
       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.
       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 (360deg).
    property Slices: Integer read FSlices write SetSlices default 16;

    property Normals: TGLNormalSmoothing read FNormals write SetNormals default
      nsFlat;
    property NormalDirection: TGLNormalDirection read FNormalDirection write
      SetNormalDirection default ndOutside;
  end;

  (* Extrudes a complex Polygon into Z direction.
    For contour description see TGLMultiPolygonBase.
    properties Parts, Height (or should we better cal it Depth, because its in Z?),
    Stacks, Normals and NormalDirection are equivalent to TGLRevolutionSolid.
    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(TGLMultiPolygonBase)
  private
    FStacks: Integer;
    FNormals: TGLNormalSmoothing;
    FTriangleCount: Integer;
    FNormalDirection: TGLNormalDirection;
    FParts: TGLExtrusionSolidParts;
    FHeight: TGLFloat;
    FMinSmoothAngle: Single;
    FMinSmoothAngleCos: Single;
    FAxisAlignedDimensionsCache: TVector;
    procedure SetHeight(const Value: TGLFloat);
    procedure SetMinSmoothAngle(const Value: Single);
  protected
    procedure SetStacks(const val: Integer);
    procedure SetNormals(const val: TGLNormalSmoothing);
    procedure SetNormalDirection(const val: TGLNormalDirection);
    procedure SetParts(const val: TGLExtrusionSolidParts);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Assign(Source: TPersistent); override;
    procedure BuildList(var rci: TGLRenderContextInfo); override;
    // Number of triangles used for rendering.
    property TriangleCount: Integer read FTriangleCount;
    function AxisAlignedDimensionsUnscaled: TVector; override;
    procedure StructureChanged; override;
  published
    property Parts: TGLExtrusionSolidParts read FParts write SetParts default [espOutside];
    property Height: TGLFloat read FHeight write SetHeight;
    property Stacks: Integer read FStacks write SetStacks default 1;
    property Normals: TGLNormalSmoothing read FNormals write SetNormals default nsFlat;
    property NormalDirection: TGLNormalDirection read FNormalDirection write SetNormalDirection default ndOutside;
    property MinSmoothAngle: Single read FMinSmoothAngle write SetMinSmoothAngle;
  end;

  TGLPipeNode = class(TGLNode)
  private
    FRadiusFactor: Single;
    FColor: TGLColor;
    FTexCoordT: Single;
  protected
    function GetDisplayName: string; override;
    procedure SetRadiusFactor(const val: Single);
    function StoreRadiusFactor: Boolean;
    procedure SetColor(const val: TGLColor);
    procedure ColorChanged(sender: TObject);
    function StoreTexCoordT: Boolean;
  public
    constructor Create(Collection: TCollection); override;
    destructor Destroy; override;
    procedure Assign(Source: TPersistent); override;
  published
    property RadiusFactor: Single read FRadiusFactor write SetRadiusFactor stored
      StoreRadiusFactor;
    property Color: TGLColor read FColor write SetColor;
    property TexCoordT: Single read FTexCoordT write FTexCoordT stored
      StoreTexCoordT;
  end;

  TGLPipeNodes = class(TGLLinesNodes)
  protected
    procedure SetItems(index: Integer; const val: TGLPipeNode);
    function GetItems(index: Integer): TGLPipeNode;
  public
    constructor Create(AOwner: TComponent);
    function Add: TGLPipeNode;
    function FindItemID(ID: Integer): TGLPipeNode;
    property Items[index: Integer]: TGLPipeNode read GetItems write SetItems; default;
  end;

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

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

  TPipeTexCoordMode = (ptcmDefault, ptcmManual);

  TPipeNormalMode = (pnmDefault, pnmAdvanced);

  (* A solid object generated by extruding a circle along a trajectory.
   Texture coordinates NOT supported yet. *)
  TGLPipe = class(TGLPolygonBase)
  private
    FSlices: Integer;
    FParts: TPipeParts;
    FTriangleCount: Integer;
    FRadius: Single;
    FNodesColorMode: TPipeNodesColorMode;
    FTextCoordMode: TPipeTexCoordMode;
    FTextCoordTileS: Single;
    FTextCoordTileT: Single;
    FNormalMode: TPipeNormalMode;
    FNormalSmoothAngle: Single;
  protected
    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);
    procedure SetTextCoordMode(const val: TPipeTexCoordMode);
    procedure SetTextCoordTileS(const val: Single);
    procedure SetTextCoordTileT(const val: Single);
    function StoreTextCoordTileS: Boolean;
    function StoreTextCoordTileT: Boolean;
    procedure SetNormalMode(const val: TPipeNormalMode);
    procedure SetNormalSmoothAngle(const val: Single);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Assign(Source: TPersistent); override;
    procedure BuildList(var rci: TGLRenderContextInfo); override;
    // Number of triangles used for rendering.
    property TriangleCount: Integer read FTriangleCount;
  published
    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;
    property TexCoordMode: TPipeTexCoordMode read FTextCoordMode
      write SetTextCoordMode default ptcmDefault;
    property TexCoordTileS: Single read FTextCoordTileS write SetTextCoordTileS
      stored StoreTextCoordTileS;
    property TexCoordTileT: Single read FTextCoordTileT write SetTextCoordTileT
      stored StoreTextCoordTileT;
    property NormalMode: TPipeNormalMode read FNormalMode write SetNormalMode
      default pnmDefault;
    property NormalSmoothAngle: Single read FNormalSmoothAngle write
      SetNormalSmoothAngle;
  end;

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

uses
  GLSpline,
  GLVectorLists,
  XOpenGL;

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

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

destructor TGLRevolutionSolid.Destroy;
begin
  inherited Destroy;
end;

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

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

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

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

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

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

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

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

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;

procedure TGLRevolutionSolid.BuildList(var rci: TGLRenderContextInfo);
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^.X + ptTop^.X;
    mz := ptBottom^.Z + ptTop^.Z;
    VectorSubtract(ptBottom^, ptTop^, tb);
    normal.X := -tb.Y * mx;
    normal.Y := mx * tb.X + mz * tb.Z;
    normal.Z := -mz * tb.Y;
    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.Y := topBase.Y + yOffset;
      bottomBase.Y := bottomBase.Y + yOffset;
      yOffset := yOffset + deltaYOffset;
    end;
    CalcNormal(@topBase, @bottomBase, normal);
    SetLocalNormals;
    inc(i);
    topTPNext := topTPBase;
    bottomTPNext := bottomTPBase;
    gl.Begin_(GL_TRIANGLE_STRIP);
    gl.Normal3fv(@topNormal);
    xgl.TexCoord2fv(@topTPBase);
    gl.Vertex3fv(@topBase);
    while alpha < stopAlpha do
    begin
      gl.Normal3fv(@bottomNormal);
      xgl.TexCoord2fv(@bottomTPBase);
      gl.Vertex3fv(@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.Y := topNext.Y + yOffset;
        bottomNext.Y := bottomNext.Y + yOffset;
        yOffset := yOffset + deltaYOffset
      end;
      CalcNormal(@topNext, @bottomNext, normal);
      SetLocalNormals;
      inc(i);
      xgl.TexCoord2fv(@topTPNext);
      gl.Normal3fv(@topNormal);
      gl.Vertex3fv(@topNext);
      alpha := nextAlpha;
      topBase := topNext;
      topTPBase := topTPNext;
      bottomBase := bottomNext;
      bottomTPBase := bottomTPNext;
    end;
    gl.Normal3fv(@bottomNormal);
    xgl.TexCoord2fv(@bottomTPBase);
    gl.Vertex3fv(@bottomBase);
    gl.End_;
    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;
      xgl.TexCoord2fv(@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.Y := 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;

function TGLRevolutionSolid.AxisAlignedDimensionsUnscaled: TVector;
var
  maxRadius: Single;
  maxHeight: Single;
  i: integer;
begin
  maxRadius := 0;
  maxHeight := 0;
  if FAxisAlignedDimensionsCache.X < 0 then
  begin
    for i := 0 to Nodes.Count - 1 do
    begin
      maxHeight := MaxFloat(maxHeight, Abs(Nodes[i].Y));
      maxRadius := MaxFloat(maxRadius, Sqr(Nodes[i].X) + Sqr(Nodes[i].Z));
    end;
    maxRadius := sqrt(maxRadius);
    FAxisAlignedDimensionsCache.X := maxRadius;
    FAxisAlignedDimensionsCache.Y := maxHeight;
    FAxisAlignedDimensionsCache.Z := maxRadius;
  end;
  SetVector(Result, FAxisAlignedDimensionsCache);
end;

procedure TGLRevolutionSolid.StructureChanged;
begin
  FAxisAlignedDimensionsCache.X := -1;
  inherited;
end;

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

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

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

procedure TGLPipeNode.Assign(Source: TPersistent);
begin
  if Source is TGLPipeNode then
  begin
    RadiusFactor := TGLPipeNode(Source).FRadiusFactor;
    Color.DirectColor := TGLPipeNode(Source).Color.DirectColor;
    TexCoordT := TGLPipeNode(Source).FTexCoordT;
  end;
  inherited;
end;

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

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

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

function TGLPipeNode.StoreTexCoordT: Boolean;
begin
  Result := (FTexCoordT <> 1.0);
end;

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

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 ------------------
// ------------------

constructor TGLPipe.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FSlices := 16;
  FParts := [ppOutside];
  FRadius := 1.0;
  FTriangleCount := 0;
  FTextCoordMode := ptcmDefault;
  FTextCoordTileS := 1;
  FTextCoordTileT := 1;
  FNormalMode := pnmDefault;
  FNormalSmoothAngle := 0;
end;

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

 
destructor TGLPipe.Destroy;
begin
  inherited Destroy;
end;

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

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

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

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

function TGLPipe.StoreTextCoordTileS: Boolean;
begin
  Result := (FTextCoordTileS <> 1.0);
end;

function TGLPipe.StoreTextCoordTileT: Boolean;
begin
  Result := (FTextCoordTileT <> 1.0);
end;

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

procedure TGLPipe.SetTextCoordMode(const val: TPipeTexCoordMode);
begin
  if val <> FTextCoordMode then
  begin
    FTextCoordMode := val;
    StructureChanged;
  end;
end;

procedure TGLPipe.SetTextCoordTileS(const val: Single);
begin
  if val <> FTextCoordTileS then
  begin
    FTextCoordTileS := val;
    StructureChanged;
  end;
end;

procedure TGLPipe.SetTextCoordTileT(const val: Single);
begin
  if val <> FTextCoordTileT then
  begin
    FTextCoordTileT := val;
    StructureChanged;
  end;
end;

procedure TGLPipe.SetNormalMode(const val: TPipeNormalMode);
begin
  if val <> FNormalMode then
  begin
    FNormalMode := val;
    StructureChanged;
  end;
end;

procedure TGLPipe.SetNormalSmoothAngle(const val: Single);
begin
  if val <> FNormalSmoothAngle then
  begin
    FNormalSmoothAngle := val;
    if NormalMode = pnmAdvanced then
      StructureChanged;
  end;
end;

procedure TGLPipe.Assign(Source: TPersistent);
begin
  if Source is TGLPipe then
  begin
    Slices := TGLPipe(Source).Slices;
    Parts := TGLPipe(Source).Parts;
    Radius := TGLPipe(Source).Radius;
    NodesColorMode := TGLPipe(Source).NodesColorMode;
    TexCoordMode := TGLPipe(Source).TexCoordMode;
    TexCoordTileS := TGLPipe(Source).TexCoordTileS;
    TexCoordTileT := TGLPipe(Source).TexCoordTileT;
  end;
  inherited;
end;

var
  vSinCache, vCosCache: array of Single;

procedure TGLPipe.BuildList(var rci: TGLRenderContextInfo);
type
  TNodeData = record
    pos: TAffineVector;
    normal: TAffineVector;
    innormal: TAffineVector;
    sidedir: TVector3f;
  end;
  TRowData = record
    node: array of TNodeData;
    color: TColorVector;
    center: TVector3f;
    textcoordT: Single;
  end;
  PRowData = ^TRowData;
const
  cPNCMtoEnum: array[pncmEmission..pncmAmbientAndDiffuse] of Cardinal =
    (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);
      SetVector(row^.node[i].sidedir, 0, 0, 0);
    end;
    row^.center := center;
  end;

  procedure RenderDisk(row: PRowData;
    const center: TVector; const normal: TAffineVector;
    invert: Boolean; TextCoordTileS: Single);
  var
    i: Integer;
  begin
    begin
      if NodesColorMode <> pncmNone then
        gl.Color4fv(@row^.color);
      // it was necessary to change build process to generate textcoords
      gl.Begin_(GL_TRIANGLE_STRIP);
      gl.Normal3fv(@normal);

      case TexCoordMode of
        ptcmDefault, ptcmManual:
          begin
            if invert then
            begin
              for i := 0 to High(row^.node) - 1 do
              begin
                gl.TexCoord2f(i / (High(row^.node)) * TextCoordTileS, 1);
                gl.Vertex3fv(@row^.node[i].pos);
                gl.TexCoord2f(i / (High(row^.node)) * TextCoordTileS, 0);
                gl.Vertex3fv(@center);
              end;
              gl.TexCoord2f(TextCoordTileS, 1);
              gl.Vertex3fv(@row^.node[High(row^.node)].pos);
            end
            else
            begin
              for i := High(row^.node) downto 1 do
              begin
                gl.TexCoord2f(i / (High(row^.node)) * TextCoordTileS, 0);
                gl.Vertex3fv(@row^.node[i].pos);
                gl.TexCoord2f(i / (High(row^.node)) * TextCoordTileS, 1);
                gl.Vertex3fv(@center);
              end;
              gl.TexCoord2f(0, 0);
              gl.Vertex3fv(@row^.node[0].pos);
            end;
          end;
      end;

      gl.End_;
    end;
  end;

  procedure CalculateSides(prevRow, curRow: PRowData; const trajvec: TVector3f);
  var
    j, k, m, n: Integer;
    deltaNormal, deltaPos: array of Double;
    smoothanglerad: Single;
  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;

    case NormalMode of
      pnmDefault:
        begin
          for j := 0 to Slices do
          begin
            curRow.node[j].innormal := VectorNegate(curRow.node[j].normal);
            prevRow.node[j].innormal := VectorNegate(prevRow.node[j].normal);
          end;
        end;
      pnmAdvanced:
        begin
          smoothanglerad := DegToRadian(NormalSmoothAngle);
          for j := 0 to Slices do
          begin
            curRow.node[j].sidedir :=
              VectorNormalize(VectorSubtract(curRow.node[j].pos,
              prevRow.node[j].pos));
            if VectorDotProduct(curRow.node[j].sidedir, prevRow.node[j].sidedir)
              < Cos(smoothanglerad) then
            begin
              if VectorDotProduct(curRow.node[j].sidedir, VectorNormalize(
                VectorSubtract(curRow.node[j].pos, curRow.center))) > 0.99 then
              begin
                curRow.node[j].normal :=
                  VectorCrossProduct(curRow.node[j].sidedir,
                  VectorCrossProduct(curRow.node[j].sidedir,
                    VectorNormalize(trajvec)));
                prevRow.node[j].normal :=
                  VectorCrossProduct(curRow.node[j].sidedir,
                  VectorCrossProduct(curRow.node[j].sidedir,
                    VectorNormalize(trajvec)));
              end
              else
              begin
                if VectorDotProduct(curRow.node[j].sidedir, VectorNormalize(
                  VectorSubtract(curRow.node[j].pos, curRow.center))) < -0.99
                    then
                begin
                  curRow.node[j].normal := VectorCrossProduct(VectorCrossProduct
                    (curRow.node[j].sidedir, VectorNormalize(trajvec)),
                      curRow.node[j].sidedir);
                  prevRow.node[j].normal := VectorCrossProduct(VectorCrossProduct
                    (curRow.node[j].sidedir, VectorNormalize(trajvec)),
                      curRow.node[j].sidedir);
                end
                else
                begin
                  if VectorDotProduct(trajvec, curRow.node[j].sidedir) < 0 then
                  begin
                    curRow.node[j].normal :=
                      VectorCrossProduct(VectorNormalize(VectorCrossProduct
                      (VectorNormalize(VectorSubtract(curRow.node[j].pos,
                        curRow.center)),
                      curRow.node[j].sidedir)), curRow.node[j].sidedir);
                    prevRow.node[j].normal :=
                      VectorCrossProduct(VectorNormalize(VectorCrossProduct
                      (VectorNormalize(VectorSubtract(prevRow.node[j].pos,
                        prevRow.center)),
                      curRow.node[j].sidedir)), curRow.node[j].sidedir);
                  end
                  else
                  begin
                    curRow.node[j].normal :=
                      VectorCrossProduct(curRow.node[j].sidedir, VectorNormalize
                      (VectorCrossProduct(VectorNormalize(VectorSubtract(curRow.node[j].pos, curRow.center)),
                      curRow.node[j].sidedir)));
                    prevRow.node[j].normal :=
                      VectorCrossProduct(curRow.node[j].sidedir, VectorNormalize
                      (VectorCrossProduct(VectorNormalize(VectorSubtract(prevRow.node[j].pos, prevRow.center)),
                      curRow.node[j].sidedir)));
                  end;
                end;
                if VectorLength(curRow.node[j].normal) = 0 then
                  curRow.node[j].normal := prevRow.node[j].normal;
                if VectorLength(prevRow.node[j].normal) = 0 then
                  prevRow.node[j].normal := curRow.node[j].normal;
                //compute inside normales
                curRow.node[j].innormal := VectorNegate(curRow.node[j].normal);
                prevRow.node[j].innormal :=
                  VectorNegate(prevRow.node[j].normal);
              end;
            end
            else
            begin
              if VectorDotProduct(curRow.node[j].sidedir,
                VectorNormalize(VectorSubtract
                (curRow.node[j].pos, curRow.center))) > 0.99 then
              begin
                curRow.node[j].normal :=
                  VectorCrossProduct(curRow.node[j].sidedir,
                  VectorCrossProduct(curRow.node[j].sidedir,
                    VectorNormalize(trajvec)));
              end
              else
              begin
                if VectorDotProduct(curRow.node[j].sidedir, VectorNormalize(
                  VectorSubtract(curRow.node[j].pos, curRow.center))) < -0.99
                    then
                begin
                  curRow.node[j].normal := VectorCrossProduct(VectorCrossProduct
                    (curRow.node[j].sidedir, VectorNormalize(trajvec)),
                      curRow.node[j].sidedir);
                end
                else
                begin
                  if VectorDotProduct(trajvec, curRow.node[j].sidedir) < 0 then
                  begin
                    curRow.node[j].normal :=
                      VectorCrossProduct(VectorNormalize(VectorCrossProduct
                      (VectorNormalize(VectorSubtract(curRow.node[j].pos,
                        curRow.center)),
                      curRow.node[j].sidedir)), curRow.node[j].sidedir);
                  end
                  else
                  begin
                    curRow.node[j].normal :=
                      VectorCrossProduct(curRow.node[j].sidedir, VectorNormalize
                      (VectorCrossProduct(VectorNormalize(VectorSubtract(curRow.node[j].pos,
                      curRow.center)), curRow.node[j].sidedir)));
                  end;
                end;
                //compute inside normales
                curRow.node[j].innormal := VectorNegate(curRow.node[j].normal);
              end;
            end;
          end;
        end;
    end;
  end;

  procedure RenderSides(prevRow, curRow: PRowData; TextCoordTileS,
    TextCoordTileT: Single; outside: Boolean);
  var
    j: Integer;
  begin
    begin
      gl.Begin_(GL_TRIANGLE_STRIP);
      if outside then
      begin
        if NodesColorMode <> pncmNone then
          gl.Color4fv(@curRow^.color);

        gl.TexCoord2f(0, curRow^.textcoordT * TextCoordTileT);
        gl.Normal3fv(@curRow^.node[0].normal);
        gl.Vertex3fv(@curRow^.node[0].pos);
        for j := 0 to Slices - 1 do
        begin
          if NodesColorMode <> pncmNone then
            gl.Color4fv(@prevRow^.color);
          gl.TexCoord2f(j / Slices * TextCoordTileS, prevRow^.textcoordT *
            TextCoordTileT);
          gl.Normal3fv(@prevRow^.node[j].normal);
          gl.Vertex3fv(@prevRow^.node[j].pos);
          if NodesColorMode <> pncmNone then
            gl.Color4fv(@curRow^.color);
          gl.TexCoord2f((j + 1) / Slices * TextCoordTileS, curRow^.textcoordT *
            TextCoordTileT);
          gl.Normal3fv(@curRow^.node[j + 1].normal);
          gl.Vertex3fv(@curRow^.node[j + 1].pos);
        end;
        if NodesColorMode <> pncmNone then
          gl.Color4fv(@prevRow^.color);
        gl.TexCoord2f(TextCoordTileS, prevRow^.textcoordT * TextCoordTileT);
        gl.Normal3fv(@prevRow^.node[Slices].normal);
        gl.Vertex3fv(@prevRow^.node[Slices].pos);
      end
      else
      begin
        for j := 0 to Slices do
        begin
          curRow.node[j].innormal := VectorNegate(curRow.node[j].normal);
          prevRow.node[j].innormal := VectorNegate(prevRow.node[j].normal);
        end;
        if NodesColorMode <> pncmNone then
          gl.Color4fv(@prevRow^.color);

        gl.TexCoord2f(0, prevRow^.textcoordT * TextCoordTileT);
        gl.Normal3fv(@prevRow^.node[0].innormal);
        gl.Vertex3fv(@prevRow^.node[0].pos);
        for j := 0 to Slices - 1 do
        begin
          if NodesColorMode <> pncmNone then
            gl.Color4fv(@curRow^.color);
          gl.TexCoord2f(j / Slices * TextCoordTileS, curRow^.textcoordT *
            TextCoordTileT);
          gl.Normal3fv(@curRow^.node[j].innormal);
          gl.Vertex3fv(@curRow^.node[j].pos);
          if NodesColorMode <> pncmNone then
            gl.Color4fv(@prevRow^.color);
          gl.TexCoord2f((j + 1) / Slices * TextCoordTileS, prevRow^.textcoordT *
            TextCoordTileT);
          gl.Normal3fv(@prevRow^.node[j + 1].innormal);
          gl.Vertex3fv(@prevRow^.node[j + 1].pos);
        end;
        if NodesColorMode <> pncmNone then
          gl.Color4fv(@curRow^.color);
        gl.TexCoord2f(TextCoordTileS, curRow^.textcoordT * TextCoordTileT);
        gl.Normal3fv(@curRow^.node[Slices].innormal);
        gl.Vertex3fv(@curRow^.node[Slices].pos);
      end;
      gl.End_;
    end;
  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
    // creates position spline
    posSpline := Nodes.CreateNewCubicSpline;
    // creates 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
    gl.ColorMaterial(GL_FRONT_AND_BACK, cPNCMtoEnum[NodesColorMode]);
    rci.GLStates.Enable(stColorMaterial);
  end
  else
    rci.GLStates.Disable(stColorMaterial);

  CalculateRow(@rows[0], PAffineVector(@Nodes[0].AsVector)^, normal,
    TGLPipeNode(Nodes[0]).RadiusFactor);
  rows[0].color := TGLPipeNodes(Nodes)[0].Color.Color;
  case TexCoordMode of
    ptcmDefault: rows[0].textcoordT := 0;
    ptcmManual: rows[0].textcoordT := TGLPipeNode(Nodes[0]).TexCoordT;
  end;

  if ppStartDisk in Parts then
  begin
    NegateVector(normal);
    if ppOutside in Parts then
    begin
      RenderDisk(@rows[0], Nodes[0].AsVector, normal, True, TexCoordTileS);
      FTriangleCount := FTriangleCount + Slices * 2; //Slices+1;
    end;
    if ppInside in Parts then
    begin
      RenderDisk(@rows[0], Nodes[0].AsVector, VectorNegate(normal), False,
        TexCoordTileS);
      FTriangleCount := FTriangleCount + Slices * 2; //Slices+1;
    end;
  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;
        //
        case TexCoordMode of
          ptcmDefault:
            begin
              k := Trunc(t);
              if k < Nodes.Count - 1 then
                rows[curRow].textcoordT := Lerp(k,
                  k + 1,
                  Frac(t))
              else
                rows[curRow].textcoordT := k;
            end;
          ptcmManual:
            begin
              k := Trunc(t);
              if k < Nodes.Count - 1 then
                rows[curRow].textcoordT := Lerp(TGLPipeNode(Nodes[k]).TexCoordT,
                  TGLPipeNode(Nodes[k + 1]).TexCoordT,
                  Frac(t))
              else
                rows[curRow].textcoordT := TGLPipeNode(Nodes[k]).TexCoordT;
            end;
        end;
        if (ppOutside in Parts) or (ppInside in Parts) then
          CalculateSides(@rows[curRow xor 1], @rows[curRow], normal);
        if ppOutside in Parts then
          RenderSides(@rows[curRow xor 1], @rows[curRow], TexCoordTileS,
            TexCoordTileT, True);
        if ppInside in Parts then
          RenderSides(@rows[curRow xor 1], @rows[curRow], TexCoordTileS,
            TexCoordTileT, False);
      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);
        //Initialize Texture coordinates
        case TexCoordMode of
          ptcmDefault: rows[curRow].textcoordT := i;
          ptcmManual: rows[curRow].textcoordT :=
            TGLPipeNode(Nodes[i]).TexCoordT;
        end;
        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) or (ppInside in Parts) then
          CalculateSides(@rows[curRow xor 1], @rows[curRow], Nodes.Vector(i));
        if ppOutside in Parts then
          RenderSides(@rows[curRow xor 1], @rows[curRow], TexCoordTileS,
            TexCoordTileT, True);
        if ppInside in Parts then
          RenderSides(@rows[curRow xor 1], @rows[curRow], TexCoordTileS,
            TexCoordTileT, False);
      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;
    if ppOutside in Parts then
    begin
      RenderDisk(@rows[0], Nodes[i].AsVector, normal, False, TexCoordTileS);
      FTriangleCount := FTriangleCount + Slices * 2; //Slices+1;
    end;
    if ppInside in Parts then
    begin
      RenderDisk(@rows[0], Nodes[i].AsVector, VectorNegate(normal), True,
        TexCoordTileS);
      FTriangleCount := FTriangleCount + Slices * 2; //Slices+1;
    end;
  end;
  if SplineMode = lsmCubicSpline then
  begin
    posSpline.Free;
    rSpline.Free;
  end;
end;

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

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;


procedure TGLExtrusionSolid.BuildList(var rci: TGLRenderContextInfo);
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.X := Bottom.Y - Top.Y;
    normal.Y := Top.X - Bottom.X;
    normal.Z := 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;
    dir: TAffineVector;
  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;
    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;

    dir := VectorNormalize(VectorSubtract(bottomBase, topBase));

    topTPBase.S := VectorDotProduct(topBase, dir);
    topTPBase.T := topBase.Z;
    bottomTPBase.S := VectorDotProduct(bottomBase, dir);
    bottomTPBase.T := bottomBase.Z;

    lastNormal := normal;
    topNext := topBase;
    bottomNext := bottomBase;
    topTPNext := topTPBase;
    bottomTPNext := bottomTPBase;
    gl.Begin_(GL_TRIANGLE_STRIP);
    gl.Normal3fv(@normTop);
    xgl.TexCoord2fv(@topTPBase);
    gl.Vertex3fv(@topBase);
    for step := 1 to FStacks do
    begin
      gl.Normal3fv(@normBottom);
      xgl.TexCoord2fv(@bottomTPBase);
      gl.Vertex3fv(@bottomBase);
      topNext.Z := step * DeltaZ;
      bottomNext.Z := topNext.Z;
      topTPNext.T := topNext.Z;
      bottomTPNext.T := bottomNext.Z;
      xgl.TexCoord2fv(@topTPNext);
      gl.Normal3fv(@normTop);
      gl.Vertex3fv(@topNext);
      topBase := topNext;
      topTPBase := topTPNext;
      bottomBase := bottomNext;
      bottomTPBase := bottomTPNext;
    end;
    gl.Normal3fv(@normBottom);
    xgl.TexCoord2fv(@bottomTPBase);
    gl.Vertex3fv(@bottomBase);
    gl.End_;
  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;
    xgl.TexCoord2fv(@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
      gl.PushMatrix;
      gl.Translatef(0, 0, FHeight);
      RenderTesselatedPolygon(true, @normal, invertedNormals);
      gl.PopMatrix;
    end;
    // tessellate start polygon
    if espStartPolygon in FParts then
    begin
      NegateVector(normal);
      RenderTesselatedPolygon(true, @normal, not invertedNormals);
    end;
  end;
end;

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

 
destructor TGLExtrusionSolid.Destroy;
begin
  inherited;
end;


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


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


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


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


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


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


function TGLExtrusionSolid.AxisAlignedDimensionsUnscaled: TVector;
var
  dMin, dMax: TAffineVector;
begin
  if FAxisAlignedDimensionsCache.X < 0 then
  begin
    Contours.GetExtents(dMin, dMax);
    FAxisAlignedDimensionsCache.X := MaxFloat(Abs(dMin.X), Abs(dMax.X));
    FAxisAlignedDimensionsCache.Y := MaxFloat(Abs(dMin.Y), Abs(dMax.Y));
    FAxisAlignedDimensionsCache.Z := MaxFloat(Abs(dMin.Z), Abs(dMax.Z +
      Height));
  end;
  SetVector(Result, FAxisAlignedDimensionsCache);
end;


procedure TGLExtrusionSolid.StructureChanged;
begin
  FAxisAlignedDimensionsCache.X := -1;
  inherited;
end;

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

  RegisterClasses([TGLRevolutionSolid, TGLExtrusionSolid, TGLPipe]);

end.

