unit ClientVisuals;

interface

uses
  Windows, Messages, SysUtils, Types, UITypes, Classes, ASVisualization,
  MainForm, ASKernelDefs, Generics.Defaults, Generics.Collections, Controls,
  Forms, VisForm, ASObjects, Graphics, Menus, ClientDefs, DoublePoint, VisCtl,
  rgl, VisCtl2D;

type
  TVisFrm = class(TForm)
  strict private
    FAspect: Double;
  strict protected
    const
      CMD_SETSIZE = 101;
      CMD_DISCARD = 102;
  protected
    procedure CMTextChanged(var Message: TMessage); message CM_TEXTCHANGED;
    procedure WMSysCommand(var Msg: TWMSysCommand); message WM_SYSCOMMAND;
    function CanResize(var NewWidth: Integer; var NewHeight: Integer): Boolean;
      override;
    procedure KeyPress(var Key: Char); override;
    procedure DoClose(var Action: TCloseAction); override;
  public
    constructor CreateNew(AOwner: TComponent; Dummy: Integer = 0); override;
  end;

  TManagedVisCtl2D = class(TVisCtl2D)
  strict private
    FDiagramName: string;
    FmnuDetach: TMenuItem;
    FmnuDock: TMenuItem;
    class var FInstances: TDictionary<string, TManagedVisCtl2D>;
    class constructor ClassCreate;
    class destructor ClassDestroy;
    procedure Detach(Sender: TObject);
    procedure Dock(Sender: TObject); reintroduce;
    procedure ContextPopup(Sender: TObject);
  protected
    procedure SetWindowWidth(AWidth: Integer); override;
    procedure SetWindowHeight(AHeight: Integer); override;
    procedure SetDetached(ADetached: Boolean); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property DiagramName: string read FDiagramName;
    class property Instances: TDictionary<string, TManagedVisCtl2D> read FInstances;
  end;

  TTemporaryVisCtl2D = class(TVisCtl2D);

  TManagedVisCtl3D = class(TVisCtl3D)
  strict private
    FSceneName: string;
    FmnuDetach: TMenuItem;
    FmnuDock: TMenuItem;
    class var FInstances: TDictionary<string, TManagedVisCtl3D>;
    class constructor ClassCreate;
    class destructor ClassDestroy;
    procedure Detach(Sender: TObject);
    procedure Dock(Sender: TObject); reintroduce;
    procedure ContextPopup(Sender: TObject);
  protected
    procedure SetWindowWidth(AWidth: Integer); override;
    procedure SetWindowHeight(AHeight: Integer); override;
    procedure SetDetached(ADetached: Boolean); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property SceneName: string read FSceneName;
    class property Instances: TDictionary<string, TManagedVisCtl3D> read FInstances;
  end;

  TTemporaryVisCtl3D = class(TVisCtl3D);

  TVisualRec = class
    ID: TGUID;
    Name: string;
    ClassType: TVisObjClass;
    Title,
    Description: string;
  end;

  TVisualization = record
  strict private
    class var FDiagram: string;
    class var FScene: string;
    class var FPopups: Boolean;
    class var FDiagramNumber: UInt64;
    class var FSceneNumber: UInt64;
    class function BarChart(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; static;
    class function PieChart(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; static;
    class function Histogram(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; static;
    class function ScatterPlot(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; overload; static;
    class function ScatterPlot(ACtl: TVisCtl3D; AVisual: TVisual): TDrawable3D; overload; static;
    class function Surface(ACtl: TVisCtl3D; AVisual: TVisual): TDrawable3D; static;
    class function SpaceCurve(ACtl: TVisCtl3D; AVisual: TVisual): TDrawable3D; static;
    class function Object3D(ACtl: TVisCtl3D; AVisual: TVisual): TDrawable3D; static;
    class function RegionPlot(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; static;
    class function Heatmap(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; static;
    class function VectorField(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; overload; static;
    class function VectorField(ACtl: TVisCtl3D; AVisual: TVisual): TDrawable3D; overload; static;
    class function Line(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; static;
    class function Rectangle(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; static;
    class function Circle(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; static;
    class function Polygon(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; static;
    class function Text(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; overload; static;
    class function Text(ACtl: TVisCtl3D; AVisual: TVisual): TDrawable3D; overload; static;
    class function Pixmap(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; overload; static;
    class function Pixmap(ACtl: TVisCtl3D; AVisual: TVisual): TDrawable3D; overload; static;
    class function Arrow(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable; overload; static;
    class function Arrow(ACtl: TVisCtl3D; AVisual: TVisual): TDrawable3D; overload; static;
    class function GetDrawableByGUID(const AGUID: TGUID): TVisObj; static;
  public
    class constructor ClassCreate;
    class procedure Visualize(AVisual: TVisual; ARef: PAlgosimReference = nil); static;
    class procedure RemoveVisual(const AGUID: TGUID); static;
    class procedure ConfigVisual(const AGUID: TGUID; ASettings: TAlgosimStructure); static;
    class procedure ExportVisual(const AGUID: TGUID; const AFileName: TFileName;
      ASettings: TAlgosimStructure); static;
    class function QueryVisual(var AVisualRec: TVisualRec): Boolean; static;
    class procedure EnumVisuals(AList: TList<TGUID>); static;
    class property Diagram: string read FDiagram write FDiagram;
    class property Scene: string read FScene write FScene;
    class property Popups: Boolean read FPopups write FPopups;
    class function GetDiagram(out ANewDiagram: Boolean): TVisCtl2D; static;
    class function GetScene(out ANewScene: Boolean): TVisCtl3D; static;
    class function ShowDiagram(ADiagram: TDiagram): Boolean; overload; static;
    class function ShowDiagram(ADiagram: TVisCtl2D): Boolean; overload; static;
    class function ShowDiagram(const ADiagram: string): Boolean; overload; static;
    class function ShowDiagram(const ADiagram: TGUID): Boolean; overload; static;
    class function ShowScene(AScene: TScene): Boolean; overload; static;
    class function ShowScene(AScene: TVisCtl3D): Boolean; overload; static;
    class function ShowScene(const AScene: string): Boolean; overload; static;
    class function ShowScene(const AScene: TGUID): Boolean; overload; static;
    class function ShowVisCtl(const AID: TGUID): Boolean; static;
  end;

implementation

uses
  ASNum, Gallery, Math, FormFader, WinMgrForm, ImageSizeForm;

{ TVisualization }

class function TVisualization.Arrow(ACtl: TVisCtl3D;
  AVisual: TVisual): TDrawable3D;
var
  Arrow: TArrow absolute Result;
begin
  Arrow := TArrow.Create(ACtl);
  var Data := AVisual.Data as TArrowDataR3;
  Arrow.Position := Data.a;
  Arrow.Vector := Data.v;
  Arrow.Color := clRed;
end;

class function TVisualization.Arrow(ACtl: TVisCtl2D;
  AVisual: TVisual): TDrawable;
var
  Line: TLine absolute Result;
begin
  Line := TLine.Create(ACtl, ACtl.View);
  var Data := AVisual.Data as TArrowDataR2;
  Line.Start := Data.a;
  Line.&End := Data.a + Data.v;
  Line.EndMarker.Kind := lemSemiArrow;
end;

class function TVisualization.BarChart(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable;
var
  BarChart: TBarChart absolute Result;
  CatData: TCategoryData;
begin
  BarChart := TBarChart.Create(ACtl, ACtl.View);
  for CatData in AVisual.Data as TCategoryDataList do
    BarChart.AddBar(CatData.Name, CatData.Value);
end;

{$IF SizeOf(ASR) = SizeOf(Double)}
type
  ASRArrayToDoubleArray = TArray<Double>;
  ASR2ArrayToDoublePointArray = TArray<TPointD>;
{$ELSE}
function ASRArrayToDoubleArray(const A: TArray<TASR>): TArray<Double>;
begin
  SetLength(Result, Length(A));
  for var i := 0 to High(A) do
    Result[i] := A[i];
end;

function ASR2ArrayToDoublePointArray(const A: TArray<TASR2>): TArray<TPointD>;
begin
  SetLength(Result, Length(A));
  for var i := 0 to High(A) do
  begin
    Result[i].X := A[i].X;
    Result[i].Y := A[i].Y;
  end;
end;
{$ENDIF}

class function TVisualization.Heatmap(ACtl: TVisCtl2D;
  AVisual: TVisual): TDrawable;
var
  Heatmap: THeatmap absolute Result;
begin
  Heatmap := THeatmap.Create(ACtl, ACtl.View);
  with AVisual.Data as THeatmapData do
  begin
    var bm := Pixmap.CreateGDIBitmap;
    try
      Heatmap.Bitmap := bm;
    finally
      bm.Free;
    end;
    Heatmap.Rect := TRectD.Create(x0, y1, x1, y0);
  end;
end;

class function TVisualization.Histogram(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable;
var
  Histogram: THistogram absolute Result;
begin
  Histogram := THistogram.Create(ACtl, ACtl.View);
  Histogram.Data := ASRArrayToDoubleArray((AVisual.Data as THistogramData).Numbers);
end;

class function TVisualization.Line(ACtl: TVisCtl2D;
  AVisual: TVisual): TDrawable;
var
  Line: TLine absolute Result;
begin
  Line := TLine.Create(ACtl, ACtl.View);
  Line.Start := (AVisual.Data as TLineDataR2).a;
  Line.&End := (AVisual.Data as TLineDataR2).b;
end;

class function TVisualization.Circle(ACtl: TVisCtl2D;
  AVisual: TVisual): TDrawable;
var
  Circle: TCircle absolute Result;
begin
  Circle := TCircle.Create(ACtl, ACtl.View);
  Circle.Center := (AVisual.Data as TCircleData).a;
  Circle.Radius := (AVisual.Data as TCircleData).r;
end;

class constructor TVisualization.ClassCreate;
begin
  FPopups := True;
end;

class procedure TVisualization.ConfigVisual(const AGUID: TGUID;
  ASettings: TAlgosimStructure);
var
  Drawable: TDrawable;
  Drawable3D: TDrawable3D;
begin
  if TDrawable.TryGetDrawableByGUID(AGUID, Drawable) then
  begin
    Drawable.Control.BeginBackgroundPaint;
    try
      if Assigned(ASettings) then
        Drawable.Configure(ASettings)
      else
        Drawable.ShowOptionsForm(AlgosimMainForm);
    finally
      Drawable.Control.EndBackgroundPaint;
    end;
  end
  else if TDrawable3D.TryGetDrawableByGUID(AGUID, Drawable3D) then
  begin
    Drawable3D.Control.BeginBackgroundPaint;
    try
      if Assigned(ASettings) then
        Drawable3D.Configure(ASettings)
      else
        Drawable3D.ShowOptionsForm(AlgosimMainForm);
    finally
      Drawable3D.Control.EndBackgroundPaint;
    end;
  end;
end;

class procedure TVisualization.EnumVisuals(AList: TList<TGUID>);
begin

  for var D in TDrawable.Instances do
    AList.Add(D.Key);

  for var D in TDrawable3D.Instances do
    AList.Add(D.Key);

end;

class procedure TVisualization.ExportVisual(const AGUID: TGUID;
  const AFileName: TFileName; ASettings: TAlgosimStructure);
var
  Drawable: TDrawable;
  Drawable3D: TDrawable3D;
begin
  if TDrawable.TryGetDrawableByGUID(AGUID, Drawable) then
  begin
    if string(AFileName).IsEmpty then
      Drawable.Control.SaveAsSVG
    else
    begin
      var LOptions := DefaultSVGExportOptions;
      var LWidth, LHeight: string;
      if ASettings.HasMember('width') then
        LWidth := ASettings['width'].ToString;
      if ASettings.HasMember('height') then
        LHeight := ASettings['height'].ToString;
      LOptions.SetDimensionsFromText(LWidth, LHeight);
      if ASettings.HasMember('stretch') then
        LOptions.Stretch := ASettings['stretch'].ToBoolean;
      if ASettings.HasMember('title') then
        LOptions.Title := ASettings['title'].ToString;
      if ASettings.HasMember('description') then
        LOptions.Description := ASettings['description'].ToString;
      if ASettings.HasMember('language') then
        LOptions.Language := ASettings['language'].ToString;
      Drawable.Control.SaveAsSVG(AFileName, LOptions);
    end;
  end
  else if TDrawable3D.TryGetDrawableByGUID(AGUID, Drawable3D) then
  begin
    var LWidth := Drawable3D.Control.ClientWidth;
    var LHeight := Drawable3D.Control.ClientHeight;
    if ASettings.HasMember('width') then
      LWidth := ASettings['width'].ToInteger;
    if ASettings.HasMember('height') then
      LHeight := ASettings['height'].ToInteger;
    Drawable3D.Control.SaveToBitmap(AFileName, LWidth, LHeight);
  end;
end;

class function TVisualization.GetDiagram(out ANewDiagram: Boolean): TVisCtl2D;
var
  ManagedDiagram: TManagedVisCtl2D;
begin

  if FDiagram.IsEmpty then
  begin
    if FPopups then
    begin
      Inc(FDiagramNumber);
      var frm := TVisFrm.CreateNew(AlgosimMainForm);
      frm.Caption := 'Unnamed diagram' + #32 + FDiagramNumber.ToString;
      var ctl := TVisCtl2D.Create(frm);
      ctl.Parent := frm;
      ctl.Align := alClient;
      frm.Show;
      ANewDiagram := True;
      Result := ctl;
    end
    else
    begin
      if TDrawable.ModalLevel > 0 then
        raise Exception.Create('Cannot remove a drawable object when a settings dialog is open.');
      TGallery.Vacuum;
      Result := TTemporaryVisCtl2D.Create(AlgosimMainForm);
      Result.Parent := AlgosimMainForm.GalleryPanel;
      Result.Align := alClient;
      ANewDiagram := True;
    end;
  end
  else if TManagedVisCtl2D.Instances.TryGetValue(FDiagram, ManagedDiagram) then
  begin
    Result := ManagedDiagram;
    ANewDiagram := False;
  end
  else
  begin
    TGallery.Vacuum;
    Result := TManagedVisCtl2D.Create(nil);
    Result.Parent := AlgosimMainForm.GalleryPanel;
    Result.Align := alClient;
    ANewDiagram := True;
  end;

end;

class function TVisualization.GetDrawableByGUID(const AGUID: TGUID): TVisObj;
var
  Drawable: TDrawable;
  Drawable3D: TDrawable3D;
begin
  if TDrawable.TryGetDrawableByGUID(AGUID, Drawable) then
    Result := Drawable
  else if TDrawable3D.TryGetDrawableByGUID(AGUID, Drawable3D) then
    Result := Drawable3D
  else
    Result := nil;
end;

class function TVisualization.GetScene(out ANewScene: Boolean): TVisCtl3D;
var
  ManagedScene: TManagedVisCtl3D;
begin

  if FScene.IsEmpty then
  begin
    if FPopups then
    begin
      Inc(FSceneNumber);
      var frm := TVisFrm.CreateNew(AlgosimMainForm);
      frm.Caption := 'Unnamed scene' + #32 + FSceneNumber.ToString;
      var ctl := TVisCtl3D.Create(frm);
      ctl.Parent := frm;
      ctl.Align := alClient;
      frm.Show;
      ANewScene := True;
      Result := ctl;
    end
    else
    begin
      if TDrawable3D.ModalLevel > 0 then
        raise Exception.Create('Cannot remove a drawable object when a settings dialog is open.');
      TGallery.Vacuum;
      Result := TTemporaryVisCtl3D.Create(AlgosimMainForm);
      Result.Parent := AlgosimMainForm.GalleryPanel;
      Result.Align := alClient;
      ANewScene := True;
    end;
  end
  else if TManagedVisCtl3D.Instances.TryGetValue(FScene, ManagedScene) then
  begin
    Result := ManagedScene;
    ANewScene := False;
  end
  else
  begin
    TGallery.Vacuum;
    Result := TManagedVisCtl3D.Create(nil);
    Result.Parent := AlgosimMainForm.GalleryPanel;
    Result.Align := alClient;
    ANewScene := True;
  end;

end;

class function TVisualization.PieChart(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable;
var
  PieChart: TPieChart absolute Result;
  CatData: TCategoryData;
begin
  PieChart := TPieChart.Create(ACtl, ACtl.View);
  for CatData in AVisual.Data as TCategoryDataList do
    PieChart.AddSlice(CatData.Name, CatData.Value);
end;

class function TVisualization.Pixmap(ACtl: TVisCtl3D;
  AVisual: TVisual): TDrawable3D;
var
  Pixmap: TImageRect absolute Result;
begin
  Pixmap := TImageRect.Create(ACtl);
  var bm := (AVisual.Data as TPixmapData).Pixmap.CreateGDIBitmap;
  try
    Pixmap.Bitmap := bm
  finally
    bm.Free;
  end;
end;

class function TVisualization.Pixmap(ACtl: TVisCtl2D;
  AVisual: TVisual): TDrawable;
var
  Pixmap: TPixmap absolute Result;
begin
  Pixmap := TPixmap.Create(ACtl, ACtl.View);
  Pixmap.Rect := (AVisual.Data as TPixmapData).Rect;
  var bm := (AVisual.Data as TPixmapData).Pixmap.CreateGDIBitmap;
  try
    Pixmap.Bitmap := bm
  finally
    bm.Free;
  end;
end;

class function TVisualization.Polygon(ACtl: TVisCtl2D;
  AVisual: TVisual): TDrawable;
var
  Polygon: TPolygon absolute Result;
begin
  Polygon := TPolygon.Create(ACtl, ACtl.View);
  Polygon.Points := ASR2ArrayToDoublePointArray((AVisual.Data as TPolygonData).Points)
end;

class function TVisualization.QueryVisual(var AVisualRec: TVisualRec): Boolean;
begin

  var D := GetDrawableByGUID(AVisualRec.ID);

  Result := Assigned(D) and (D.GUID = AVisualRec.ID);

  if Result then
  begin
    AVisualRec.Name := D.Name;
    if D.ClassType.InheritsFrom(TVisObj) then
      AVisualRec.ClassType := TVisObjClass(D.ClassType);
    AVisualRec.Title := D.Title;
    AVisualRec.Description := D.Description;
  end;

end;

class function TVisualization.Rectangle(ACtl: TVisCtl2D;
  AVisual: TVisual): TDrawable;
var
  Rectangle: TRectangle absolute Result;
begin
  Rectangle := TRectangle.Create(ACtl, ACtl.View);
  with AVisual.Data as TRectangleData do
    Rectangle.Rect := TRectD.Create(a.X, a.Y - h, a.X + w, a.Y);
end;

class function TVisualization.RegionPlot(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable;
var
  RegionPlot: TRegion absolute Result;
begin
  RegionPlot := TRegion.Create(ACtl, ACtl.View);
  with AVisual.Data as TRegionDataR2 do
  begin
    RegionPlot.SliceData := Slices;
    RegionPlot.Axis := Axis;
    RegionPlot.UnboundedMin := UnboundedMin;
    RegionPlot.UnboundedMax := UnboundedMax;
  end;
  RegionPlot.Points := False;
  RegionPlot.Lines := False;
end;

class procedure TVisualization.RemoveVisual(const AGUID: TGUID);
var
  Drawable: TDrawable;
  Drawable3D: TDrawable3D;
begin
  if TDrawable.TryGetDrawableByGUID(AGUID, Drawable) then
  begin
    if TDrawable.ModalLevel > 0 then
      raise Exception.Create('Cannot remove a drawable object when a settings dialog is open.');
    if Drawable is TDiagram then
    begin
      if Drawable.Control.Parent is TVisFrm then
        Drawable.Control.Parent.Free
      else if Drawable.Control is TManagedVisCtl2D then
        Drawable.Control.Free;
    end
    else
    begin
      Drawable.Control.BeginBackgroundPaint;
      try
        Drawable.Control.RemoveObject(Drawable);
      finally
        Drawable.Control.EndBackgroundPaint;
      end;
    end;
  end
  else if TDrawable3D.TryGetDrawableByGUID(AGUID, Drawable3D) then
  begin
    if TDrawable3D.ModalLevel > 0 then
      raise Exception.Create('Cannot remove a drawable object when a settings dialog is open.');
    if Drawable3D is TScene then
    begin
      if Drawable3D.Control.Parent is TVisFrm then
        Drawable3D.Control.Parent.Free
      else if Drawable3D.Control is TManagedVisCtl3D then
        Drawable3D.Control.Free;
    end
    else
    begin
      Drawable3D.Control.BeginBackgroundPaint;
      try
        Drawable3D.Control.RemoveObject(Drawable3D);
      finally
        Drawable3D.Control.EndBackgroundPaint;
      end;
    end;
  end;
end;

class function TVisualization.ScatterPlot(ACtl: TVisCtl2D; AVisual: TVisual): TDrawable;
var
  ScatterPlot: TXYPlot absolute Result;
begin
  ScatterPlot := TXYPlot.Create(ACtl, ACtl.View);
  ScatterPlot.Data := ASR2ArrayToDoublePointArray((AVisual.Data as TScatterDataR2).Points);
end;

class function TVisualization.ScatterPlot(ACtl: TVisCtl3D; AVisual: TVisual): TDrawable3D;
var
  SimpleScatterPlot: TSimpleScatterPlot absolute Result;
  AdvScatterPlot: TAdvScatterPlot absolute Result;
begin
  if AVisual.Data is TScatterDataR3 then
  begin
    SimpleScatterPlot := TSimpleScatterPlot.Create(ACtl);
    SimpleScatterPlot.DataAsDoubles := (AVisual.Data as TScatterDataR3).Points;
  end
  else if AVisual.Data is TScatterDataR3cs then
  begin
    AdvScatterPlot := TAdvScatterPlot.Create(ACtl);
    AdvScatterPlot.DataAsDoubles := (AVisual.Data as TScatterDataR3cs).Points;
  end;
end;

class function TVisualization.ShowDiagram(ADiagram: TVisCtl2D): Boolean;
begin
  Result := ShowDiagram(ADiagram.Diagram);
end;

class function TVisualization.ShowDiagram(ADiagram: TDiagram): Boolean;
begin

  var Ctl := ADiagram.Control;
  Result := Assigned(Ctl);
  if Result then
  begin
    if Ctl.Parent = AlgosimMainForm.GalleryPanel then
    begin
      if AlgosimMainForm.GUIMode = guiConsole then
        AlgosimMainForm.GUIMode := guiMixed;
      TGallery.ShowControl(Ctl);
    end
    else
    begin
      var Frm := GetParentForm(Ctl);
      if Assigned(Frm) then
        Frm.Show;
    end;
  end;

end;

class function TVisualization.ShowScene(AScene: TVisCtl3D): Boolean;
begin
  Result := ShowScene(AScene.Scene);
end;

class function TVisualization.ShowScene(AScene: TScene): Boolean;
begin

  var Ctl := AScene.Control;
  Result := Assigned(Ctl);
  if Result then
  begin
    if Ctl.Parent = AlgosimMainForm.GalleryPanel then
    begin
      if AlgosimMainForm.GUIMode = guiConsole then
        AlgosimMainForm.GUIMode := guiMixed;
      TGallery.ShowControl(Ctl);
    end
    else
    begin
      var Frm := GetParentForm(Ctl);
      if Assigned(Frm) then
        Frm.Show;
    end;
  end;

end;

class function TVisualization.VectorField(ACtl: TVisCtl2D;
  AVisual: TVisual): TDrawable;
var
  VectorField: TVectorField absolute Result;
  VFD: TVectorFieldDataR2;
begin
  VFD := AVisual.Data as TVectorFieldDataR2;
  VectorField := TVectorField.Create(ACtl, ACtl.View);
  VectorField.BeginAddVector;
  try
    for var i := 0 to High(VFD.Vectors) do
      VectorField.AddVector(VFD.Vectors[i].Key, VFD.Vectors[i].Value);
  finally
    VectorField.EndAddVector;
  end;
end;

class function TVisualization.VectorField(ACtl: TVisCtl3D;
  AVisual: TVisual): TDrawable3D;
var
  VectorField: rgl.TVectorField absolute Result;
  VFD: TVectorFieldDataR3;
begin
  VFD := AVisual.Data as TVectorFieldDataR3;
  VectorField := rgl.TVectorField.Create(ACtl);
  VectorField.Data := VFD.Vectors;
  VectorField.PerVertexColors := VFD.Colored;
end;

class procedure TVisualization.Visualize(AVisual: TVisual; ARef: PAlgosimReference);
begin

  case AVisual.Target of
    vt2D:
      begin

        var NewDiagram: Boolean;
        var Ctl := GetDiagram(NewDiagram);

        if Ctl = nil then
          raise Exception.Create('No diagram control.');

        Ctl.BeginBackgroundPaint;
        try

          var Obj := TDrawable(nil);

          case AVisual.Kind of
            vkNull: ;
            vkBarChart:
              Obj := BarChart(Ctl, AVisual);
            vkPieChart:
              Obj := PieChart(Ctl, AVisual);
            vkHistogram:
              Obj := Histogram(Ctl, AVisual);
            vkXYPlot:
              Obj := ScatterPlot(Ctl, AVisual);
            vkRegion:
              Obj := RegionPlot(Ctl, AVisual);
            vkHeatmap:
              Obj := Heatmap(Ctl, AVisual);
            vkVectorField:
              Obj := VectorField(Ctl, AVisual);
            vkPixmap:
              Obj := Pixmap(Ctl, AVisual);
            vkText:
              Obj := Text(Ctl, AVisual);
            vkLine:
              Obj := Line(Ctl, AVisual);
            vkRectangle:
              Obj := Rectangle(Ctl, AVisual);
            vkCircle:
              Obj := Circle(Ctl, AVisual);
            vkPolygon:
              Obj := Polygon(Ctl, AVisual);
            vkArrow:
              Obj := Arrow(Ctl, AVisual);
          end;

          if Assigned(Obj) then
          begin
            Ctl.AddObject(Obj);
            if NewDiagram and Assigned(AVisual.ViewSetupProc2D) then
              AVisual.ViewSetupProc2D(Ctl, Obj);
            if Assigned(AVisual.OwnSetupProc2D) then
              AVisual.OwnSetupProc2D(Ctl, Obj);
            if Assigned(ARef) then
              ARef^ := Obj.CreateReference;
          end;

        finally
          Ctl.EndBackgroundPaint;
        end;

      end;
    vt3D:
      begin

        var NewScene: Boolean;
        var Ctl := GetScene(NewScene);

        if Ctl = nil then
          raise Exception.Create('No visualization control.');

        Ctl.BeginBackgroundPaint;
        try

          var Obj := TDrawable3D(nil);

          case AVisual.Kind of
            vkNull: ;
            vkXYZPlot, vkXYZcsPlot:
              Obj := ScatterPlot(Ctl, AVisual);
            vkSurface:
              Obj := Surface(Ctl, AVisual);
            vkSpaceCurve:
              Obj := SpaceCurve(Ctl, AVisual);
            vkObject3D:
              Obj := Object3D(Ctl, AVisual);
            vkVectorField:
              Obj := VectorField(Ctl, AVisual);
            vkPixmap:
              Obj := Pixmap(Ctl, AVisual);
            vkText:
              Obj := Text(Ctl, AVisual);
            vkArrow:
              Obj := Arrow(Ctl, AVisual);
          end;

          if Assigned(Obj) then
          begin
            Ctl.AddObject(Obj);
            if NewScene and Assigned(AVisual.ViewSetupProc3D) then
              AVisual.ViewSetupProc3D(Ctl, Obj);
            if Assigned(AVisual.OwnSetupProc3D) then
              AVisual.OwnSetupProc3D(Ctl, Obj);
            if Assigned(ARef) then
              ARef^ := Obj.CreateReference;
          end;

        finally
          Ctl.EndBackgroundPaint;
        end;

      end;
  end;

end;

class function TVisualization.ShowDiagram(const ADiagram: string): Boolean;
var
  Ctl: TManagedVisCtl2D;
begin
  Result := TManagedVisCtl2D.Instances.TryGetValue(ADiagram, Ctl);
  if Result then
    ShowDiagram(Ctl);
end;

class function TVisualization.ShowDiagram(const ADiagram: TGUID): Boolean;
var
  Drawable: TDrawable;
begin
  Result := TDrawable.TryGetDrawableByGUID(ADiagram, Drawable);
  if Result then
    ShowDiagram(Drawable.Control);
end;

class function TVisualization.ShowScene(const AScene: string): Boolean;
var
  Ctl: TManagedVisCtl3D;
begin
  Result := TManagedVisCtl3D.Instances.TryGetValue(AScene, Ctl);
  if Result then
    ShowScene(Ctl);
end;

class function TVisualization.ShowScene(const AScene: TGUID): Boolean;
var
  Drawable: TDrawable3D;
begin
  Result := TDrawable3D.TryGetDrawableByGUID(AScene, Drawable);
  if Result then
    ShowScene(Drawable.Control);
end;

class function TVisualization.ShowVisCtl(const AID: TGUID): Boolean;
var
  Drawable2D: TDrawable;
  Drawable3D: TDrawable3D;
begin
  Result := TDrawable.TryGetDrawableByGUID(AID, Drawable2D);
  if Result then
  begin
    ShowDiagram(Drawable2D.Control);
    Exit;
  end;
  Result := TDrawable3D.TryGetDrawableByGUID(AID, Drawable3D);
  if Result then
  begin
    ShowScene(Drawable3D.Control);
    Exit;
  end;
end;

class function TVisualization.SpaceCurve(ACtl: TVisCtl3D;
  AVisual: TVisual): TDrawable3D;
var
  Curve: TCurve3D absolute Result;
  CCurve: TColoredCurve3D absolute Result;
begin
  if AVisual.Data is TCurveDataR3c then
  begin
    CCurve := TColoredCurve3D.Create(ACtl);
    CCurve.Data := (AVisual.Data as TCurveDataR3c).Points;
  end
  else if AVisual.Data is TCurveDataR3 then
  begin
    Curve := TCurve3D.Create(ACtl);
    Curve.Data := (AVisual.Data as TCurveDataR3).Points;
  end
  else
    raise Exception.Create('Unknown space curve data class.');
end;

class function TVisualization.Object3D(ACtl: TVisCtl3D;
  AVisual: TVisual): TDrawable3D;
begin
  var D := AVisual.Data as TObject3DData;
  Result := D.ObjectClass.Create(ACtl);
  if not D.Name.IsEmpty then
    Result.Name := D.Name;
  if Result is TGeometricObject3D then
    TGeometricObject3D(Result).Color := clRed;
  if (Result is TObjModel) and not D.Data.IsEmpty then
    TObjModel(Result).LoadModel(D.Data);
end;

class function TVisualization.Surface(ACtl: TVisCtl3D;
  AVisual: TVisual): TDrawable3D;
var
  Surf: TCustomSurface absolute Result;
  CSurf: TCustomColoredSurface absolute Result;
begin
  if AVisual.Data is TColoredSurfaceData then
  begin
    CSurf := TCustomColoredSurface.Create(ACtl);
    CSurf.Data := (AVisual.Data as TColoredSurfaceData).Data;
    CSurf.Domain := (AVisual.Data as TColoredSurfaceData).Domain;
    CSurf.Nx := (AVisual.Data as TColoredSurfaceData).Nx;
    CSurf.Ny := (AVisual.Data as TColoredSurfaceData).Ny;
  end
  else if AVisual.Data is TSurfaceData then
  begin
    Surf := TCustomSurface.Create(ACtl);
    Surf.Data := (AVisual.Data as TSurfaceData).Data;
    Surf.Domain := (AVisual.Data as TSurfaceData).Domain;
    Surf.Nx := (AVisual.Data as TSurfaceData).Nx;
    Surf.Ny := (AVisual.Data as TSurfaceData).Ny;
    Surf.Color := clRed;
  end
  else
    raise Exception.Create('Unknown surface data class.');
end;

class function TVisualization.Text(ACtl: TVisCtl3D;
  AVisual: TVisual): TDrawable3D;
var
  Text: TTextRect absolute Result;
begin
  Text := TTextRect.Create(ACtl);
  Text.Text := (AVisual.Data as TTextData).Text;
end;

class function TVisualization.Text(ACtl: TVisCtl2D;
  AVisual: TVisual): TDrawable;
var
  Text: TText absolute Result;
begin
  Text := TText.Create(ACtl, ACtl.View);
  Text.Position := (AVisual.Data as TTextData).Position;
  Text.Text := (AVisual.Data as TTextData).Text;
end;

{ TManagedVisCtl2D }

class constructor TManagedVisCtl2D.ClassCreate;
begin
  FInstances := TDictionary<string, TManagedVisCtl2D>.Create;
end;

class destructor TManagedVisCtl2D.ClassDestroy;
begin
  if Assigned(FInstances) then
    for var Ctl in FInstances do
      Ctl.Value.Free;
  FreeAndNil(FInstances);
end;

procedure TManagedVisCtl2D.ContextPopup(Sender: TObject);
begin
  FmnuDetach.Visible := Parent = AlgosimMainForm.GalleryPanel;
  FmnuDock.Visible := Parent is TVisFrm;
end;

constructor TManagedVisCtl2D.Create(AOwner: TComponent);
begin
  inherited;
  FDiagramName := TVisualization.Diagram;
  if Assigned(FInstances) then
    FInstances.Add(TVisualization.Diagram, Self);
  FmnuDetach := TMenuItem.Create(Self);
  FmnuDetach.Caption := 'Detach';
  FmnuDetach.Hint := 'Detaches this diagram from the main form.';
  FmnuDetach.Tag := NativeInt(Self);
  FmnuDetach.OnClick := Detach;
  AddMenuItem(FmnuDetach);
  FmnuDock := TMenuItem.Create(Self);
  FmnuDock.Caption := 'Dock';
  FmnuDock.Hint := 'Attaches this diagram in the main form.';
  FmnuDock.Tag := NativeInt(Self);
  FmnuDock.OnClick := Dock;
  AddMenuItem(FmnuDock);
  OnBeforeContextPopup := ContextPopup;
end;

destructor TManagedVisCtl2D.Destroy;
begin
  if Assigned(FInstances) then
    FInstances.Remove(FDiagramName);
  inherited;
end;

procedure TManagedVisCtl2D.Detach(Sender: TObject);
begin
  if Parent = AlgosimMainForm.GalleryPanel then
  begin
    var frm := TVisFrm.CreateNew(AlgosimMainForm);
    frm.Caption := FDiagramName;
    Parent := frm;
    Align := alClient;
    frm.Show;
  end;
end;

procedure TManagedVisCtl2D.Dock(Sender: TObject);
begin
  if Parent is TVisFrm then
  begin
    var frm := TVisFrm(Parent);
    Parent := AlgosimMainForm.GalleryPanel;
    Align := alClient;
    TGallery.ShowControl(Self);
    frm.Free;
  end;
end;

procedure TManagedVisCtl2D.SetDetached(ADetached: Boolean);
begin
  if ADetached then
    Detach(Self)
  else
    Dock(Self);
end;

procedure TManagedVisCtl2D.SetWindowHeight(AHeight: Integer);
begin
  Detach(Self);
  if Parent is TVisFrm then
    TVisFrm(Parent).ClientHeight := AHeight;
end;

procedure TManagedVisCtl2D.SetWindowWidth(AWidth: Integer);
begin
  Detach(Self);
  if Parent is TVisFrm then
    TVisFrm(Parent).ClientWidth := AWidth;
end;

{ TManagedVisCtl3D }

class constructor TManagedVisCtl3D.ClassCreate;
begin
  FInstances := TDictionary<string, TManagedVisCtl3D>.Create;
end;

class destructor TManagedVisCtl3D.ClassDestroy;
begin
  if Assigned(FInstances) then
    for var Ctl in FInstances do
      Ctl.Value.Free;
  FreeAndNil(FInstances);
end;

procedure TManagedVisCtl3D.ContextPopup(Sender: TObject);
begin
  FmnuDetach.Visible := Parent = AlgosimMainForm.GalleryPanel;
  FmnuDock.Visible := Parent is TVisFrm;
end;

constructor TManagedVisCtl3D.Create(AOwner: TComponent);
begin
  inherited;
  FSceneName := TVisualization.Scene;
  if Assigned(FInstances) then
    FInstances.Add(TVisualization.Scene, Self);
  FmnuDetach := TMenuItem.Create(Self);
  FmnuDetach.Caption := 'Detach';
  FmnuDetach.Hint := 'Detaches this scene from the main form.';
  FmnuDetach.Tag := NativeInt(Self);
  FmnuDetach.OnClick := Detach;
  AddMenuItem(FmnuDetach);
  FmnuDock := TMenuItem.Create(Self);
  FmnuDock.Caption := 'Dock';
  FmnuDock.Hint := 'Attaches this scene in the main form.';
  FmnuDock.Tag := NativeInt(Self);
  FmnuDock.OnClick := Dock;
  AddMenuItem(FmnuDock);
  OnBeforeContextPopup := ContextPopup;
end;

destructor TManagedVisCtl3D.Destroy;
begin
  if Assigned(FInstances) then
    FInstances.Remove(FSceneName);
  inherited;
end;

procedure TManagedVisCtl3D.Detach(Sender: TObject);
begin
  if Parent = AlgosimMainForm.GalleryPanel then
  begin
    var frm := TVisFrm.CreateNew(AlgosimMainForm);
    frm.Caption := FSceneName;
    Parent := frm;
    Align := alClient;
    frm.Show;
  end;
end;

procedure TManagedVisCtl3D.Dock(Sender: TObject);
begin
  if Parent is TVisFrm then
  begin
    var frm := TVisFrm(Parent);
    Parent := AlgosimMainForm.GalleryPanel;
    Align := alClient;
    TGallery.ShowControl(Self);
    frm.Free;
  end;
end;

procedure TManagedVisCtl3D.SetDetached(ADetached: Boolean);
begin
  if ADetached then
    Detach(Self)
  else
    Dock(Self);
end;

procedure TManagedVisCtl3D.SetWindowHeight(AHeight: Integer);
begin
  Detach(Self);
  if Parent is TVisFrm then
    TVisFrm(Parent).ClientHeight := AHeight;
end;

procedure TManagedVisCtl3D.SetWindowWidth(AWidth: Integer);
begin
  Detach(Self);
  if Parent is TVisFrm then
    TVisFrm(Parent).ClientWidth := AWidth;
end;

{ TVisFrm }

function TVisFrm.CanResize(var NewWidth, NewHeight: Integer): Boolean;
begin
  Result := True;
  if FAspect <> 0.0 then
    NewWidth := Max(64, Round(NewHeight * FAspect));
end;

procedure TVisFrm.CMTextChanged(var Message: TMessage);
begin
  inherited;
  TWndMgr.CaptionChanged(Self);
end;

constructor TVisFrm.CreateNew(AOwner: TComponent; Dummy: Integer);
var
  WA, FA: TRect;
  PrefW, PrefH, MinSize: Integer;

type
  TSide = (sNone, sTop, sRight, sBottom, sLeft);

  procedure Init;
  begin
    var MF := AlgosimMainForm;
    FA := MF.BoundsRect;
    var MON := MF.Monitor;
    if Assigned(MON) then
      WA := MON.WorkareaRect
    else
      WA := Screen.WorkAreaRect;
    PrefW := _Scale(800);
    PrefH := _Scale(600);
    MinSize := _Scale(400);
    DefaultMonitor := dmDesktop;
    Position := poDesigned;
  end;

  procedure PlaceToTheRight;
  begin
    Width := Math.Min(PrefW, WA.Right - FA.Right);
    Left := FA.Right;
    Height := Math.Min(PrefH, WA.Height);
    Top := WA.Top + Random(WA.Height - Height);
  end;

  procedure PlaceToTheLeft;
  begin
    Width := Math.Min(PrefW, FA.Left - WA.Left);
    Left := FA.Left - Width;
    Height := Math.Min(PrefH, WA.Height);
    Top := WA.Top + Random(WA.Height - Height);
  end;

  procedure PlaceAbove;
  begin
    Height := Math.Min(PrefH, FA.Top - WA.Top);
    Top := FA.Top - Height;
    Width := Math.Min(PrefW, WA.Width);
    Left := WA.Left + Random(WA.Width - Width);
  end;

  procedure PlaceBelow;
  begin
    Height := Math.Min(PrefH, WA.Bottom - FA.Bottom);
    Top := FA.Bottom;
    Width := Math.Min(PrefW, WA.Width);
    Left := WA.Left + Random(WA.Width - Width);
  end;

  function BestSide: TSide;
  var
    SpaceTop, SpaceRight, SpaceBottom, SpaceLeft: Integer;
    Spaces: TArray<Integer>;
    MaxSpace: Integer;
  begin
    SpaceTop := FA.Top - WA.Top;
    SpaceRight := WA.Right - FA.Right;
    SpaceBottom := WA.Bottom - FA.Bottom;
    SpaceLeft := FA.Left - WA.Left;
    Spaces := [SpaceTop, SpaceRight, SpaceBottom, SpaceLeft];
    MaxSpace := MaxIntValue(Spaces);
    if MaxSpace < MinSize then
      Exit(sNone)
    else
      Result := TSide(IndexInt(MaxSpace, Spaces) + 1);
  end;

  function PlaceNextToForm: Boolean;
  begin
    Result := True;
    case BestSide of
      sTop:
        PlaceAbove;
      sRight:
        PlaceToTheRight;
      sBottom:
        PlaceBelow;
      sLeft:
        PlaceToTheLeft;
    else
      Result := False;
    end;
  end;

  function PlaceOnSecondScreen: Boolean;
  begin
    Exit(False); // We cannot even know if it is turned on...
    {
    Result := Screen.MonitorCount > 1;
    if Result then
      for var i := 0 to Screen.MonitorCount - 1 do
        if Screen.Monitors[i] <> AlgosimMainForm.Monitor then
        begin
          var R := Screen.Monitors[i].WorkareaRect;
          Width := Math.Min(PrefW, R.Width);
          Height := Math.Min(PrefH, R.Height);
          Left := EnsureRange(R.Left + Random(R.Width - Width), R.Left, R.Right - Width);
          Top := EnsureRange(R.Top + Random(R.Height - Height), R.Top, R.Bottom - Height);
          Exit;
        end;
    Result := False;
    }
  end;

  function PlaceOnRightSideOfMainForm: Boolean;
  begin
    Width := Math.Min(PrefW, WA.Width);
    Height := Math.Min(PrefH, WA.Height);
    Left := WA.Right - Width;
    Top := WA.Top + Random(WA.Height - Height);
    Result := True;
  end;

begin

  inherited;

  Color := clWhite;

  Init;

  if PlaceNextToForm or PlaceOnSecondScreen or PlaceOnRightSideOfMainForm then
    MakeFullyVisible;

  TFormFader.Create(Self);
  TWindowWatcher.Create(Self);

  var SysMenu := GetSystemMenu(Handle, False);
  if SysMenu = 0 then Exit;
  AppendMenu(SysMenu, MF_SEPARATOR, 0, nil);
  AppendMenu(SysMenu, MF_STRING, CMD_SETSIZE, 'Set size...');
  AppendMenu(SysMenu, MF_SEPARATOR, 0, nil);
  AppendMenu(SysMenu, MF_STRING, CMD_DISCARD, 'Close and discard'#9'Shift+Alt+F4');

  KeyPreview := True;

end;

procedure TVisFrm.DoClose(var Action: TCloseAction);
begin
  inherited;
  if IsKeyDown(VK_SHIFT) then
    Action := caFree;
end;

procedure TVisFrm.KeyPress(var Key: Char);
begin
  inherited;
  case Key of
    's':
      begin
        Perform(WM_SYSCOMMAND, CMD_SETSIZE, 0);
        Key := #0;
      end;
  end;
end;

procedure TVisFrm.WMSysCommand(var Msg: TWMSysCommand);
begin

  inherited;
  case Msg.CmdType of
    CMD_SETSIZE:
      begin
        var W := ClientWidth;
        var H := ClientHeight;
        var Aspect: Double := FAspect;
        if ImageSizeDialog(Self, W, H, Aspect) then
        begin
          FAspect := 0.0;
          if WindowState <> TWindowState.wsNormal then
            WindowState := wsNormal;
          ClientWidth := W;
          ClientHeight := H;
          FAspect := Aspect;
        end;
      end;
    CMD_DISCARD:
      Release;
  end;

end;

end.
