{**********************************************}
{   TeeChart - 3D text tool                    }
{   Copyright (c) 2006-2020 by Steema Software }
{**********************************************}
unit TeeText3D;
{$I TeeDefs.inc}

interface

uses
  {$IFNDEF LINUX}
  Windows, Messages,
  {$ENDIF}
  SysUtils, Classes,

  {$IFDEF D16}
  System.UITypes,
  {$ENDIF}

  Graphics, Controls, Forms, Dialogs, ComCtrls, StdCtrls, ExtCtrls,

  TeCanvas, TeeProcs, TeEngine, Chart, TeeEdiFont, TeeBrushDlg;

type
  TText3DTool=class(TTeeCustomTool)
  private
    FAngle     : Double;
    FFont      : TTeeFont;
    FPosition  : TFloatXYZ;
    FRotation  : TFloatXYZ;
    FText      : String;

    IDirty : Boolean;

    procedure Changed(Sender:TObject);
    procedure DestroyPolygons;
    procedure FontChanged(Sender:TObject);
    procedure SetAngle(const Value:Double);
    procedure SetFont(const Value:TTeeFont);
    procedure SetPosition(const Value:TFloatXYZ);
    procedure SetRotation(const Value:TFloatXYZ);
    procedure SetText(const Value:String);
  protected
    Procedure ChartEvent(AEvent:TChartToolEvent); override;
    class Function GetEditorClass:String; override;
  public
    Clipped      : Array of Integer;
    Interior     : Array of Boolean;
    PolygonCount : Integer;
    Polygons     : Array of TPointArray;

    Constructor Create(AOwner:TComponent); override;
    Destructor Destroy; override;

    procedure Assign(Source:TPersistent); override;
    class Function Description:String; override;
    class Function LongDescription:String; override;
  published
    property Active;
    property Angle:Double read FAngle write SetAngle;
    property Brush;
    property Font:TTeeFont read FFont write SetFont;
    property Pen;
    property Position:TFloatXYZ read FPosition write SetPosition;
    property Rotation:TFloatXYZ read FRotation write SetRotation;
    property Text:String read FText write SetText;
  end;

  TText3DEditor = class(TForm)
    PageControl1: TPageControl;
    TabSheet1: TTabSheet;
    TabFont: TTabSheet;
    MemoText: TMemo;
    TabSheet3: TTabSheet;
    Label4: TLabel;
    Label5: TLabel;
    ECustLeft: TEdit;
    UDLeft: TUpDown;
    ECustTop: TEdit;
    UDTop: TUpDown;
    Label2: TLabel;
    Edit1: TEdit;
    UDZ: TUpDown;
    TabSheet2: TTabSheet;
    Label3: TLabel;
    TBRotation: TTrackBar;
    LRotation: TLabel;
    Label6: TLabel;
    TBElevation: TTrackBar;
    LElevation: TLabel;
    Label7: TLabel;
    TBTilt: TTrackBar;
    LTilt: TLabel;
    TabPattern: TTabSheet;
    Panel1: TPanel;
    Button2: TButton;
    Panel2: TPanel;
    Label1: TLabel;
    TabBorder: TTabSheet;
    procedure ECustLeftChange(Sender: TObject);
    procedure ECustTopChange(Sender: TObject);
    procedure MemoTextChange(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure Edit1Change(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure TBRotationChange(Sender: TObject);
    procedure TBElevationChange(Sender: TObject);
    procedure TBTiltChange(Sender: TObject);
    procedure PageControl1Change(Sender: TObject);
  private
    { Private declarations }
    Tool : TText3DTool;
  public
    { Public declarations }
  end;

implementation

{$IFNDEF LCL}
{$R *.DFM}
{$ELSE}
{$R *.lfm}
{$ENDIF}

uses
  TeeStringsEditor, TeeProCo, TeePenDlg;

Constructor TText3DTool.Create(AOwner:TComponent);
begin
  inherited;

  FFont:=TTeeFont.Create(FontChanged);
  FFont.Size:=24;

  FFont.OutLine.Color:=clWhite;
  FFont.OutLine.Show;

  Brush.Color:=clWhite;
  Brush.BackColor:=Font.Color;

  FPosition:=TFloatXYZ.Create;
  FPosition.X:=50;
  FPosition.Y:=50;
  FPosition.OnChange:=Changed;

  FRotation:=TFloatXYZ.Create;
  FRotation.OnChange:=Changed;

  IDirty:=True;
end;

Destructor TText3DTool.Destroy;
begin
  DestroyPolygons;
  FFont.Free;
  FRotation.Free;
  FPosition.Free;
  inherited;
end;

procedure TText3DTool.FontChanged(Sender:TObject);
begin
  IDirty:=True;
  Repaint;
end;

procedure TText3DTool.Changed(Sender:TObject);
begin
  Repaint;
end;

procedure TText3DTool.DestroyPolygons;
var t : Integer;
begin
  Interior:=nil;
  Clipped:=nil;

  for t:=0 to PolygonCount-1 do
      Polygons[t]:=nil;

  Polygons:=nil;
  PolygonCount:=0;
end;

type
  TTeeCanvas3DAccess=class(TTeeCanvas3D);
  TTeePanelAccess=class(TCustomTeePanel);

Procedure TText3DTool.ChartEvent(AEvent:TChartToolEvent);

  procedure DrawPolygons;

    procedure DoDrawPolygons;
    var t    : Integer;
        PP   : TPointArray;
        tmpZ : Integer;
        Old  : TColor;
    begin
      PP:=nil;

      tmpZ:=Round(FPosition.Z);

      for t:=0 to PolygonCount-1 do
      begin
        if Interior[t] then
        begin
          if ParentChart.Canvas.Pen.Visible then
          begin
            Old:=ParentChart.Canvas.Brush.Color;
            ParentChart.Canvas.Brush.Style:=bsClear;

            ParentChart.Canvas.PolygonWithZ(Polygons[t],tmpZ);

            ParentChart.Canvas.Brush.Style:=bsSolid;
            ParentChart.Canvas.Brush.Color:=Old;
          end;
        end
        else
        begin
          if Clipped[t]<>-1 then
          begin
            PP:=ParentChart.Canvas.Calc3DPoints(Polygons[Clipped[t]],tmpZ);
            try
              ParentChart.Canvas.ClipPolygon(PP,Length(PP),True);
            finally
              PP:=nil;
            end;
          end;

          ParentChart.Canvas.PolygonWithZ(Polygons[t],tmpZ);

          if Clipped[t]<>-1 then
             ParentChart.Canvas.UnClipRectangle;
        end;
      end;
    end;

  var AShadow  : TTeeShadow;

      OldR,
      OldE : Double;
      OldT : Integer;
      tmpRotate : Boolean;
  begin
    // Apply rotation
    tmpRotate:= (Rotation.X<>0) or (Rotation.Y<>0) or (Rotation.Z<>0);

    if tmpRotate then
    begin
      with ParentChart.Canvas.View3DOptions do
      begin
        oldR:=RotationFloat;
        oldE:=ElevationFloat;
        oldT:=Tilt;

        RotationFloat:=oldR+Self.Rotation.X;
        ElevationFloat:=oldE+Self.Rotation.Y;
        Tilt:=oldT+Round(Self.Rotation.Z);
      end;

      if ParentChart.Canvas is TTeeCanvas3D then
      begin
        TTeeCanvas3DAccess(ParentChart.Canvas).CalcTrigValues;
        TTeeCanvas3DAccess(ParentChart.Canvas).CalcPerspective(ParentChart.ChartRect);
      end;

      // Restore previous rotation
      with ParentChart.Canvas.View3DOptions do
      begin
        RotationFloat:=oldR;
        ElevationFloat:=oldE;
        Tilt:=oldT;
      end;
    end;

    // Draw Font Shadow:

    AShadow:=FFont.Shadow;

    if AShadow.ShouldDraw then
    begin
      if ParentChart.Canvas is TTeeCanvas3D then
      begin
        Inc(TTeeCanvas3DAccess(ParentChart.Canvas).XCenterOffset,AShadow.HorizSize);
        Inc(TTeeCanvas3DAccess(ParentChart.Canvas).YCenterOffset,AShadow.VertSize);
      end;

      ParentChart.Canvas.Brush.Style:=bsSolid;
      ParentChart.Canvas.Brush.Gradient.Visible:=False;
      ParentChart.Canvas.Brush.Color:=AShadow.Color;

      ParentChart.Canvas.Pen.Style:=psClear;

      DoDrawPolygons;

      if ParentChart.Canvas is TTeeCanvas3D then
      begin
        Dec(TTeeCanvas3DAccess(ParentChart.Canvas).XCenterOffset,AShadow.HorizSize);
        Dec(TTeeCanvas3DAccess(ParentChart.Canvas).YCenterOffset,AShadow.VertSize);
      end;
    end;

    // Draw Text 3D:
    ParentChart.Canvas.AssignBrush(Font.Brush,Font.Color);
    ParentChart.Canvas.AssignVisiblePen(FFont.OutLine);
    DoDrawPolygons;

    if tmpRotate then
    if ParentChart.Canvas is TTeeCanvas3D then
    begin
      TTeeCanvas3DAccess(ParentChart.Canvas).CalcTrigValues;
      TTeeCanvas3DAccess(ParentChart.Canvas).CalcPerspective(ParentChart.ChartRect);
    end;
  end;

  Procedure CreatePolygons;

    procedure AddPoint(const P:TPoint);
    var tmpCount : Integer;
    begin
      tmpCount:=Length(Polygons[PolygonCount-1]);
      SetLength(Polygons[PolygonCount-1],tmpCount+1);
      Polygons[PolygonCount-1,tmpCount]:=P;
    end;

  type
    TPArray=packed Array[0..0] of TPoint;
    PTP=^TPArray;
    TBArray=packed Array[0..0] of Byte;
    PTB=^TBArray;

  var DC : HDC;
      LogRec : TLogFont;
      NewFont,
      OldFont : HFont;

      {$IFNDEF D23}
      NullP : Pointer;
      NullT : Pointer;
      {$ENDIF}

      P     : PTP;
      Types : PTB;

      tmp   : Integer;
      tmpC  : Integer;
      t     : Integer;
      tmpHeight : Integer;
      tmpNumLines : Integer;
      b     : Byte;
      PL,P0 : TPoint;
      St    : String;
  begin
    ParentChart.Canvas.ReferenceCanvas.Font:=FFont;

    DC:=ParentChart.Canvas.ReferenceCanvas.Handle;

    SetBkMode(DC,Ord(cbmTransparent));
    
    if BeginPath(DC) then
    begin
      if FAngle<>0 then
      begin
        GetObject(ParentChart.Canvas.Font.Handle,SizeOf(LogRec), @LogRec);

        LogRec.lfOrientation:=Round(FAngle*10.0);
        LogRec.lfQuality:=TeeFontAntiAlias;

        NewFont:=CreateFontIndirect({$IFDEF LCL}@{$ENDIF}LogRec);
        OldFont:=SelectObject(DC,NewFont);

        SetGraphicsMode(DC,GM_ADVANCED);
      end
      else OldFont:=0;

      tmpHeight:=ParentChart.Canvas.FontHeight;
      tmpNumLines:=TTeePanelAccess(ParentChart).LinesOfText(FText);

      for t:=1 to tmpNumLines do
      begin
        St:=TeeExtractField(FText,t,TeeLineSeparator);
        Windows.TextOut(DC,Round(FPosition.X),Round(FPosition.Y)+((t-1)*tmpHeight),
                        PChar(St),Length(St));
      end;

      if FAngle<>0 then
      begin
        DeleteObject(SelectObject(DC,OldFont));
        SetGraphicsMode(DC,{$IFDEF LCL}GM_COMPATIBLE{$ELSE}GM_LAST{$ENDIF});
      end;

      if not EndPath(DC) then
         Abort;

      if not FlattenPath(DC) then
         Abort;

      // optional:  WidenPath(DC);

      {$IFDEF D23}
      tmp:=GetPath(DC,nil,nil,0);
      {$ELSE}
      NullP:=nil;
      NullT:=nil;
      tmp:=GetPath(DC,NullP^,NullT^,0);
      {$ENDIF}

      if tmp>0 then
      begin
        GetMem(P,SizeOf(TPoint)*tmp);
        GetMem(Types,SizeOf(Byte)*tmp);

        {$IFDEF D24}
        GetPath(DC,PPoint(P),PByte(Types),tmp);
        {$ELSE}
        {$IFDEF D23}
        GetPath(DC,PPointL(P),PByte(Types),tmp);
        {$ELSE}
        GetPath(DC,P^,Types^,tmp);
        {$ENDIF}
        {$ENDIF}

        if tmp=-1 then
        begin
          tmp:=GetLastError;
          Raise Exception.Create('GetPath error: '+IntToStr(tmp));
        end;

        if tmp>0 then
        begin
          PolygonCount:=0;

          for t:=0 to tmp-1 do
          begin
            {$IFOPT R+}
            {$DEFINE WASRANGE}
            {$ENDIF}

            {$R-}

            b:=Types[t];

            case b of
             PT_MOVETO : begin
                           Inc(PolygonCount);
                           SetLength(Polygons,PolygonCount);
                           AddPoint(P[t]);
                         end;
             PT_LINETO : AddPoint(P[t]);
           PT_BEZIERTO : AddPoint(P[t]);

        PT_CLOSEFIGURE : ;
        PT_CLOSEFIGURE+
             PT_LINETO : AddPoint(P[t]);
        PT_CLOSEFIGURE+
        PT_BEZIERTO    : AddPoint(P[t]);

               else
                 Abort;  // error !
            end;
          end;

          {$IFDEF WASRANGE}
          {$R+}
          {$ENDIF}

          // Remove last repeated XY points:

          for t:=0 to PolygonCount-1 do
          begin
            tmpC:=Length(Polygons[t])-1;

            if tmpC>0 then
            begin
              P0:=Polygons[t,0];

              while tmpC>1 do
              begin
                PL:=Polygons[t,tmpC];

                if (PL.X=P0.X) and (PL.Y=P0.Y) then
                begin
                  SetLength(Polygons[t],tmpC);
                  Dec(tmpC);
                end
                else Break;
              end;
            end;
          end;
        end;

        FreeMem(Types);
        FreeMem(P);
      end;
    end;
  end;

  // Guess polygons inside other polygons:
  procedure GuessInteriors;
  var t, tt : Integer;
  begin
    SetLength(Interior,PolygonCount);
    SetLength(Clipped,PolygonCount);

    for t:=0 to PolygonCount-1 do
    begin
      Interior[t]:=False;

      for tt:=0 to PolygonCount-1 do
      if tt<>t then
         if PolygonInPolygon(Polygons[t],Polygons[tt]) then
         begin
           Interior[t]:=True;
           break;
         end;

      Clipped[t]:=-1;

      for tt:=0 to PolygonCount-1 do
      if tt<>t then
         if PolygonInPolygon(Polygons[tt],Polygons[t]) then
         begin
           Clipped[t]:=tt;
           break;
         end;
    end;
  end;

begin
  inherited;

  if AEvent=cteAfterDraw then
  begin
    ParentChart.Canvas.BackMode:=cbmTransparent;
    ParentChart.Canvas.AssignVisiblePen(FFont.OutLine);
    TTeePanelAccess(ParentChart).TryAssignFont(FFont);
    Brush.BackColor:=Font.Color;

    if IDirty then
    begin
      DestroyPolygons;
      CreatePolygons;
      GuessInteriors;
      IDirty:=False;
    end;

    DrawPolygons;
  end;
end;

class function TText3DTool.Description: String;
begin
  result:=TeeMsg_Text3D;
end;

class function TText3DTool.LongDescription: String;
begin
  result:=TeeMsg_Text3DDesc;
end;

procedure TText3DTool.SetAngle(const Value: Double);
begin
  SetDoubleProperty(FAngle,Value);
end;

procedure TText3DTool.SetFont(const Value: TTeeFont);
begin
  FFont.Assign(Value);
end;

procedure TText3DTool.SetText(const Value: String);
begin
  if FText<>Value then
  begin
    FText:=Value;
    IDirty:=True;
    Repaint;
  end;
end;

procedure TText3DTool.SetPosition(const Value: TFloatXYZ);
begin
  FPosition.Assign(Value);
end;

procedure TText3DTool.SetRotation(const Value: TFloatXYZ);
begin
  FRotation.Assign(Value);
end;

procedure TText3DEditor.ECustLeftChange(Sender: TObject);
begin
  if Showing then
     Tool.Position.X:=Round(UDLeft.Position);
end;

procedure TText3DEditor.ECustTopChange(Sender: TObject);
begin
  if Showing then
     Tool.Position.Y:=Round(UDTop.Position);
end;

procedure TText3DEditor.MemoTextChange(Sender: TObject);
begin
  {$IFDEF POSIX}
  Tool.Text:=MemoText.Lines.Text;
  {$ELSE}
  Tool.Text:=ReplaceChar(MemoText.Lines.Text,#10);
  {$ENDIF}
end;

procedure TText3DEditor.FormShow(Sender: TObject);
begin
  Tool:=TText3DTool(Tag);

  if Assigned(Tool) then
  with Tool do
  begin
    UDLeft.Position:=Round(Position.X);
    UDTop.Position:=Round(Position.Y);
    UDZ.Position:=Round(Position.Z);
    MemoText.Text:=Text;

    TBRotation.Position:=Round(Rotation.X);
    TBElevation.Position:=Round(Rotation.Y);
    TBTilt.Position:=Round(Rotation.Z);
  end;
end;

procedure TText3DTool.Assign(Source: TPersistent);
begin
  if Source is TText3DTool then
  with TText3DTool(Source) do
  begin
    Self.FAngle:=FAngle;
    Self.FFont.Assign(FFont);
    Self.FPosition.Assign(FPosition);
    Self.FRotation.Assign(FRotation);
    Self.FText:=FText;
  end;

  inherited;
end;

class function TText3DTool.GetEditorClass: String;
begin
  result:='TText3DEditor'; // Do not localize
end;

procedure TText3DEditor.Edit1Change(Sender: TObject);
begin
  if Showing then
     Tool.Position.Z:=Round(UDZ.Position);
end;

procedure TText3DEditor.Button2Click(Sender: TObject);
begin
  TeeEditStrings(Self,MemoText.Lines);
end;

procedure TText3DEditor.TBRotationChange(Sender: TObject);
begin
  Tool.Rotation.X:=TBRotation.Position;
  LRotation.Caption:=IntToStr(TBRotation.Position);
end;

procedure TText3DEditor.TBElevationChange(Sender: TObject);
begin
  Tool.Rotation.Y:=TBElevation.Position;
  LElevation.Caption:=IntToStr(TBElevation.Position);
end;

procedure TText3DEditor.TBTiltChange(Sender: TObject);
begin
  Tool.Rotation.Z:=TBTilt.Position;
  LTilt.Caption:=IntToStr(TBTilt.Position);
end;

procedure TText3DEditor.PageControl1Change(Sender: TObject);
begin
  if (PageControl1.ActivePage=TabFont) and (TabFont.ControlCount=0) then
     InsertTeeFontEditor(TabFont).RefreshControls(Tool.Font)
  else
  if (PageControl1.ActivePage=TabPattern) and (TabPattern.ControlCount=0) then
     TBrushDialog.AddDialog(Tool.Font.Brush,TabPattern)
  else
  if (PageControl1.ActivePage=TabBorder) and (TabBorder.ControlCount=0) then
     TPenDialog.InsertForm(Tool.Font.OutLine,TabBorder);
end;

initialization
  RegisterClass(TText3DEditor);
  RegisterTeeTools([TText3DTool]);
finalization
  UnRegisterTeeTools([TText3DTool]);
end.

