// SPDX-License-Identifier: LGPL-3.0-linking-exception
unit BGRAArrow;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, BGRABitmapTypes, BGRAGraphics;

type
  { TBGRAArrow }

  TBGRAArrow = class(TBGRACustomArrow)
  private
    FLineCap: TPenEndCap;
    FWidth : single;
    FStart : ArrayOfTPointF;
    FStartComputed: boolean;
    FStartStyle: TBGRAArrowStyle;
    FStartSizeFactor: TPointF;
    FStartTipStyle: TPenJoinStyle;
    FStartOffsetX: single;
    FStartRepeatCount: integer;
    FStartRelativePenWidth: single;
    FStartTriangleBackOffset: single;
    FEnd : ArrayOfTPointF;
    FEndComputed: boolean;
    FEndStyle: TBGRAArrowStyle;
    FEndSizeFactor: TPointF;
    FEndTipStyle: TPenJoinStyle;
    FEndOffsetX: single;
    FEndRepeatCount: integer;
    FEndRelativePenWidth: single;
    FEndTriangleBackOffset: single;
    function ComputeAnyAt(const AData: ArrayOfTPointF; const APosition, ADirection: TPointF): ArrayOfTPointF;
    function ComputeData(AStyle: TBGRAArrowStyle; const ASizeFactor: TPointF;
        ATipStyle: TPenJoinStyle; ALineCap: TPenEndCap; const AWidth: single; AOffsetX: single;
        ARepeatCount: integer; ARelativePenWidth: single; ATriangleBackOffset: single): ArrayOfTPointF;
    procedure SetWidth(AValue: single);
  protected
    function GetEndRepeatCount: integer; override;
    function GetEndSizeFactor: TPointF; override;
    function GetIsEndDefined: boolean; override;
    function GetIsStartDefined: boolean; override;
    function GetEndOffsetX: single; override;
    function GetStartOffsetX: single; override;
    function GetStartRepeatCount: integer; override;
    function GetStartSizeFactor: TPointF; override;
    procedure SetEndOffsetX(AValue: single); override;
    procedure SetEndRepeatCount(AValue: integer); override;
    procedure SetEndSizeFactor(AValue: TPointF); override;
    procedure SetStartOffsetX(AValue: single); override;
    procedure SetStartRepeatCount(AValue: integer); override;
    procedure SetStartSizeFactor(AValue: TPointF); override;
    function GetLineCap: TPenEndCap; override;
    procedure SetLineCap(AValue: TPenEndCap); override;
    procedure SetStart(AStyle: TBGRAArrowStyle; ATipStyle: TPenJoinStyle = pjsMiter; ARelativePenWidth: single = 1; ATriangleBackOffset: single = 0);
    procedure SetEnd(AStyle: TBGRAArrowStyle; ATipStyle: TPenJoinStyle = pjsMiter; ARelativePenWidth: single = 1; ATriangleBackOffset: single = 0);
  public
    constructor Create;
    procedure StartAsNone; override;
    procedure StartAsClassic(AFlipped: boolean = false; ACut: boolean = false; ARelativePenWidth: single = 1); override;
    procedure StartAsTriangle(ABackOffset: single = 0; ARounded: boolean = false; AHollow: boolean = false; AHollowPenWidth: single = 0.5); override;
    procedure StartAsTail; override;
    procedure EndAsNone; override;
    procedure EndAsClassic(AFlipped: boolean = false; ACut: boolean = false; ARelativePenWidth: single = 1); override;
    procedure EndAsTriangle(ABackOffset: single = 0; ARounded: boolean = false; AHollow: boolean = false; AHollowPenWidth: single = 0.5); override;
    procedure EndAsTail; override;
    function ComputeStartAt(const APosition: TPointF; const ADirection: TPointF; const AWidth: single; const ACurrentPos: single): ArrayOfTPointF; override;
    function ComputeEndAt(const APosition: TPointF; const ADirection: TPointF; const AWidth: single; const ACurrentPos: single): ArrayOfTPointF; override;

  end;

implementation

uses BGRAClasses, BGRATransform, BGRAPen, BGRAPath;

{ TBGRAArrow }

function TBGRAArrow.ComputeAnyAt(const AData: ArrayOfTPointF; const APosition,
  ADirection: TPointF): ArrayOfTPointF;
var m: TAffineMatrix;
  i: integer;
begin
  if (AData = nil) or isEmptyPointF(APosition) or isEmptyPointF(ADirection) or ((ADirection.x = 0) and (ADirection.y = 0)) then
  begin
    result := nil;
    exit;
  end;
  m := AffineMatrixTranslation(APosition.x,APosition.y)*AffineMatrixLinear(ADirection, PointF(-ADirection.y, ADirection.x));
  setlength(result, length(AData));
  for i := 0 to high(result) do
    if not isEmptyPointF(AData[i]) then
      result[i] := m*AData[i]
    else
      result[i] := EmptyPointF;
end;

function TBGRAArrow.ComputeData(AStyle: TBGRAArrowStyle;
  const ASizeFactor: TPointF; ATipStyle: TPenJoinStyle; ALineCap: TPenEndCap;
  const AWidth: single; AOffsetX: single; ARepeatCount: integer;
  ARelativePenWidth: single; ATriangleBackOffset: single): ArrayOfTPointF;
var sizeFactorX,sizeFactorY,ofsX: single;
  prefix,suffix,middle: ArrayOfTPointF;
  arc: TArcDef;
  i,j,n : integer;
  withCut: ArrayOfTPointF;
  subResult: ArrayOfTPointF;
  w: single;
  backOfs: single;
  tailSizeX, tailAdditionalWidth: single;
begin
  sizeFactorX := abs(ASizeFactor.X)*AWidth;
  sizeFactorY := abs(ASizeFactor.Y)*AWidth;
  if (sizeFactorX = 0) or (sizeFactorY = 0) then
  begin
    result := nil;
    exit;
  end;
  w := AWidth*ARelativePenWidth;
  if AStyle in [asTail,asTailRepeat] then
  begin
    tailSizeX := sizeFactorX/SizeFactorY*AWidth*0.5;
    if AStyle = asTailRepeat then
    begin
      tailAdditionalWidth:= AWidth-tailSizeX;
      if tailAdditionalWidth < 0 then tailAdditionalWidth := 0;
    end else
      tailAdditionalWidth:=0;
  end
  else
  begin
    tailSizeX := 0;
    tailAdditionalWidth:=0;
  end;
  case AStyle of
  asTriangle,asHollowTriangle: begin
    backOfs := ATriangleBackOffset*sizeFactorX;
    if AStyle = asHollowTriangle then
    begin
      result := ComputeWidePolylinePoints(PointsF([PointF(0.5*w,-AWidth*0.5),
             PointF(0.5*w-backOfs,-sizeFactorY+w*0.5),
             PointF(sizeFactorX-w*0.5,0),
             PointF(w*0.5-backOfs,sizeFactorY-w*0.5),
             PointF(0.5*w,AWidth*0.5)]),
             w,BGRABlack,ALineCap,ATipStyle,nil,[plCycle]);
    end else
    begin
      prefix := PointsF([PointF(0,-AWidth*0.5),PointF(-backOfs,-sizeFactorY)]);
      suffix := PointsF([PointF(-backOfs,sizeFactorY),PointF(0,AWidth*0.5)]);
      if (ATipStyle in[pjsRound,pjsBevel]) then
      begin
        arc := Html5ArcTo(prefix[1],PointF(sizeFactorX,0),suffix[0],AWidth*0.5);
        if ATipStyle = pjsRound then
          middle := ComputeArc(arc)
        else
          middle := PointsF([ArcStartPoint(arc),ArcEndPoint(arc)]);
      end
      else middle := PointsF([PointF(sizeFactorX,0)]);
      result := ConcatPointsF([prefix,middle,suffix]);
    end;
  end;
  asNormal,asCut:
    begin
      if AStyle = asCut then ALineCap:= pecSquare;
      result := ComputeWidePolylinePoints([PointF(-sizeFactorX,-sizeFactorY),
        PointF(0,0),PointF(-sizeFactorX,+sizeFactorY)],w,BGRABlack,ALineCap,ATipStyle,nil,[]);
    end;
  asFlipped,asFlippedCut:
    begin
      if AStyle = asFlippedCut then ALineCap:= pecSquare;
      result := ComputeWidePolylinePoints([PointF(+sizeFactorX,-sizeFactorY),
       PointF(0,0),PointF(+sizeFactorX,+sizeFactorY)],w,BGRABlack,ALineCap,ATipStyle,nil,[]);
    end;
  asTail: result := PointsF([PointF(0,-0.5*AWidth),PointF(tailSizeX,-0.5*AWidth),PointF(0,0),PointF(tailSizeX,0.5*AWidth),PointF(0,0.5*AWidth)]);
  asTailRepeat: result := PointsF([PointF(0,-0.5*AWidth),PointF(tailSizeX+tailAdditionalWidth,-0.5*AWidth),PointF(tailAdditionalWidth,0),PointF(tailSizeX+tailAdditionalWidth,0.5*AWidth),PointF(0,0.5*AWidth),PointF(-tailSizeX,0)]);
  else
    result := nil;
  end;
  if (AStyle in [asCut,asFlippedCut,asHollowTriangle]) then
  begin
    n := 0;
    setlength(withCut,length(result)*2);
    for i := 0 to high(result) do
      if isEmptyPointF(result[i]) then
      begin
        if (n > 0) and not isEmptyPointF(withCut[n-1]) then
        begin
          withCut[n] := EmptyPointF;
          inc(n);
        end;
      end else
      if abs(result[i].y)<=sizeFactorY then
      begin
        withCut[n] := result[i];
        inc(n);
      end else
      if result[i].y>sizeFactorY then
      begin
        j := (i+length(result)-1) mod length(result);
        if result[j].y<=sizeFactorY then
        begin
          withCut[n].x := result[j].x + (result[i].x-result[j].x)/(result[i].y-result[j].y)*(sizeFactorY-result[j].y);
          withCut[n].y := sizeFactorY;
          inc(n);
        end;
        j := (i+1) mod length(result);
        if result[j].y<=sizeFactorY then
        begin
          withCut[n].x := result[j].x + (result[i].x-result[j].x)/(result[i].y-result[j].y)*(sizeFactorY-result[j].y);
          withCut[n].y := sizeFactorY;
          inc(n);
        end;
      end else
      if result[i].y<-sizeFactorY then
      begin
        j := (i+length(result)-1) mod length(result);
        if result[j].y>=-sizeFactorY then
        begin
          withCut[n].x := result[j].x + (result[i].x-result[j].x)/(result[i].y-result[j].y)*(-sizeFactorY-result[j].y);
          withCut[n].y := -sizeFactorY;
          inc(n);
        end;
        j := (i+1) mod length(result);
        if result[j].y>=-sizeFactorY then
        begin
          withCut[n].x := result[j].x + (result[i].x-result[j].x)/(result[i].y-result[j].y)*(-sizeFactorY-result[j].y);
          withCut[n].y := -sizeFactorY;
          inc(n);
        end;
      end;
      if (n > 0) and isEmptyPointF(withCut[n-1]) then dec(n);
      setlength(withCut,n);
      result := withCut;
  end;
  if AOffsetX <> 0 then
  begin
    ofsX := AOffsetX*AWidth;
    for i := 0 to high(result) do
      if not isEmptyPointF(result[i]) then
        IncF(result[i].x, ofsX);
  end;
  if ARepeatCount > 1 then
  begin
    if ARepeatCount > 10 then ARepeatCount:= 10;
    if AStyle in[asTriangle,asHollowTriangle] then IncF(AOffsetX, sizeFactorX/AWidth)
    else if AStyle in[asTail,asTailRepeat] then IncF(AOffsetX, (tailSizeX+tailAdditionalWidth)/AWidth+1)
    else IncF(AOffsetX, 2*ARelativePenWidth);
    if AStyle = asTail then AStyle := asTailRepeat;
    subResult := ComputeData(AStyle,ASizeFactor,ATipStyle,ALineCap,AWidth,AOffsetX,ARepeatCount-1,ARelativePenWidth,ATriangleBackOffset);
    result := ConcatPointsF([result,PointsF([EmptyPointF]),subResult]);
  end;
end;

function TBGRAArrow.GetIsEndDefined: boolean;
begin
  result := FEndStyle <> asNone;
end;

function TBGRAArrow.GetIsStartDefined: boolean;
begin
  result := FStartStyle <> asNone;
end;

function TBGRAArrow.GetEndOffsetX: single;
begin
  result := FEndOffsetX;
end;

function TBGRAArrow.GetStartOffsetX: single;
begin
  result := FStartOffsetX;
end;

function TBGRAArrow.GetStartRepeatCount: integer;
begin
  result := FStartRepeatCount;
end;

function TBGRAArrow.GetStartSizeFactor: TPointF;
begin
  result := FStartSizeFactor;
end;

procedure TBGRAArrow.SetEndOffsetX(AValue: single);
begin
  if FEndOffsetX=AValue then Exit;
  FEndOffsetX:=AValue;
  FEndComputed:= false;
  FEnd := nil;
end;

function TBGRAArrow.GetLineCap: TPenEndCap;
begin
  result := FLineCap;
end;

procedure TBGRAArrow.SetEndRepeatCount(AValue: integer);
begin
  if FEndRepeatCount=AValue then Exit;
  FEndRepeatCount:=AValue;
  FEndComputed:= false;
  FEnd := nil;
end;

procedure TBGRAArrow.SetEndSizeFactor(AValue: TPointF);
begin
  if FEndSizeFactor=AValue then Exit;
  FEndSizeFactor:=AValue;
  FEndComputed:= false;
  FEnd := nil;
end;

procedure TBGRAArrow.SetLineCap(AValue: TPenEndCap);
begin
  if FLineCap=AValue then Exit;
  FLineCap:=AValue;
  FStartComputed:= false;
  FEndComputed:= false;
  FStart:= nil;
  FEnd := nil;
end;

procedure TBGRAArrow.SetStartOffsetX(AValue: single);
begin
  if FStartOffsetX=AValue then Exit;
  FStartOffsetX:=AValue;
  FStartComputed:= false;
  FStart := nil;
end;

procedure TBGRAArrow.SetStartRepeatCount(AValue: integer);
begin
  if FStartRepeatCount=AValue then Exit;
  FStartRepeatCount:=AValue;
  FStartComputed:= false;
  FStart := nil;
end;

procedure TBGRAArrow.SetStartSizeFactor(AValue: TPointF);
begin
  if FStartSizeFactor=AValue then Exit;
  FStartSizeFactor:=AValue;
  FStartComputed:= false;
  FStart := nil;
end;

procedure TBGRAArrow.SetWidth(AValue: single);
begin
  if FWidth=AValue then Exit;
  FWidth:=AValue;
  FStartComputed := false;
  FEndComputed:= false;
end;

function TBGRAArrow.GetEndRepeatCount: integer;
begin
  Result:= FEndRepeatCount;
end;

function TBGRAArrow.GetEndSizeFactor: TPointF;
begin
  Result:= FEndSizeFactor;
end;

constructor TBGRAArrow.Create;
begin
  FWidth := 1;
  FStartSizeFactor := PointF(2,2);
  FEndSizeFactor := PointF(2,2);
end;

procedure TBGRAArrow.StartAsNone;
begin
  SetStart(asNone);
end;

procedure TBGRAArrow.StartAsClassic(AFlipped: boolean; ACut: boolean;
  ARelativePenWidth: single);
var join: TPenJoinStyle;
begin
  if (LineCap = pecRound) and not ACut then join := pjsRound else join := pjsMiter;
  if ACut then
  begin
    if AFlipped then
      SetStart(asFlippedCut,join,ARelativePenWidth)
    else
      SetStart(asCut,join,ARelativePenWidth)
  end
  else
  begin
    if AFlipped then
      SetStart(asFlipped,join,ARelativePenWidth)
    else
      SetStart(asNormal,join,ARelativePenWidth)
  end;
end;

procedure TBGRAArrow.StartAsTriangle(ABackOffset: single; ARounded: boolean;
  AHollow: boolean; AHollowPenWidth: single);
var join: TPenJoinStyle;
begin
  if ARounded then join := pjsRound else join := pjsMiter;
  if AHollow then
    SetStart(asHollowTriangle, join,AHollowPenWidth, ABackOffset)
  else
    SetStart(asTriangle, join,1,ABackOffset);
end;

procedure TBGRAArrow.StartAsTail;
begin
  SetStart(asTail);
end;

procedure TBGRAArrow.EndAsNone;
begin
  SetEnd(asNone);
end;

procedure TBGRAArrow.EndAsClassic(AFlipped: boolean; ACut: boolean;
  ARelativePenWidth: single);
var join: TPenJoinStyle;
begin
  if (LineCap = pecRound) and not ACut then join := pjsRound else join := pjsMiter;
  if ACut then
  begin
    if AFlipped then
      SetEnd(asFlippedCut,join,ARelativePenWidth)
    else
      SetEnd(asCut,join,ARelativePenWidth)
  end
  else
  begin
    if AFlipped then
      SetEnd(asFlipped,join,ARelativePenWidth)
    else
      SetEnd(asNormal,join,ARelativePenWidth)
  end;
end;

procedure TBGRAArrow.EndAsTriangle(ABackOffset: single; ARounded: boolean;
  AHollow: boolean; AHollowPenWidth: single);
var join: TPenJoinStyle;
begin
  if ARounded then join := pjsRound else join := pjsMiter;
  if AHollow then
    SetEnd(asHollowTriangle, join,AHollowPenWidth, ABackOffset)
  else
    SetEnd(asTriangle, join,1, ABackOffset);
end;

procedure TBGRAArrow.EndAsTail;
begin
  SetEnd(asTail);
end;

procedure TBGRAArrow.SetStart(AStyle: TBGRAArrowStyle;
  ATipStyle: TPenJoinStyle; ARelativePenWidth: single; ATriangleBackOffset: single);
begin
  FStartStyle := AStyle;
  FStartTipStyle := ATipStyle;
  FStartComputed := false;
  FStartRelativePenWidth:= ARelativePenWidth;
  FStartTriangleBackOffset := ATriangleBackOffset;
  FStart := nil;
end;

procedure TBGRAArrow.SetEnd(AStyle: TBGRAArrowStyle; ATipStyle: TPenJoinStyle;
  ARelativePenWidth: single; ATriangleBackOffset: single);
begin
  FEndStyle := AStyle;
  FEndTipStyle := ATipStyle;
  FEndComputed := false;
  FEndRelativePenWidth:= ARelativePenWidth;
  FEndTriangleBackOffset := ATriangleBackOffset;
  FEnd := nil;
end;

function TBGRAArrow.ComputeStartAt(const APosition: TPointF;
  const ADirection: TPointF; const AWidth: single; const ACurrentPos: single
  ): ArrayOfTPointF;
begin
  if not IsStartDefined then
  begin
    result := nil;
    exit;
  end;
  if AWidth <> FWidth then
  begin
    FWidth := AWidth;
    FStartComputed:= false;
  end;
  if not FStartComputed then
  begin
    FStart := ComputeData(FStartStyle,FStartSizeFactor,FStartTipStyle,FLineCap,FWidth,
    FStartOffsetX-ACurrentPos,FStartRepeatCount,FStartRelativePenWidth,FStartTriangleBackOffset);
    FStartComputed:= true;
  end;
  result := ComputeAnyAt(FStart,APosition,ADirection);
end;

function TBGRAArrow.ComputeEndAt(const APosition: TPointF;
  const ADirection: TPointF; const AWidth: single; const ACurrentPos: single
  ): ArrayOfTPointF;
begin
  if not IsEndDefined then
  begin
    result := nil;
    exit;
  end;
  if AWidth <> FWidth then
  begin
    FWidth := AWidth;
    FEndComputed:= false;
  end;
  if not FEndComputed then
  begin
    FEnd := ComputeData(FEndStyle,FEndSizeFactor,FEndTipStyle,FLineCap,FWidth,
      FEndOffsetX-ACurrentPos,FEndRepeatCount,FEndRelativePenWidth,FEndTriangleBackOffset);
    FEndComputed:= true;
  end;
  result := ComputeAnyAt(FEnd,APosition,ADirection);
end;

end.

