// SPDX-License-Identifier: GPL-3.0-only unit UToolSelect; {$mode objfpc}{$H+} interface uses Classes, SysUtils, Graphics, BGRABitmapTypes, BGRABitmap, UTool, UToolBasic, UToolVectorial, UToolPolygon, ULayerAction, LCVectorOriginal; type { TVectorialSelectTool } TVectorialSelectTool = class(TVectorialTool) protected function GetIsSelectingTool: boolean; override; procedure AssignShapeStyle({%H-}AMatrix: TAffineMatrix; {%H-}AAlwaysFit: boolean); override; function RoundCoordinate(constref ptF: TPointF): TPointF; override; function UpdateShape(toolDest: TBGRABitmap): TRect; override; procedure ShapeChange({%H-}ASender: TObject; ABounds: TRectF; ADiff: TVectorShapeDiff); override; procedure QuickDefineEnd; override; function BigImage: boolean; public function GetContextualToolbars: TContextualToolbars; override; end; { TToolSelectRect } TToolSelectRect = class(TVectorialSelectTool) protected function ShapeClass: TVectorShapeAny; override; public function Render(VirtualScreen: TBGRABitmap; {%H-}VirtualScreenWidth, {%H-}VirtualScreenHeight: integer; BitmapToVirtualScreen: TBitmapToVirtualScreenFunction):TRect; override; function GetContextualToolbars: TContextualToolbars; override; end; { TToolSelectEllipse } TToolSelectEllipse = class(TVectorialSelectTool) protected function ShapeClass: TVectorShapeAny; override; function GetGridMatrix: TAffineMatrix; override; public function Render(VirtualScreen: TBGRABitmap; {%H-}VirtualScreenWidth, {%H-}VirtualScreenHeight: integer; BitmapToVirtualScreen: TBitmapToVirtualScreenFunction):TRect; override; function GetContextualToolbars: TContextualToolbars; override; end; { TToolSelectPoly } TToolSelectPoly = class(TToolPolygon) protected procedure AssignShapeStyle(AMatrix: TAffineMatrix; AAlwaysFit: boolean); override; function GetIsSelectingTool: boolean; override; public function GetContextualToolbars: TContextualToolbars; override; end; { TToolSelectSpline } TToolSelectSpline = class(TToolSpline) protected procedure AssignShapeStyle(AMatrix: TAffineMatrix; AAlwaysFit: boolean); override; function GetIsSelectingTool: boolean; override; public function GetContextualToolbars: TContextualToolbars; override; end; { TToolMagicWand } TToolMagicWand = class(TGenericTool) protected function GetIsSelectingTool: boolean; override; function DoToolDown(toolDest: TBGRABitmap; pt: TPoint; {%H-}ptF: TPointF; rightBtn: boolean): TRect; override; public function GetContextualToolbars: TContextualToolbars; override; end; { TToolSelectionPen } TToolSelectionPen = class(TToolPen) protected function GetIsSelectingTool: boolean; override; function GetUniversalBrush(ARightButton: boolean): TUniversalBrush; override; public function GetContextualToolbars: TContextualToolbars; override; end; { TTransformSelectionTool } TTransformSelectionTool = class(TGenericTool) protected function GetIsSelectingTool: boolean; override; function GetAction: TLayerAction; override; function FixSelectionTransform: boolean; override; function DoGetToolDrawingLayer: TBGRABitmap; override; end; { TToolMoveSelection } TToolMoveSelection = class(TTransformSelectionTool) protected handMoving, notMovedAtAll: boolean; handOriginF: TPointF; selectionTransformBefore: TAffineMatrix; function DoToolDown({%H-}toolDest: TBGRABitmap; {%H-}pt: TPoint; ptF: TPointF; {%H-}rightBtn: boolean): TRect; override; function DoToolMove({%H-}toolDest: TBGRABitmap; {%H-}pt: TPoint; ptF: TPointF): TRect; override; public constructor Create(AManager: TToolManager); override; function ToolUp: TRect; override; destructor Destroy; override; end; { TToolRotateSelection } TToolRotateSelection = class(TTransformSelectionTool) protected class var HintShown: boolean; FHandRotating, FHandTranslating: boolean; FHandOrigin: TPointF; FSnapMode: boolean; FUnsnappedAngle: single; FOriginalTransform: TAffineMatrix; FCurrentAngle: single; FCurrentCenter: TPointF; FOffsetBeforeMove, FFinalOffset: TPointF; function DoToolDown({%H-}toolDest: TBGRABitmap; {%H-}pt: TPoint; ptF: TPointF; rightBtn: boolean): TRect; override; function DoToolMove({%H-}toolDest: TBGRABitmap; {%H-}pt: TPoint; ptF: TPointF): TRect; override; function DoToolKeyDown(var key: Word): TRect; override; function DoToolKeyUp(var key: Word): TRect; override; function GetStatusText: string; override; procedure UpdateTransform; public class procedure ForgetHintShown; constructor Create(AManager: TToolManager); override; function ToolUp: TRect; override; function Render(VirtualScreen: TBGRABitmap; {%H-}VirtualScreenWidth, {%H-}VirtualScreenHeight: integer; BitmapToVirtualScreen: TBitmapToVirtualScreenFunction):TRect; override; destructor Destroy; override; end; implementation uses types, ugraph, LCLType, LazPaintType, Math, BGRATransform, BGRAPath, BGRAPen, LCVectorRectShapes, Controls, BGRAGrayscaleMask; procedure AssignSelectShapeStyle(AShape: TVectorShape; ASwapColor: boolean); var f: TVectorShapeFields; begin f:= AShape.MultiFields; if vsfPenFill in f then AShape.PenFill.Clear; if vsfPenStyle in f Then AShape.PenStyle := ClearPenStyle; if vsfBackFill in f then begin if ASwapColor then AShape.BackFill.SetSolid(BGRABlack) else AShape.BackFill.SetSolid(BGRAWhite); end; end; { TToolSelectSpline } procedure TToolSelectSpline.AssignShapeStyle(AMatrix: TAffineMatrix; AAlwaysFit: boolean); begin FShape.BeginUpdate; inherited AssignShapeStyle(AMatrix, AAlwaysFit); AssignSelectShapeStyle(FShape, FSwapColor); FShape.EndUpdate; end; function TToolSelectSpline.GetIsSelectingTool: boolean; begin Result:= true; end; function TToolSelectSpline.GetContextualToolbars: TContextualToolbars; begin Result:= [ctSplineStyle, ctCloseShape]; end; { TToolSelectPoly } procedure TToolSelectPoly.AssignShapeStyle(AMatrix: TAffineMatrix; AAlwaysFit: boolean); begin FShape.BeginUpdate; inherited AssignShapeStyle(AMatrix, AAlwaysFit); AssignSelectShapeStyle(FShape, FSwapColor); FShape.EndUpdate; end; function TToolSelectPoly.GetIsSelectingTool: boolean; begin Result:= true; end; function TToolSelectPoly.GetContextualToolbars: TContextualToolbars; begin Result:= []; end; { TVectorialSelectTool } function TVectorialSelectTool.GetIsSelectingTool: boolean; begin Result:= true; end; procedure TVectorialSelectTool.AssignShapeStyle(AMatrix: TAffineMatrix; AAlwaysFit: boolean); begin AssignSelectShapeStyle(FShape, FSwapColor); if FShape is TCustomRectShape then begin if Manager.ShapeRatio = 0 then TCustomRectShape(FShape).FixedRatio:= EmptySingle else TCustomRectShape(FShape).FixedRatio:= Manager.ShapeRatio; end; end; function TVectorialSelectTool.RoundCoordinate(constref ptF: TPointF): TPointF; begin Result:= PointF(floor(ptF.x)+0.5,floor(ptF.y)+0.5); end; function TVectorialSelectTool.UpdateShape(toolDest: TBGRABitmap): TRect; begin if BigImage and FQuickDefine then result := OnlyRenderChange else Result:= inherited UpdateShape(toolDest); end; procedure TVectorialSelectTool.ShapeChange(ASender: TObject; ABounds: TRectF; ADiff: TVectorShapeDiff); begin if BigImage and FQuickDefine then begin ADiff.Free; exit; end; inherited ShapeChange(ASender, ABounds, ADiff); end; procedure TVectorialSelectTool.QuickDefineEnd; var toolDest: TBGRABitmap; r: TRect; begin toolDest := GetToolDrawingLayer; r := UpdateShape(toolDest); Action.NotifyChange(toolDest, r); end; function TVectorialSelectTool.BigImage: boolean; begin result := Manager.Image.Width*Manager.Image.Height > 480000; end; function TVectorialSelectTool.GetContextualToolbars: TContextualToolbars; begin Result:= []; end; { TToolSelectRect } function TToolSelectRect.ShapeClass: TVectorShapeAny; begin result := TRectShape; end; function TToolSelectRect.Render(VirtualScreen: TBGRABitmap; VirtualScreenWidth, VirtualScreenHeight: integer; BitmapToVirtualScreen: TBitmapToVirtualScreenFunction): TRect; var ab: TAffineBox; ptsF: ArrayOfTPointF; i: Integer; begin Result:= inherited Render(VirtualScreen, VirtualScreenWidth, VirtualScreenHeight, BitmapToVirtualScreen); if BigImage and FQuickDefine then begin ab := TCustomRectShape(FShape).GetAffineBox( AffineMatrixTranslation(0.5,0.5)*FEditor.Matrix*AffineMatrixTranslation(-0.5,-0.5), false); if Assigned(VirtualScreen) then begin ptsF := ab.AsPolygon; result := RectUnion(result, NiceFrame(VirtualScreen, Manager.CanvasScale, ptsF, BGRAWhite,BGRABlack)); end else result := RectUnion(result, NiceFrameBounds(Manager.CanvasScale, ptsF)); end; end; function TToolSelectRect.GetContextualToolbars: TContextualToolbars; begin Result:= [ctRatio]; end; { TToolSelectEllipse } function TToolSelectEllipse.ShapeClass: TVectorShapeAny; begin result := TEllipseShape; end; function TToolSelectEllipse.GetGridMatrix: TAffineMatrix; begin result := AffineMatrixScale(0.5,0.5); end; function TToolSelectEllipse.Render(VirtualScreen: TBGRABitmap; VirtualScreenWidth, VirtualScreenHeight: integer; BitmapToVirtualScreen: TBitmapToVirtualScreenFunction): TRect; var ptsF: ArrayOfTPointF; i: Integer; begin Result:= inherited Render(VirtualScreen, VirtualScreenWidth, VirtualScreenHeight, BitmapToVirtualScreen); if BigImage and FQuickDefine then begin if Assigned(VirtualScreen) then begin with TCustomRectShape(FShape) do ptsF := BGRAPath.ComputeEllipse(FEditor.Matrix*Origin, FEditor.Matrix*XAxis,FEditor.Matrix*YAxis); for i := 0 to high(ptsF) do ptsF[i] := ptsF[i] + PointF(0.5, 0.5); result := RectUnion(result, NiceFrame(VirtualScreen, Manager.CanvasScale, ptsF, BGRAWhite,BGRABlack)); end else result := RectUnion(result, NiceFrameBounds(Manager.CanvasScale, ptsF)); end; end; function TToolSelectEllipse.GetContextualToolbars: TContextualToolbars; begin Result:= [ctRatio]; end; { TTransformSelectionTool } function TTransformSelectionTool.GetIsSelectingTool: boolean; begin result := true; end; function TTransformSelectionTool.GetAction: TLayerAction; begin Result:= nil; end; function TTransformSelectionTool.FixSelectionTransform: boolean; begin Result:= false; end; function TTransformSelectionTool.DoGetToolDrawingLayer: TBGRABitmap; begin result := Manager.Image.SelectionMaskReadonly; end; { TToolRotateSelection } function TToolRotateSelection.DoToolDown(toolDest: TBGRABitmap; pt: TPoint; ptF: TPointF; rightBtn: boolean): TRect; begin result := EmptyRect; if not FHandRotating and not FHandTranslating and not Manager.Image.SelectionMaskEmpty then begin if rightBtn then begin if FSnapMode then begin ptF.x := round(ptF.x*2)/2; ptF.y := round(ptF.y*2)/2; end; FCurrentAngle := 0; FFinalOffset := PointF(0, 0); FCurrentCenter := ptF; UpdateTransform; result := OnlyRenderChange; end else begin if VectLen(ptF - (FCurrentCenter + FFinalOffset)) < SelectionMaxPointDistance then FHandTranslating:= true else FHandRotating := true; FHandOrigin := ptF; FOffsetBeforeMove := FFinalOffset; end; end; end; function TToolRotateSelection.DoToolMove(toolDest: TBGRABitmap; pt: TPoint; ptF: TPointF): TRect; var angleDiff: single; finalCenter, newOfs: TPointF; begin if not HintShown then begin Manager.ToolPopup(tpmHoldKeyRestrictRotation, VK_CONTROL); HintShown:= true; end; if FHandRotating and ((FHandOrigin.X <> ptF.X) or (FHandOrigin.Y <> ptF.Y)) then begin finalCenter := FCurrentCenter + FFinalOffset; angleDiff := ComputeAngle(ptF.X - finalCenter.X, ptF.Y - finalCenter.Y)- ComputeAngle(FHandOrigin.X - finalCenter.X, FHandOrigin.Y - finalCenter.Y); if FSnapMode then begin FUnsnappedAngle += angleDiff; FCurrentAngle := round(FUnsnappedAngle/15)*15; end else FCurrentAngle := FCurrentAngle + angleDiff; UpdateTransform; FHandOrigin := ptF; result := OnlyRenderChange; end else if FHandTranslating and ((FHandOrigin.X <> ptF.X) or (FHandOrigin.Y <> ptF.Y)) then begin newOfs := FOffsetBeforeMove + ptF - FHandOrigin; if FSnapMode then begin newOfs.X := round(newOfs.x*2)/2; newOfs.Y := round(newOfs.y*2)/2; end; if newOfs <> FFinalOffset then begin FFinalOffset := newOfs; UpdateTransform; result := OnlyRenderChange; end else result := EmptyRect; end else begin if VectLen(ptF - (FCurrentCenter + FFinalOffset)) < SelectionMaxPointDistance then Cursor := crSizeAll else Cursor := crDefault; result := EmptyRect; end; end; function TToolRotateSelection.GetStatusText: string; begin Result:= 'α = '+FloatToStrF(FCurrentAngle,ffFixed,5,1) + '|' + 'Δx = '+FloatToStrF(FFinalOffset.X,ffFixed,6,1) + '|' + 'Δy = '+FloatToStrF(FFinalOffset.Y,ffFixed,6,1); end; procedure TToolRotateSelection.UpdateTransform; begin Manager.Image.SelectionTransform := AffineMatrixTranslation(FFinalOffset.X, FFinalOffset.Y) * AffineMatrixTranslation(FCurrentCenter.X,FCurrentCenter.Y) * AffineMatrixRotationDeg(FCurrentAngle) * AffineMatrixTranslation(-FCurrentCenter.X,-FCurrentCenter.Y) * FOriginalTransform; end; class procedure TToolRotateSelection.ForgetHintShown; begin HintShown:= false; end; constructor TToolRotateSelection.Create(AManager: TToolManager); begin inherited Create(AManager); FCurrentCenter := Manager.Image.SelectionTransform * Manager.Image.GetSelectionMaskCenter; FOriginalTransform := Manager.Image.SelectionTransform; FCurrentAngle := 0; FFinalOffset := PointF(0, 0); end; function TToolRotateSelection.DoToolKeyDown(var key: Word): TRect; begin result := EmptyRect; if key = VK_CONTROL then begin if not FSnapMode then begin FSnapMode := true; FUnsnappedAngle := FCurrentAngle; if FHandRotating then begin FCurrentAngle := round(FUnsnappedAngle/15)*15; UpdateTransform; result := OnlyRenderChange; end else if FHandTranslating then begin FFinalOffset.x := round(FFinalOffset.x*2)/2; FFinalOffset.y := round(FFinalOffset.y*2)/2; UpdateTransform; result := OnlyRenderChange; end; end; Key := 0; end else if key = VK_ESCAPE then begin if FCurrentAngle <> 0 then begin FCurrentAngle := 0; FFinalOffset := PointF(0, 0); UpdateTransform; result := OnlyRenderChange; end; Key := 0; end; end; function TToolRotateSelection.DoToolKeyUp(var key: Word): TRect; begin if key = VK_CONTROL then begin FSnapMode := false; Key := 0; end; result := EmptyRect; end; function TToolRotateSelection.ToolUp: TRect; begin FHandRotating:= false; FHandTranslating:= false; Result:= EmptyRect; end; function TToolRotateSelection.Render(VirtualScreen: TBGRABitmap; VirtualScreenWidth, VirtualScreenHeight: integer; BitmapToVirtualScreen: TBitmapToVirtualScreenFunction): TRect; var pictureRotateCenter: TPointF; begin pictureRotateCenter := BitmapToVirtualScreen(FCurrentCenter + FFinalOffset); result := NicePoint(VirtualScreen, pictureRotateCenter.X,pictureRotateCenter.Y); end; destructor TToolRotateSelection.Destroy; begin if FHandRotating then FHandRotating := false; if FHandTranslating then FHandTranslating := false; inherited Destroy; end; { TToolMoveSelection } function TToolMoveSelection.DoToolDown(toolDest: TBGRABitmap; pt: TPoint; ptF: TPointF; rightBtn: boolean): TRect; var untransformedPtF: TPointF; begin if not handMoving and not Manager.Image.SelectionMaskEmpty then begin handMoving := true; notMovedAtAll := true; if IsAffineMatrixInversible(Manager.Image.SelectionTransform) then begin untransformedPtF := AffineMatrixInverse(Manager.Image.SelectionTransform) * ptF; if Manager.Image.SelectionMaskReadonly.GetPixel(untransformedPtF.X, untransformedPtF.Y).green <> 0 then notMovedAtAll:= false; end; handOriginF := ptF; selectionTransformBefore := Manager.Image.SelectionTransform; end; result := EmptyRect; end; function TToolMoveSelection.DoToolMove(toolDest: TBGRABitmap; pt: TPoint; ptF: TPointF): TRect; var dx,dy: single; newSelTransform: TAffineMatrix; begin result := EmptyRect; if handMoving then begin dx := ptF.X-HandOriginF.X; dy := ptF.Y-HandOriginF.Y; if ssSnap in ShiftState then begin dx := round(dx); dy := round(dy); end; newSelTransform := AffineMatrixTranslation(dx,dy) * selectionTransformBefore; if Manager.Image.SelectionTransform <> newSelTransform then begin Manager.Image.SelectionTransform := newSelTransform; notMovedAtAll := false; result := OnlyRenderChange; end; end; end; constructor TToolMoveSelection.Create(AManager: TToolManager); begin inherited Create(AManager); handMoving := false; end; function TToolMoveSelection.ToolUp: TRect; begin if handMoving then begin handMoving := false; if notMovedAtAll then Manager.QueryExitTool; end; result := EmptyRect; end; destructor TToolMoveSelection.Destroy; begin if handMoving then handMoving := false; inherited Destroy; end; { TToolSelectionPen } function TToolSelectionPen.GetIsSelectingTool: boolean; begin Result:= true; end; function TToolSelectionPen.GetUniversalBrush(ARightButton: boolean): TUniversalBrush; begin if ARightButton then TBGRABitmap.SolidBrush(result, BGRABlack, dmLinearBlend) else TBGRABitmap.SolidBrush(result, BGRAWhite, dmLinearBlend); end; function TToolSelectionPen.GetContextualToolbars: TContextualToolbars; begin Result:= [ctPenWidth, ctAliasing]; end; { TToolMagicWand } function TToolMagicWand.GetIsSelectingTool: boolean; begin Result:= true; end; function TToolMagicWand.DoToolDown(toolDest: TBGRABitmap; pt: TPoint; ptF: TPointF; rightBtn: boolean): TRect; var penColor: TBGRAPixel; ofs: TPoint; mask: TGrayscaleMask; source: TBGRABitmap; targetRect: TRect; psource: PBGRAPixel; yb, xb: Integer; pmask: PByte; compareColor: TExpandedPixel; toleranceW: word; diff, diffDiv: integer; maskBrush: TUniversalBrush; begin if not Manager.Image.CurrentLayerVisible then begin result := EmptyRect; exit; end; if rightBtn then penColor := BGRABlack else penColor := BGRAWhite; ofs := Manager.Image.LayerOffset[Manager.Image.CurrentLayerIndex]; source := Manager.Image.CurrentLayerReadOnly; targetRect := RectWithSize(ofs.x, ofs.y, source.Width, source.Height); targetRect.Intersect( rect(0,0,toolDest.Width,toolDest.Height) ); if not targetRect.IsEmpty then begin if ffProgressive in Manager.FloodFillOptions then begin mask := TGrayscaleMask.Create(targetRect.Width, targetRect.Height, 0); compareColor := GammaExpansion(source.GetPixel(pt.X-ofs.X, pt.Y-ofs.Y)); toleranceW := Manager.Tolerance + (Manager.Tolerance shl 8); mask.SolidBrush(maskBrush, ByteMaskWhite); Manager.Image.CurrentLayerReadOnly.ParallelFloodFill(pt.X-ofs.X, pt.Y-ofs.Y, mask, maskBrush, false, toleranceW, ofs.X - targetRect.Left, ofs.Y - targetRect.Top); diffDiv := toleranceW + 1; for yb := 0 to mask.Height-1 do begin psource := PBGRAPixel(source.GetPixelAddress(targetRect.Left - ofs.x, yb + targetRect.Top - ofs.y)); pmask := mask.ScanLine[yb]; for xb := mask.Width-1 downto 0 do begin if pmask^ <> 0 then begin diff := diffDiv - ExpandedDiff(psource^.ToExpanded, compareColor); if diff >= 0 then pmask^ := (pmask^ * diff + (diffDiv shr 1)) div diffDiv else pmask^ := 0; end; inc(pmask); inc(psource); end; end; toolDest.FillMask(targetRect.Left, targetRect.Top, mask, penColor, dmDrawWithTransparency); mask.Free; end else source.ParallelFloodFill(pt.X-ofs.X, pt.Y-ofs.Y, toolDest, penColor, fmDrawWithTransparency, Manager.Tolerance, ofs.X, ofs.Y); end; result := targetRect; Action.NotifyChange(toolDest, result); ValidateAction; end; function TToolMagicWand.GetContextualToolbars: TContextualToolbars; begin Result:= [ctTolerance]; end; initialization RegisterTool(ptMagicWand,TToolMagicWand); RegisterTool(ptSelectPen,TToolSelectionPen); RegisterTool(ptSelectRect,TToolSelectRect); RegisterTool(ptSelectEllipse,TToolSelectEllipse); RegisterTool(ptSelectPoly,TToolSelectPoly); RegisterTool(ptSelectSpline,TToolSelectSpline); RegisterTool(ptMoveSelection,TToolMoveSelection); RegisterTool(ptRotateSelection,TToolRotateSelection); end.