unit MainFm;
(*
 * Copyright (c) 2015 - 2025 Dr. Yuriy Kotsarenko. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and limitations under the License.
 *)

// This example illustrates the following features:
//  * Creating and working with framework UI widgets. "SketchPad" widget is used as a 3D viewports and four
//    of those are used simultaneously to view and manipulate different areas of the scene.
//  * This illustrates using "Placer" widget as a 2x2 grid for the viewports, as well as for placing labels
//    alongside gauge/spin edits so that it is automatically resized.
//  * Changing parameters of P-Q torus knot and super-ellipse at runtime using UI widgets.
//  * Showing multiple UI windows on top of each other, using semi-transparency and blur effects.
//  * Enumerating properties for existing UI widgets and saving them to disk.

interface

uses
  System.Types, System.Classes, Vcl.Graphics, Vcl.Forms, Vcl.Dialogs, Vcl.Controls, PXT.Types, PXT.Graphics,
  PXT.Controls, PXT.Widgets, SketchpadViewports;

type
  TMainForm = class(TForm)
    SaveDialog: TSaveDialog;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure FormKeyPress(Sender: TObject; var Key: Char);
  private
    FRenderingControl: TRenderingControl;
    FDisplaySize: TPoint2i;
    FDisplayScale: Single;
    FMousePos: TPoint2i;

    FDevice: TDevice;
    FSwapChain: TSwapChain;
    FCanvas: TCanvas;
    FTextRenderer: TTextRenderer;

    FMeshes: TSceneMeshes;
    FObjects: TObjectModels;
    FSceneLights: TSceneLights;
    FMaterials: TObjectMaterials;
    FTimer: TTimer;
    FManager: TWidget;

    FShadowCastingAtlas: TShadowCastingAtlas;
    FShadowCasterView: TObjectModelView;
    FShadowMapNeedsUpdate: Boolean;

    FSceneDepthNormals: TScene;
    FSceneModeling: TScene;
    FConstructingUI: Boolean;
    FRecreateTorusKnot: Boolean;
    FRecreateCube: Boolean;

    procedure ApplicationIdle(Sender: TObject; var Done: Boolean);
    procedure RenderingControlResize(Sender: TObject);
    procedure RenderingControlMouseDown(Sender: TObject; Button: Vcl.Controls.TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure RenderingControlMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
    procedure RenderingControlMouseUp(Sender: TObject; Button: Vcl.Controls.TMouseButton; Shift: TShiftState;
      X, Y: Integer);
    procedure RenderingControlMouseWheelUp(Sender: TObject; Shift: TShiftState; MousePos: TPoint;
      var Handled: Boolean);
    procedure RenderingControlMouseWheelDown(Sender: TObject; Shift: TShiftState; MousePos: TPoint;
      var Handled: Boolean);
    procedure RenderingControlPaint(Sender: TObject);
    function ManagerUpdateSettle: Boolean;
    procedure UpdateViewports(const AParent: TWidget);
    procedure InvalidateViewports(const AParent: TWidget);
    procedure ReleaseViewports(const AParent: TWidget);
    procedure RecreateTorusKnot;
    procedure RecreateCube;
    procedure AssignMeshToObjects(const AMesh: TSceneMesh);
    function RemoveAndUnassignMesh(const AMeshName: Utf8String): Boolean;
    procedure EnumeratePropertiesAndSaveToDisk;
    procedure CreateObjects;
    procedure CreateMeshes;
    procedure CreateLightsAndMaterials;
    procedure CreateRenderingControl;
    procedure CreateUI;
  public
  end;

var
  MainForm: TMainForm;

implementation
{$R *.dfm}

uses
  System.SysUtils, System.Math, System.UITypes, System.Rtti, PXT.Headers, PXT.Graphics.Consts,
  PXT.Widgets.Utility;

const
  VertexElements: array[0..2] of TVertexElement = (
    (Name: 'vertexPosition'; Format: TElementFormat.Float; Count: 3; Channel: 0; Offset: 0),
    (Name: 'vertexNormal'; Format: TElementFormat.Byte; Count: 4; Channel: 0 or TVertexElement.Normalized; Offset: 0),
    (Name: 'vertexColor'; Format: TElementFormat.UnsignedByte; Count: 4; Channel: 0 or TVertexElement.Normalized; Offset: 0));

  LightPosition: TVector3f = (X: 300.0; Y: 800.0; Z: -300.0);

function InterfaceExternalEvent(Widget: TUntypedObjectHandle; Event: PAnsiChar;
  User: Pointer): TLibraryBool; cdecl; forward;

procedure TMainForm.FormCreate(Sender: TObject);
var
  LDepthClipNegative: Boolean;
begin
  CreateRenderingControl;
  FDisplaySize := Point2i(FRenderingControl.Width, FRenderingControl.Height);
  FDisplayScale := PixelsPerInch / 96.0;
  LDepthClipNegative := FDevice.Behavior and TDeviceBehavior.DepthClipNegative <> 0;
  try
    FDevice := TDevice.Create;
    FSwapChain := TSwapChain.Create(FDevice, FRenderingControl.WindowHandle, FDisplaySize);
    FCanvas := TCanvas.Create(FDevice);
    FTextRenderer := TTextRenderer.Create(FCanvas, Point2i(256, 256), TPixelFormat.LA8);

    FMeshes := TSceneMeshes.Create(FDevice);
    FObjects := TObjectModels.Create(FMeshes);
    FSceneLights := TSceneLights.Create(FDevice);
    FMaterials := TObjectMaterials.Create;

    FShadowCastingAtlas := TShadowCastingAtlas.Create(FDevice, Point2i(256, 256), TTechniqueShadows.EVSM, 8);

    FSceneDepthNormals := TScene.CreateDepthNormals(FDevice);
    FSceneDepthNormals.SetVertexElements(0, VertexElements);

    FSceneModeling := TScene.CreateModeling(FDevice);
    FSceneModeling.SetVertexElements(0, VertexElements);

    FShadowCasterView := FObjects.CreateView;
    FShadowCasterView.View := TMatrix4f.LookAt(LightPosition, TVector3f.Zero, TVector3f.AxisY);
    FShadowCasterView.SetProjection(TMatrix4f.PerspectiveFOVY(Pi * 0.25, 1.0, 100.0, 1400.0,
      LDepthClipNegative), LDepthClipNegative);

    FShadowCastingAtlas.Add(Point2i(256, 256));

    FTimer := TTimer.Create;
    FManager := TWidget.CreateManager(nil, FTextRenderer, 'Manager', TPixelFormat.RGBA8, 8,
      TWidgetManagerAttribute.Composition, nil);
    FManager.Scale := FDisplayScale;
    FManager.Rect := FloatRect(0.0, 0.0, FDisplaySize.X, FDisplaySize.Y) * (1.0 / FDisplayScale);
    FManager.SetExternalEvent(InterfaceExternalEvent, Self);

    CreateLightsAndMaterials;

    FConstructingUI := True;
    try
      CreateUI;
    finally
      FConstructingUI := False;
    end;

    CreateMeshes;
    RecreateTorusKnot;
    RecreateCube;
    CreateObjects;
  except
    on E: EAfterwarpException do
    begin
      MessageDlg(E.Message, mtError, [mbOK], 0);
      Application.Terminate;
      Exit;
    end;
  end;
  Application.OnIdle := ApplicationIdle;
  FShadowMapNeedsUpdate := True;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  ReleaseViewports(FManager);
  FManager.Free;
  FTimer.Free;
  FSceneModeling.Free;
  FSceneDepthNormals.Free;
  FShadowCastingAtlas.Free;
  FMaterials.Free;
  FSceneLights.Free;
  FObjects.Free;
  FMeshes.Free;
  FTextRenderer.Free;
  FCanvas.Free;
  FSwapChain.Free;
  FDevice.Free;
end;

procedure TMainForm.ApplicationIdle(Sender: TObject; var Done: Boolean);
begin
  FTimer.Update;

  if FManager.Update(FTimer.Latency / (1000.0 * 1000.0)) then
    FRenderingControl.Invalidate
  else
    Sleep(5);

  Done := False;
end;

procedure TMainForm.RenderingControlResize(Sender: TObject);
begin
  FDisplayScale := Max(PixelsPerInch / 96.0, 1.0);

  if FSwapChain.Handle <> nil then
  begin
    FDisplaySize := Point2i(FRenderingControl.Width, FRenderingControl.Height);
    FSwapChain.Resize(FDisplaySize);

    FManager.Scale := FDisplayScale;
    FManager.Rect := FloatRect(0.0, 0.0, FDisplaySize.X, FDisplaySize.Y) * (1.0 / FDisplayScale);
    FRenderingControl.Invalidate;
  end;
end;

procedure TMainForm.RenderingControlMouseDown(Sender: TObject; Button: Vcl.Controls.TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  FMousePos := Point2i(X, Y);
  FManager.AcceptMouse(TMouseEvent.MouseDown, MouseButtonToUI(Button), FMousePos / FDisplayScale);
end;

procedure TMainForm.RenderingControlMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
begin
  FMousePos := Point2i(X, Y);
  FManager.AcceptMouse(TMouseEvent.MouseMove, PXT.Types.TMouseButton.None, FMousePos / FDisplayScale);
  FRenderingControl.Cursor := CursorFromAppCursor(FManager.Cursor);
end;

procedure TMainForm.RenderingControlMouseUp(Sender: TObject; Button: Vcl.Controls.TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  FMousePos := Point2i(X, Y);
  FManager.AcceptMouse(TMouseEvent.MouseUp, MouseButtonToUI(Button), FMousePos / FDisplayScale);
end;

procedure TMainForm.RenderingControlMouseWheelUp(Sender: TObject; Shift: TShiftState; MousePos: TPoint;
  var Handled: Boolean);
begin
  FManager.AcceptMouse(TMouseEvent.WheelUp, PXT.Types.TMouseButton.None, FMousePos / FDisplayScale);
end;

procedure TMainForm.RenderingControlMouseWheelDown(Sender: TObject; Shift: TShiftState; MousePos: TPoint;
  var Handled: Boolean);
begin
  FManager.AcceptMouse(TMouseEvent.WheelDown, PXT.Types.TMouseButton.None, FMousePos / FDisplayScale);
end;

procedure TMainForm.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  FManager.AcceptKey(TKeyEvent.Pressed, PlatformKeyToUI(Key), 0);
end;

procedure TMainForm.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  FManager.AcceptKey(TKeyEvent.Released, PlatformKeyToUI(Key), 0);
end;

procedure TMainForm.FormKeyPress(Sender: TObject; var Key: Char);
begin
  FManager.AcceptKey(TKeyEvent.Typing, TKey.Null, Ord(Key));
end;

procedure TMainForm.RenderingControlPaint(Sender: TObject);
var
  LShadowCaster: TShadowCaster;
  LFontParams: TFontParameters;
  LTechString: string;
  LTexCoords: TQuad;
begin
  FDisplayScale := PixelsPerInch / 96.0;

  if FRecreateTorusKnot then
  begin
    RecreateTorusKnot;
    FRecreateTorusKnot := False;
  end;

  if FRecreateCube then
  begin
    RecreateCube;
    FRecreateCube := False;
  end;

  ManagerUpdateSettle;

  // Update shadow map. This is only needed when some object has moved.
  if FShadowMapNeedsUpdate then
  begin
    FShadowMapNeedsUpdate := False;
    FShadowCastingAtlas.BorderFill;

    LShadowCaster := FShadowCastingAtlas.Casters[0];
    LShadowCaster.ViewProjection := FShadowCasterView.ViewProjection;

    FShadowCasterView.Update;
    LShadowCaster.Clear;
    LShadowCaster.BeginScene;
    try
      FSceneDepthNormals.Attributes := [];
      FSceneDepthNormals.View := FShadowCasterView.View;
      FSceneDepthNormals.Projection := FShadowCasterView.Projection;

      FSceneDepthNormals.BeginScene;
      try
        FShadowCasterView.AutoDraw(FSceneDepthNormals, 0);
      finally;
        FSceneDepthNormals.EndScene;
      end;
    finally
      LShadowCaster.EndScene;
    end;
    LShadowCaster.Filter;
  end;

  // Update viewport contents.
  UpdateViewports(FManager);

  // Render UI.
  if FManager.Attributes and TWidgetManagerAttribute.Composition <> 0 then
  begin
    FManager.Texture[TWidgetManagerTextureType.Screen].Clear(FloatColor(0.0, 1.0, 0.0, 1.0));
    FManager.Present;
  end;

  // Render on the form.
  FSwapChain.BeginScene;
  try
    FDevice.Clear([TClearLayer.Color], FloatColor($FF000050));

    FCanvas.BeginScene;
    try
      // Draw UI composition texture.
      if FManager.Attributes and TWidgetManagerAttribute.Composition <> 0 then
      begin
        FCanvas.SamplerState := TCanvasSamplerState.Create(TTextureFilter.Nearest, TTextureFilter.Nearest);
        FCanvas.ContextState := TCanvasContextState.FlatScene;
        FCanvas.Attributes := [];

        LTexCoords := TQuad.Unity;

        if FDevice.Technology = TDeviceTechnology.OpenGL then
          LTexCoords := LTexCoords.Flip;

        FCanvas.Quad(FManager.Texture[TWidgetManagerTextureType.Screen], Quad(0.0, 0.0, FDisplaySize.X,
          FDisplaySize.Y), LTexCoords, TColorRect.White);
      end
      else
        FManager.Present;

      // Draw information text.
      FCanvas.ContextState := TCanvasContextState.FlatText;

      LFontParams := TFontParameters.Create('Segoe UI', 14.0 * FDisplayScale, TFontWeight.Medium);
      LFontParams.Effect.FillBrightness := 1.0;
      LFontParams.Effect.BorderBrightness := 0.25;
      LFontParams.Effect.BorderOpacity := 0.75;
      LFontParams.Effect.BorderType := TFontBorder.SemiHeavy;
      LFontParams.Effect.ShadowBrightness := 0.15;
      LFontParams.Effect.ShadowOpacity := 0.75;
      FTextRenderer.FontParameters := LFontParams;

      if FDevice.Technology = TDeviceTechnology.Direct3D then
        LTechString := 'Direct3D (' + IntToStr(FDevice.TechVersion shr 8) + '.' +
          IntToStr((FDevice.TechVersion shr 4) and $0F) + '), features: '
      else
        LTechString := 'OpenGL (' + IntToStr(FDevice.TechVersion shr 8) + '.' +
          IntToStr((FDevice.TechVersion shr 4) and $0F) + '+), features: ';

      LTechString := LTechString + IntToStr(FDevice.TechFeatureVersion shr 8) + '.' +
        IntToStr((FDevice.TechFeatureVersion shr 4) and $0F);

      FTextRenderer.Draw(Point2f(8.0 * FDisplayScale, 4.0 * FDisplayScale), Utf8String(LTechString),
        ColorPair($FFFFFFFF));
    finally
      FCanvas.EndScene;
    end;
  finally
    FSwapChain.EndScene;
  end;
  // Update cursor from UI.
  FRenderingControl.Cursor := CursorFromAppCursor(FManager.Cursor);
end;

function TMainForm.ManagerUpdateSettle: Boolean;
var
  I: Integer;
begin
  // Perform iterative widget accomodation as long as "repaint" is requested. This ensures that all widget
  // children settle on their positioning and size.
  Result := False;

  for I := 0 to 15 do
  begin
    if FManager.Update(0.0) then
      Result := True
    else
      Break;
  end;
end;

procedure TMainForm.UpdateViewports(const AParent: TWidget);
var
  I: Integer;
  LChild: TWidget;
begin
  for I := 0 to AParent.ChildCount - 1 do
  begin
    LChild := AParent.Children[I];
    if LChild.Payload <> nil then
      TSketchpadViewport(LChild.Payload).Update;

    UpdateViewports(LChild);
  end;
end;

procedure TMainForm.InvalidateViewports(const AParent: TWidget);
var
  I: Integer;
  LChild: TWidget;
begin
  for I := 0 to AParent.ChildCount - 1 do
  begin
    LChild := AParent.Children[I];
    if LChild.Payload <> nil then
      TSketchpadViewport(LChild.Payload).Invalidate;

    InvalidateViewports(LChild);
  end;
end;

procedure TMainForm.ReleaseViewports(const AParent: TWidget);
var
  I: Integer;
  LChild: TWidget;
begin
  for I := 0 to AParent.ChildCount - 1 do
  begin
    LChild := AParent.Children[I];
    if LChild.Payload <> nil then
      TSketchpadViewport(LChild.Payload).Free;

    ReleaseViewports(LChild);
  end;
end;

procedure TMainForm.RecreateTorusKnot;
const
  MeshName = 'TorusKnot';
var
  LMeshBuffer: TMeshBuffer;
  LExists: Boolean;
  LMesh: TSceneMesh;
  LPValue, LQValue: Integer;
  LInsideRadiusShape, LLateralRotation: Single;
begin
  LMeshBuffer := TMeshBuffer.Create;
  try
    LExists := RemoveAndUnassignMesh(MeshName);

    LPValue := Round(FManager.Widget['PValueSpinEdit'].Value['Value.Last'].AsType<Double>);
    LQValue := Round(FManager.Widget['QValueSpinEdit'].Value['Value.Last'].AsType<Double>);
    LInsideRadiusShape := FManager.Widget['InsideRadiusShapeGaugeEdit'].Value['Value'].AsType<Double>;
    LLateralRotation := DegToRad(FManager.Widget['LateralRotationGaugeEdit'].Value['Value'].AsType<Double>);

    LMeshBuffer.TorusKnot(512, 128, TVector3f.Zero, LPValue, LQValue, 0.0, Pi * 2.0, 100.0, 0.0, Pi * 2.0,
      LInsideRadiusShape, Point2f(20.0, 20.0), TPoint2f.Zero, TPoint2f.Unity, LLateralRotation,
      TFloatColor.White, True);

    LMesh := FMeshes.AddFromBuffer(MeshName, LMeshBuffer, nil, VertexElements);
  finally
    LMeshBuffer.Free;
  end;

  if LExists then
    AssignMeshToObjects(LMesh);
end;

procedure TMainForm.RecreateCube;
const
  MeshName = 'Cube';
var
  LMeshBuffer: TMeshBuffer;
  LExists: Boolean;
  LMesh: TSceneMesh;
  LLongitudeShape, LLatitudeShape: Single;
begin
  LMeshBuffer := TMeshBuffer.Create;
  try
    LExists := RemoveAndUnassignMesh(MeshName);

    LLongitudeShape := FManager.Widget['LongitudeShapeGaugeEdit'].Value['Value'].AsType<Double>;
    LLatitudeShape := FManager.Widget['LatitudeShapeGaugeEdit'].Value['Value'].AsType<Double>;

    LMeshBuffer.SuperEllipse(32, 32, TVector3f.Zero, Vector3f(4.0, 4.0, 4.0), 0.0, Pi * 2.0, LLongitudeShape,
      -Pi * 0.5, Pi * 0.5, LLatitudeShape, TPoint2f.Zero, TPoint2f.Unity, TFloatColor.White, True);

    // When a surface of super-ellipse begins to have cusps, smooth normals no longer look correctly, no
    // matter how they are calculated. Therefore, opt for flat normals in such cases.
    if (LLatitudeShape >= 2.5) or (LLongitudeShape > 1.9) then
      LMeshBuffer.CalculateFlatNormals;

    LMesh := FMeshes.AddFromBuffer(MeshName, LMeshBuffer, nil, VertexElements);
  finally
    LMeshBuffer.Free;
  end;

  if LExists then
    AssignMeshToObjects(LMesh);
end;

procedure TMainForm.AssignMeshToObjects(const AMesh: TSceneMesh);
var
  LObject: TObjectModel;
  LAssigned: Boolean;
begin
  LAssigned := False;

  for LObject in FObjects do
    if LObject.Mesh.Handle = nil then
    begin
      LObject.Mesh := AMesh;
      LAssigned := True;
    end;

  if LAssigned then
  begin
    InvalidateViewports(FManager);
    FShadowMapNeedsUpdate := True;
    FRenderingControl.Invalidate;
  end;
end;

function TMainForm.RemoveAndUnassignMesh(const AMeshName: Utf8String): Boolean;
var
  LObject: TObjectModel;
  LMesh: TSceneMesh;
begin
  LMesh := FMeshes.TryMesh(AMeshName);
  Result := LMesh.Handle <> nil;
  if Result then
  begin
    for LObject in FObjects do
      if LObject.Mesh.Handle = LMesh.Handle then
      begin
        LObject.Mesh := TSceneMesh(NullObject);
        LObject.Size := TVector3f.Zero;
      end;

    FMeshes.Erase(LMesh);
  end;
end;

procedure TMainForm.EnumeratePropertiesAndSaveToDisk;
const
  Widgets: array[0..12] of Utf8String = ('Widget', 'Label', 'Button', 'Edit', 'RadioButton', 'CheckBox',
    'GaugeEdit', 'SpinEdit', 'Placer', 'Panel', 'TabSheets', 'Window', 'SketchPad');
var
  LStrings, LPropNames: TStringList;
  LPropName: string;
  LWidget: TWidget;
  LProp: TWidgetPropertyValue;
  I: Integer;
begin
  if not SaveDialog.Execute then
    Exit;

  LStrings := TStringList.Create;
  try
    // Cycle through all possible widgets.
    for I := Low(Widgets) to High(Widgets) do
    begin
      LWidget := TWidget.Create(FManager, Widgets[I], 'TestWidget');
      try
        LStrings.Add(LWidget.ClassName);

        // Enumerate all widget's properties and events.
        LPropNames := TStringList.Create;
        try
          for LProp in LWidget.Properties do
          begin
            LPropName := string(LProp.Name);

            if LProp.PropType <> TWidgetPropertyType.None then
              LPropName := LPropName + ': ' + WidgetPropertyTypeToString(LProp.PropType)
            else if LProp.Behavior = TWidgetPropertyBehavior.Event then
              LPropName := LPropName + ' [event]';

            LPropNames.Add(LPropName);
          end;

          // Make sure the properties and events are sorted by their name.
          LPropNames.Sorted := True;

          for LPropName in LPropNames do
            LStrings.Add('  ' + LPropName);
        finally
          LPropNames.Free;
        end;

        LStrings.Add('');
      finally
        LWidget.Free;
      end;
    end;
    LStrings.SaveToFile(SaveDialog.FileName);
  finally
    LStrings.Free;
  end;
end;

procedure TMainForm.CreateObjects;
const
  Colors: array[0..7] of TIntColor = ($FF00FF00, $FFFF0000, $FF0000FF, $FFFF00FF, $FFFFFF00, $FF00FFFF,
    $FF808080, $FF4080FF);
var
  LRadius, LAngle: Single;
  LObject: TObjectModel;
  LPRNG: TRandomSequence;
  I: Integer;
begin
  LObject := FObjects.Add('Floor');
  LObject.MeshName := 'Plane';
  LObject.Transform[TModelTransform.Local] := TMatrix4f.Translate(0.0, -0.1, 0.0);
  LObject.Color := FloatColor($FF505050);
  LObject.Material := 0;

  LObject := FObjects.Add('TorusKnot');
  LObject.MeshName := 'TorusKnot';
  LObject.Transform[TModelTransform.Local] := TMatrix4f.Translate(0.0, 0.0, 0.0);
  LObject.Color := FloatColor($FF00E000);
  LObject.Material := 1;

  LPRNG := TRandomSequence.Create(2025);

  for I := 0 to 1999 do
  begin
    LRadius := 240.0 + (LPRNG.Gaussian * 80.0);
    LAngle := LPRNG.Value * Pi * 2.0;

    LObject := FObjects.Add;
    LObject.MeshName := 'Cube';
    LObject.Transform[TModelTransform.Local] := TMatrix4f.Scale(Max(0.75 + LPRNG.Gaussian * 0.5, 0.25)) *
      TMatrix4f.RotateY(LPRNG.Value * Pi * 0.5) * TMatrix4f.Translate(Saturate(Cos(LAngle) * LRadius, -360.0,
      360.0), 0.0, Saturate(Sin(LAngle) * LRadius, -360.0, 360.0));
    LObject.Color := FloatColor(Colors[LPRNG.Ranged(8)]);
    LObject.Material := 1;
  end;
end;

procedure TMainForm.CreateMeshes;
var
  LMeshBuffer: TMeshBuffer;
begin
  LMeshBuffer := TMeshBuffer.Create;
  try
    LMeshBuffer.Plane(1, 1, TVector3f.Zero, Vector3f(800.0, 0.0, 0.0), Vector3f(0.0, 0.0, 800.0),
      Vector3f(0.0, 1.0, 0.0), TPoint2f.Zero, TPoint2f.Unity, TFloatColor.White);

    FMeshes.AddFromBuffer('Plane', LMeshBuffer, nil, VertexElements);
  finally
    LMeshBuffer.Free;
  end;
end;

procedure TMainForm.CreateLightsAndMaterials;
var
  LLight: PSceneLight;
  LMaterial: PObjectMaterial;
begin
  LLight := FSceneLights.Add;
  LLight.AmbientColor := FloatColorRGB($FFFFFF);
  LLight.AlbedoColor := FloatColorRGB($FFFFFF);
  LLight.SpecularColor := FloatColorRGB($FFFFFF);
  LLight.SpecularExponent := 1.0;
  LLight.Position := LightPosition;
  LLight.ShadowCaster := 0;

  // (0) Floor
  LMaterial := FMaterials.Add;
  LMaterial.AmbientColor := FloatColorRGB(1.0, 1.0, 1.0);
  LMaterial.AlbedoColor := FloatColor(0.0, 0.0, 0.0, 0.0);
  LMaterial.SpecularColor := FloatColorRGB(0.0, 0.0, 0.0);
  LMaterial.Shadows := 0.5;
  LMaterial.Occlusion := 0.5;
  LMaterial.Technique := TTechniqueLighting.Phong;

  // (1) Object
  LMaterial := FMaterials.Add;
  LMaterial.AmbientColor := FloatColorRGB(0.25, 0.25, 0.25);
  LMaterial.AlbedoColor := FloatColor(1.0, 1.0, 1.0, 0.5);
  LMaterial.SpecularColor := FloatColorRGB(1.0, 1.0, 1.0);
  LMaterial.SpecularExponent := 4.0;
  LMaterial.Shadows := 0.5;
  LMaterial.Occlusion := 0.5;
  LMaterial.Roughness := Vector3f(0.8, 1.2, 0.4);
  LMaterial.Technique := TTechniqueLighting.Phong;
end;

procedure TMainForm.CreateRenderingControl;
begin
  FRenderingControl := TRenderingControl.Create(nil);
  FRenderingControl.Parent := Self;
  FRenderingControl.Align := alClient;
  FRenderingControl.Width := 256;
  FRenderingControl.Height := 256;
  FRenderingControl.OnPaint := RenderingControlPaint;
  FRenderingControl.OnResize := RenderingControlResize;
  FRenderingControl.OnMouseDown := RenderingControlMouseDown;
  FRenderingControl.OnMouseMove := RenderingControlMouseMove;
  FRenderingControl.OnMouseUp := RenderingControlMouseUp;
  FRenderingControl.OnMouseWheelUp := RenderingControlMouseWheelUp;
  FRenderingControl.OnMouseWheelDown := RenderingControlMouseWheelDown;
  FRenderingControl.OnKeyDown := FormKeyDown;
  FRenderingControl.OnKeyUp := FormKeyUp;
  FRenderingControl.OnKeyPress := FormKeyPress;
end;

procedure TMainForm.CreateUI;

  function CreateUtilityWidget(const AParent: TWidget; const AClassName, ABaseName,
    ACaption: Utf8String): TWidget;
  var
    LPlacer, LLabel: TWidget;
  begin
    LPlacer := TWidget.Create(FManager, 'Placer', ABaseName + 'Placer');
    LPlacer.Rect := FloatRect(0.0, 0.0, 0.0, 32.0);
    LPlacer.Parent := AParent;
    LPlacer.Alignment := TWidgetAlignment.Top;
    LPlacer.Margins := PXT.Types.Margins(4.0, 4.0, 8.0, 0.0);

    if AClassName = 'SpinEdit' then
      LPlacer['Columns'] := TValue.From<string>('-0.25 -1 -0.5 -0.75')
    else
      LPlacer['Columns'] := TValue.From<string>('-0.25 -1 -1 -0.25');

    LLabel := TWidget.Create(FManager, 'Label', ABaseName + 'Label');
    LLabel.Style := '@.Label';
    LLabel['Caption.Text'] := TValue.From<string>(ACaption);
    LLabel['Caption.Text.Alignment.Horizontal'] := TValue.From<TTextAlignment>(TTextAlignment.Ending);
    LLabel.Margins := PXT.Types.Margins(0.0, 0.0, 8.0, 0.0);
    LLabel.Parent := LPlacer;
    LLabel.ParentZone := 1;
    LLabel.Alignment := TWidgetAlignment.Client;

    Result := TWidget.Create(FManager, AClassName, ABaseName + AClassName);
    Result.Style := '@.' + AClassName;
    Result.Parent := LPlacer;
    Result.ParentZone := 2;
    Result.Alignment := TWidgetAlignment.Client;
  end;

  function CreateWindowCloseButton(const AParent: TWidget; const AButtonName: Utf8String): TWidget;
  begin
    Result := TWidget.Create(FManager, 'Button', AButtonName);
    Result.Style := '@.FlatButton';
    Result.Rect := FloatRect(0.0, 0.0, 24.0, 0.0);
    Result.Parent := AParent;
    Result.ParentZone := 1; // Button will appear on window's title.
    Result.Alignment := TWidgetAlignment.Right;
    Result.Margins := PXT.Types.Margins(4.0);
    Result['Icon'] := TValue.From<Integer>(3);
    Result['Icon.Padding'] := TValue.From<Single>(3.0);
    Result['Focusable'] := TValue.From<Boolean>(False);
  end;

var
  LViewportPlacer, LWindow, LComponent, LPlacer, LSketchPad, LTabSheets, LTabSheet: TWidget;
  LViewport: TSketchpadViewport;
  LPRNG: TRandomSequence;
  I: Integer;
begin
  LPRNG := TRandomSequence.Create(2025);

  // Please note that all UI controls have their style set using "@" symbol, which represents style read from
  // the manager. Therefore, if manager style is set to "Winter" and button has style set as "@.Button", then
  // its resulting style will be "Winter.Buttton". Using "@" symbol is easy to quickly change the style of
  // all UI widgets just by adjusting the style of the manager. The manager itself ignores its style value.

  // A note on handling HiDPI displays with fractional scaling: position and dimensions of each UI widget
  // are specified in logical units, which are related as V_pixel = V_logical * Scale. Common scale factors
  // for HiDPI displays with fractional scaling are 1.25, 1.5 and 1.75. Therefore, it is recommended to
  // specify widget position and dimensions as multiples of 4, so they when they are scaled, the resulting
  // values won't have fractions. This applies to widget rectangle, padding and margins, as well as their
  // sums. When a widget has fractions in its rectangle, it is considered not aligned to pixels and thus
  // may appear distorted (which is intended for scrolling and animations to ensure smooth transitions).

  FManager.Style := 'Winter';
//  FManager.Style := 'Night';

  // Create "placer" is basically a 2x2 grid for 3D viewports.
  LViewportPlacer := TWidget.Create(FManager, 'Placer', 'ViewportPlacer');
  LViewportPlacer.Parent := FManager;
  LViewportPlacer.Alignment := TWidgetAlignment.Client;

  // Note: when specifying columns and/or rows in 'Placer' widget, negative values represent proportions,
  // whereas positive values represent DPI-independent size units (pixels when Scale = 1).
  LViewportPlacer['Columns'] := TValue.From<string>('-1 -1');
  LViewportPlacer['Rows'] := TValue.From<string>('-1 -1');

  // Create 3D viewports.
  for I := 0 to 3 do
  begin
    LViewport := TSketchpadViewport.Create(I, FObjects.CreateView, FSceneDepthNormals, FSceneModeling,
      FSceneLights, FMaterials, FShadowCastingAtlas);

    LSketchPad := TWidget.Create(FManager, 'SketchPad', 'SketchPad' + IntToStr(I + 1), LViewport);
    LSketchPad.Parent := LViewportPlacer;
    LSketchPad.ParentZone := I;
    LSketchPad.Alignment := TWidgetAlignment.Client;
    LViewport.Widget := LSketchPad;
  end;

  // Window with parameters and controls
  LWindow := TWidget.Create(FManager, 'Window', 'Window1');
  LWindow['Style'] := TValue.From<string>('@.Window');
  LWindow['Caption.Text'] := TValue.From<string>('Parameters and control (resizeable, moveable)');
  LWindow.Parent := FManager;
  LWindow.Rect := FloatRect(100.0, 100.0, 500.0, 420.0);
  LWindow['AllowedSizing'] := TValue.From<Boolean>(True);
  LWindow['AllowedMovement'] := TValue.From<Boolean>(True);
  LWindow['Constraints.Width.Minimum'] := TValue.From<Single>(440.0);
  LWindow['Constraints.Height.Minimum'] := TValue.From<Single>(430.0);

  // Torus Knot label
  LComponent := TWidget.Create(FManager, 'Label', 'TorusKnotLabel');
  LComponent.Style := '@.Label';
  LComponent.Rect := FloatRect(0.0, 0.0, 0.0, 32.0);
  LComponent.Parent := LWindow;
  LComponent.Alignment := TWidgetAlignment.Top;
  LComponent.Margins := PXT.Types.Margins(0.0, 4.0, 0.0, 0.0);
  LComponent['Caption.Text.Alignment.Horizontal'] := TValue.From<TTextAlignment>(TTextAlignment.Middle);
  LComponent['Caption.Text'] := TValue.From<string>('Torus Knot parameters:');

  // Inside Radius Shape
  LComponent := CreateUtilityWidget(LWindow, 'GaugeEdit', 'InsideRadiusShape', 'Inside radius shape:');
  LComponent['Value'] := TValue.From<Double>(0.25);
  LComponent['Value.Decimal.Digits'] := TValue.From<Integer>(3);
  LComponent['Value.Minimum'] := TValue.From<Double>(0.05);
  LComponent['Value.Maximum'] := TValue.From<Double>(4.0);

  // Lateral Rotation
  LComponent := CreateUtilityWidget(LWindow, 'GaugeEdit', 'LateralRotation', 'Lateral rotation:');
  LComponent['Value'] := TValue.From<Double>(0.0);
  LComponent['Value.Minimum'] := TValue.From<Double>(0.0);
  LComponent['Value.Maximum'] := TValue.From<Double>(90.0);
  LComponent['Value.Decimal.Digits'] := TValue.From<Integer>(1);

  // P value
  LComponent := CreateUtilityWidget(LWindow, 'SpinEdit', 'PValue', 'P-value:');
  LComponent['Value.Format'] := TValue.From<Integer>(3); // Integer
  LComponent['Value'] := TValue.From<Double>(3.0);
  LComponent['Value.Minimum'] := TValue.From<Double>(1.0);
  LComponent['Value.Maximum'] := TValue.From<Double>(4.0);

  // Q value
  LComponent := CreateUtilityWidget(LWindow, 'SpinEdit', 'QValue', 'Q-value:');
  LComponent['Value.Format'] := TValue.From<Integer>(3); // Integer
  LComponent['Value'] := TValue.From<Double>(2.0);
  LComponent['Value.Minimum'] := TValue.From<Double>(1.0);
  LComponent['Value.Maximum'] := TValue.From<Double>(4.0);

  // Cube label
  LComponent := TWidget.Create(FManager, 'Label', 'CubeLabel');
  LComponent.Style := '@.Label';
  LComponent.Rect := FloatRect(0.0, 0.0, 0.0, 32.0);
  LComponent.Parent := LWindow;
  LComponent.Alignment := TWidgetAlignment.Top;
  LComponent.Margins := PXT.Types.Margins(0.0, 4.0, 0.0, 0.0);
  LComponent['Caption.Text.Alignment.Horizontal'] := TValue.From<TTextAlignment>(TTextAlignment.Middle);
  LComponent['Caption.Text'] := TValue.From<string>('Cube parameters:');

  // Latitude Shape
  LComponent := CreateUtilityWidget(LWindow, 'GaugeEdit', 'LatitudeShape', 'Latitude shape:');
  LComponent['Value'] := TValue.From<Double>(0.2);
  LComponent['Value.Decimal.Digits'] := TValue.From<Integer>(3);
  LComponent['Value.Minimum'] := TValue.From<Double>(0.05);
  LComponent['Value.Maximum'] := TValue.From<Double>(4.0);

  // Longitude Shape
  LComponent := CreateUtilityWidget(LWindow, 'GaugeEdit', 'LongitudeShape', 'Longitude shape:');
  LComponent['Value'] := TValue.From<Double>(0.2);
  LComponent['Value.Decimal.Digits'] := TValue.From<Integer>(3);
  LComponent['Value.Minimum'] := TValue.From<Double>(0.05);
  LComponent['Value.Maximum'] := TValue.From<Double>(4.0);

  // Show other windows button
  LPlacer := TWidget.Create(FManager, 'Placer', 'ShowOtherWindowsPlacer');
  LPlacer.Rect := FloatRect(0.0, 0.0, 0.0, 32.0);
  LPlacer.Parent := LWindow;
  LPlacer.Alignment := TWidgetAlignment.Top;
  LPlacer.Margins := PXT.Types.Margins(0.0, 16.0, 0.0, 0.0);
  LPlacer['Columns'] := TValue.From<string>('-1 200 -1');

  LComponent := TWidget.Create(FManager, 'Button', 'ShowOtherWindowsButton');
  LComponent.Style := '@.Button';
  LComponent.Parent := LPlacer;
  LComponent.ParentZone := 1;
  LComponent.Alignment := TWidgetAlignment.Client;
  LComponent['Caption.Text'] := TValue.From<string>('Show other windows');

  // Enumerate properties button
  LComponent := TWidget.Create(FManager, 'Button', 'EnumeratePropertiesButton');
  LComponent.Style := '@.Button';
  LComponent.Rect := FloatRect(0.0, 0.0, 0.0, 32.0);
  LComponent.Parent := LWindow;
  LComponent.Alignment := TWidgetAlignment.Top;
  LComponent.Margins := PXT.Types.Margins(20.0, 8.0, 20.0, 0.0);
  LComponent['Caption.Text'] := TValue.From<string>('Enumerate all properties of UI controls and save to disk');

  // Another test window
  LWindow := TWidget.Create(FManager, 'Window', 'Window2');
  LWindow['Style'] := TValue.From<string>('@.Window');
  LWindow['Caption.Text'] := TValue.From<string>('Window with tabs (resizeable, moveable)');
  LWindow.Parent := FManager;
  LWindow.Rect := FloatRect(700.0, 50.0, 450.0, 300.0);
  LWindow['AllowedSizing'] := TValue.From<Boolean>(True);
  LWindow['AllowedMovement'] := TValue.From<Boolean>(True);
  LWindow['Constraints.Width.Minimum'] := TValue.From<Single>(400.0);
  LWindow['Constraints.Height.Minimum'] := TValue.From<Single>(200.0);
  LWindow['Visible'] := TValue.From<Boolean>(False);

  // Tab Sheets
  LTabSheets := TWidget.Create(FManager, 'TabSheets', 'SomeTabSheets');
  LTabSheets.Style := '@.TabSheets';
  LTabSheets.Parent := LWindow;
  LTabSheets.Alignment := TWidgetAlignment.Client;

  // Create two very basic styles for a custom "ExampleTabSheet" that is used below.
  FManager['Winter.ExampleTabSheet.Fill.Color'] := TValue.From<TIntColor>($FFE0E0E0);
  FManager['Night.ExampleTabSheet.Fill.Color'] := TValue.From<TIntColor>($FF505050);

  // ...individual sheets
  for I := 0 to 9 do
  begin
    LTabSheet := TWidget.Create(FManager, 'Panel', 'TabSheet' + IntToStr(I + 1));
    LTabSheet.Style := '@.ExampleTabSheet';
    LTabSheet.Parent := LTabSheets;
    LTabSheet.Alignment := TWidgetAlignment.Client;
    LTabSheet['Tab.Caption'] := TValue.From<string>('Sheet' + IntToStr(I + 1));
    LTabSheet.Visible := I = 0;

    // Place some random button on the tab sheet.
    LComponent := TWidget.Create(FManager, 'Button', 'Sheet' + IntToStr(I + 1) + 'Button');
    LComponent.Style := '@.Button';
    LComponent.Rect := FloatRect(16.0 + Trunc((LPRNG.Value * 200.0) / 4) * 4.0, 16.0 +
      Trunc((LPRNG.Value * 200.0) / 4) * 4.0, 160.0, 32.0);
    LComponent.Parent := LTabSheet;
    LComponent['Caption.Text'] := TValue.From<string>('Some button ' + IntToStr(I + 1));
  end;

  // Create button to close the window
  CreateWindowCloseButton(LWindow, 'CloseWindowButton2');

  // Some move windows for testing.
  for I := 0 to 2 do
  begin
    LWindow := TWidget.Create(FManager, 'Window', 'Window' + IntToStr(3 + I));
    LWindow['Style'] := TValue.From<string>('@.Window');
    LWindow['Caption.Text'] := TValue.From<string>('Another window (moveable)');
    LWindow.Parent := FManager;
    LWindow.Rect := FloatRect(250.0 + I * 100.0, 250.0 + I * 100.0, 400.0, 300.0);
    LWindow['AllowedMovement'] := TValue.From<Boolean>(True);
    LWindow['Visible'] := TValue.From<Boolean>(False);

    CreateWindowCloseButton(LWindow, 'CloseWindowButton' + IntToStr(3 + I));
  end;
end;

function InterfaceExternalEvent(Widget: TUntypedObjectHandle; Event: PAnsiChar;
  User: Pointer): TLibraryBool; cdecl;
var
  LWidget: TWidget;
  LWidgetName, LEventName: string;
  LViewport : TSketchpadViewport;
begin
  LWidget := TWidget(Widget);
  LWidgetName := string(LWidget.Name);
  LEventName := string(Utf8String(Event));

  LViewport := LWidget.Payload;
  if LViewport <> nil then
    Exit(LViewport.HandleEvent(LWidget, Event));

  if not MainForm.FConstructingUI then
  begin
    if ((LWidgetName = 'PValueSpinEdit') or (LWidgetName = 'QValueSpinEdit') or
      (LWidgetName = 'InsideRadiusShapeGaugeEdit') or (LWidgetName = 'LateralRotationGaugeEdit')) and
      (LEventName = 'Value.Changed') then
    begin
      MainForm.FRecreateTorusKnot := True;
      MainForm.FRenderingControl.Invalidate;
    end
    else if ((LWidgetName = 'LongitudeShapeGaugeEdit') or (LWidgetName = 'LatitudeShapeGaugeEdit')) and
      (LEventName = 'Value.Changed') then
    begin
      MainForm.FRecreateCube := True;
      MainForm.FRenderingControl.Invalidate;
    end
    else if (LWidgetName = 'EnumeratePropertiesButton') and (LEventName = 'Action') then
      MainForm.EnumeratePropertiesAndSaveToDisk
    else if (LWidgetName = 'CloseWindowButton2') and (LEventName = 'Action') then
      LWidget.Manager.Widget['Window2'].Value['Visible'] := TValue.From<Boolean>(False)
    else if (LWidgetName = 'CloseWindowButton3') and (LEventName = 'Action') then
      LWidget.Manager.Widget['Window3'].Value['Visible'] := TValue.From<Boolean>(False)
    else if (LWidgetName = 'CloseWindowButton4') and (LEventName = 'Action') then
      LWidget.Manager.Widget['Window4'].Value['Visible'] := TValue.From<Boolean>(False)
    else if (LWidgetName = 'CloseWindowButton5') and (LEventName = 'Action') then
      LWidget.Manager.Widget['Window5'].Value['Visible'] := TValue.From<Boolean>(False)
    else if (LWidgetName = 'ShowOtherWindowsButton') and (LEventName = 'Action') then
    begin
      LWidget.Manager.Widget['Window2'].Value['Visible'] := TValue.From<Boolean>(True);
      LWidget.Manager.Widget['Window3'].Value['Visible'] := TValue.From<Boolean>(True);
      LWidget.Manager.Widget['Window4'].Value['Visible'] := TValue.From<Boolean>(True);
      LWidget.Manager.Widget['Window5'].Value['Visible'] := TValue.From<Boolean>(True);
      LWidget.Manager.Widget['Window5'].Focused := True;
    end;
  end;
  Result := True;
end;

end.