Как увидеть, перекрываются ли две фигуры

Я пытаюсь написать простое тестовое приложение firemonkey.

У меня есть форма с панелью (align:= alClient).
На форме 2 TCircle. Я установил TCircle.Dragmode:= dmAutomatic.

Я хотел бы перетаскивать круги и чтобы что-то происходило, когда круги перекрываются.
Вопрос в следующем: я не вижу в TCircle никакого метода, называемого перекрытием, и я не вижу события, вызываемого при перекрытии. Я перепробовал все события xxxxDrag, но это не помогает мне в тестировании.

Как я могу увидеть, когда перетаскиваемая фигура перекрывается другой фигурой?
Я ожидал, что одно из событий DragOver, DragEnter обнаружит это для меня, но, похоже, это не так.

Наверняка в Firemonkey для этого должен быть какой-то стандартный метод?

На данный момент файл pas выглядит так:

implementation

{$R *.fmx}

procedure TForm8.Circle1DragEnter(Sender: TObject; const Data: TDragObject;
  const Point: TPointF);
begin
  if Data.Source = Circle1 then Button1.Text:= 'DragEnter';

end;

procedure TForm8.Circle1DragOver(Sender: TObject; const Data: TDragObject;
  const Point: TPointF; var Accept: Boolean);
begin
  if (Data.Source = Circle2) then Button1.Text:= 'Circle2 drag';
end;

procedure TForm8.Circle2DragEnd(Sender: TObject);
begin
  Button1.Text:= 'DragEnd';
end;

procedure TForm8.Circle2DragEnter(Sender: TObject; const Data: TDragObject;
  const Point: TPointF);
begin
  Button1.Text:= 'DragEnter';
end;

procedure TForm8.Circle2DragLeave(Sender: TObject);
begin
  Button1.Text:= 'DragLeave';
end;

procedure TForm8.Circle2DragOver(Sender: TObject; const Data: TDragObject;
  const Point: TPointF; var Accept: Boolean);
begin
  if Data.Source = Circle2 then begin

    Button1.Text:= 'DragOver';
    Accept:= true;
  end;
end;

ДФМ выглядит примерно так:

object Form8: TForm8
  Left = 0
  Top = 0
  BiDiMode = bdLeftToRight
  Caption = 'Form8'
  ClientHeight = 603
  ClientWidth = 821
  Transparency = False
  Visible = False
  StyleLookup = 'backgroundstyle'
  object Panel1: TPanel
    Align = alClient
    Width = 821.000000000000000000
    Height = 603.000000000000000000
    TabOrder = 1
    object Button1: TButton
      Position.Point = '(16,16)'
      Width = 80.000000000000000000
      Height = 22.000000000000000000
      TabOrder = 1
      StaysPressed = False
      IsPressed = False
      Text = 'Button1'
    end
    object Circle1: TCircle
      DragMode = dmAutomatic
      Position.Point = '(248,120)'
      Width = 97.000000000000000000
      Height = 105.000000000000000000
      OnDragEnter = Circle1DragEnter
      OnDragOver = Circle1DragOver
    end
    object Circle2: TCircle
      DragMode = dmAutomatic
      Position.Point = '(168,280)'
      Width = 81.000000000000000000
      Height = 65.000000000000000000
      OnDragEnter = Circle2DragEnter
      OnDragLeave = Circle2DragLeave
      OnDragOver = Circle2DragOver
      OnDragEnd = Circle2DragEnd
    end
  end
end

person Johan    schedule 02.10.2011    source источник
comment
Будет ли разрешено использование функций Windows, таких как CombineRgn, поскольку ваши вопросы отмечены тегом firemonkey, что означает, что вы ищете решения, независимые от Windows?   -  person bummi    schedule 18.09.2013
comment
Какие формы вас интересуют? Только круг, прямоугольник, прямоугольник, эллипс и круг или больше?   -  person NGLN    schedule 18.09.2013
comment
В ответ на примечания к награде: Учитывая сложность проблемы, кажется маловероятным, что FireMonkey предоставит больше, чем самое простое и элементарное тестирование на попадание - если вообще! Вероятно, вам лучше поискать сторонние игровые библиотеки.   -  person Disillusioned    schedule 19.09.2013
comment
@CraigYoung, учитывая целевой рынок, для firemonkey имело бы смысл поддержать это, но я понимаю вашу точку зрения.   -  person Johan    schedule 20.09.2013


Ответы (5)


Общая проблема сложна и известна как обнаружение столкновений — вы можете использовать этот термин в Google, чтобы найти соответствующие алгоритмы.

Частный случай обнаружения столкновений кругов прост - достаточно вычислить расстояние между центрами кругов. Если полученное расстояние меньше суммы радиусов кругов, круги перекрываются.

person kludg    schedule 02.10.2011
comment
Я надеялся, что FireMonkey позаботится об этом за меня. - person Johan; 03.10.2011
comment
Firemonkey не заботится об обнаружении столкновений - person Glen Morse; 16.09.2013

Хотя этому вопросу уже больше года, недавно я столкнулся с аналогичной проблемой. Благодаря небольшому исследованию TRectF (используемого примитивами FMX и FM2), я придумал следующую очень простую функцию;

var
 aRect1, aRect2 : TRectF;
begin
  aRect1 := Selection1.AbsoluteRect;
  aRect2 := Selection2.AbsoluteRect;
  if System.Types.IntersectRect(aRect1,aRect2) then Result := True else Result := False;
end;

Не требует пояснений, но если 2 прямоугольника/объекта пересекаются или перекрываются, то результат верный.

Альтернативный вариант: та же процедура, но улучшенный код

var
 aRect1, aRect2 : TRectF;
begin
  aRect1 := Selection1.AbsoluteRect;
  aRect2 := Selection2.AbsoluteRect;
  result := System.Types.IntersectRect(aRect1,aRect2);
end;

Вам нужно будет поработать над этим, чтобы принять некоторые входные объекты (в моем случае я использовал TSelection, известные как Selection1 и Selection2) и, возможно, найти способ добавить смещение (взгляните на TControl.GetAbsoluteRect в FMX.Types), но теоретически это должно работать практически с любым примитивом или любым элементом управления.

В качестве дополнительного примечания, для подобных объектов используется множество TRectF;

  • AbsoluteRect
  • BoundsRect
  • LocalRect
  • UpdateRect (Может не относиться к данной ситуации, необходимо расследование)
  • ParentedRect
  • ClipRect
  • ChildrenRect

Важно использовать тот, который наиболее подходит для вашей ситуации (поскольку результаты будут сильно различаться в каждом случае). В моем примере TSelection были дочерними элементами формы, поэтому использование AbsoluteRect было лучшим выбором (поскольку LocalRect не возвращало правильные значения).

На самом деле, вы могли бы пройтись по каждому дочернему компоненту вашего родителя, чтобы выяснить, есть ли коллизия между ними, и, возможно, вы могли бы создать функцию, которая точно сообщает вам, какие из них сталкиваются (хотя для этого, вероятно, потребуется рекурсивная функция ).

Если вам когда-нибудь понадобится иметь дело с «базовой физикой», согласно которой обнаружение столкновений будет считаться одним из них (по крайней мере, в данном случае это базовый уровень) в Firemonkey, то вам нужно разобраться с TRectF. В System.Types (XE3 и, вероятно, XE2) встроено множество подпрограмм для автоматической обработки этих вещей, и поэтому вы можете избежать большого количества математических вычислений, обычно связанных с этой проблемой.

Дополнительные примечания

Что-то, что я заметил, было то, что описанная выше процедура была не очень точной и отсутствовала на несколько пикселей. Одно из решений — поместить вашу фигуру в родительский контейнер с выравниванием alClient, а затем отступом по 5 пикселей со всех сторон. Затем, вместо измерения TSelection.AbsoluteRect, измерьте AbsoluteRect дочернего объекта.

Например, я поместил TCircle внутри каждого TSelection, установил выравнивание кругов на alClient, отступы на 5 с каждой стороны и изменил процедуру для работы с Circle1 и Circle2, а не с Selection1 и Selection2. Это оказалось точным до такой степени, что если бы сами круги не перекрывались (точнее, их площадь не перекрывалась), то они не воспринимались бы как сталкивающиеся до тех пор, пока их края не соприкоснулись. Очевидно, что углы самих кругов являются проблемой, но вы, возможно, могли бы добавить еще один дочерний компонент внутри каждого круга с его видимостью, установленной на false, и с немного меньшими размерами, чтобы имитировать старый метод столкновения «Bounding Box». обнаружение.

Пример приложения

Я добавил пример приложения с исходным кодом, показывающим вышеизложенное. Вкладка 1 представляет собой полезный пример, а вторая вкладка дает краткое объяснение того, как работает TRectF (и показывает некоторые ограничения, связанные с использованием визуального интерфейса, похожего на радар). На третьей вкладке демонстрируется использование TBitmapListAnimation для создания анимированных изображений. .

Обнаружение конфликтов FMX — пример и источник

person Scott P    schedule 17.11.2012
comment
Спасибо за ответ, но я ищу не перекрывающиеся прямоугольники, а перекрывающиеся произвольные формы. Тестирование на наличие прямых попаданий — это только первый шаг. - person Johan; 19.11.2012
comment
Я полагаю, что то, что он говорил, является примером: вы должны взять одну область (прямую в данном случае) и посмотреть, разделяет ли другая область (в данном случае) ту же область. Таким образом, это будет работать с любой формой. - person Glen Morse; 16.09.2013
comment
Я считаю, что все фигуры используют «хитбокс» прямоугольника независимо от фактической отображаемой формы. Я упомянул смещения, поскольку установка отрицательного смещения позволяет вам имитировать форму через ее хитбокс. Например, мне удалось проверить круги, сталкивающиеся через их прямоугольный хитбокс, и сымитировать это, располагая углы хитбокса сразу за краем круга. Это не была абсолютная точность, но это означало, что я нашел золотую середину, будучи слишком чувствительным и недостаточно чувствительным к перекрывающимся формам. Я изучу это снова и посмотрю, смогу ли я найти лучшее решение. - person Scott P; 16.09.2013

Мне кажется, что существует слишком много возможных перестановок, чтобы легко решить эту проблему и эффективно. Некоторые частные случаи могут иметь простое и эффективное решение: например. пересечение курсора мыши упрощается за счет рассмотрения только одной точки на курсоре; предоставлена ​​очень хорошая техника кругов; многие обычные формы также могут выиграть от пользовательских формул для обнаружения столкновений.

Однако неправильные формы значительно усложняют задачу.

Одним из вариантов было бы заключить каждую фигуру в воображаемый круг. Если эти круги перекрываются, вы можете представить меньшие более узкие круги вблизи исходного пересечения. Повторяйте вычисления с меньшими и меньшими кругами столько раз, сколько необходимо. Этот подход позволит вам выбрать компромисс между требованиями к обработке и точностью обнаружения.

Более простой и очень общий, хотя и несколько менее эффективный подход, заключался бы в том, чтобы нарисовать каждую фигуру на холсте за пределами экрана, используя сплошные цвета и маску xor. Если после рисования будут обнаружены какие-либо пиксели цвета xor, это будет указывать на коллизию.

person Disillusioned    schedule 18.09.2013

Таким образом, начинается/настройка обнаружения коллизий между TCircle, TRectangle и TRoundRect:

unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Objects, Generics.Collections, Math;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    Circle1: TCircle;
    Circle2: TCircle;
    Rectangle1: TRectangle;
    Rectangle2: TRectangle;
    RoundRect1: TRoundRect;
    RoundRect2: TRoundRect;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Panel1DragOver(Sender: TObject; const Data: TDragObject;
      const Point: TPointF; var Accept: Boolean);
    procedure Panel1DragDrop(Sender: TObject; const Data: TDragObject;
      const Point: TPointF);
  private
    FShapes: TList<TShape>;
    function CollidesWith(Source: TShape; const SourceCenter: TPointF;
      out Target: TShape): Boolean;
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

function Radius(AShape: TShape): Single;
begin
  Result := Min(AShape.ShapeRect.Width, AShape.ShapeRect.Height) / 2;
end;

function TForm1.CollidesWith(Source: TShape; const SourceCenter: TPointF;
  out Target: TShape): Boolean;
var
  Shape: TShape;
  TargetCenter: TPointF;

  function CollidesCircleCircle: Boolean;
  begin
    Result :=
      TargetCenter.Distance(SourceCenter) <= (Radius(Source) + Radius(Target));
  end;

  function CollidesCircleRectangle: Boolean;
  var
    Dist: TSizeF;
    RHorz: TRectF;
    RVert: TRectF;
  begin
    Dist.cx := Abs(TargetCenter.X - SourceCenter.X);
    Dist.cy := Abs(TargetCenter.Y - SourceCenter.Y);
    RHorz := Target.ShapeRect;
    RHorz.Offset(Target.ParentedRect.TopLeft);
    RVert := RHorz;
    RHorz.Inflate(Radius(Source), 0);
    RVert.Inflate(0, Radius(Source));
    Result := RHorz.Contains(SourceCenter) or RVert.Contains(SourceCenter) or
      (Sqr(RVert.Width / 2 - Dist.cx) + Sqr(RHorz.Height / 2 - Dist.cy) <= 
        Sqr(Radius(Source)));
  end;

  function CollidesRectangleCircle: Boolean;
  var
    Dist: TSizeF;
    RHorz: TRectF;
    RVert: TRectF;
  begin
    Dist.cx := Abs(TargetCenter.X - SourceCenter.X);
    Dist.cy := Abs(TargetCenter.Y - SourceCenter.Y);
    RHorz := Source.ShapeRect;
    RHorz.Offset(Source.ParentedRect.TopLeft);
    RHorz.Offset(SourceCenter.Subtract(Source.ParentedRect.CenterPoint));
    RVert := RHorz;
    RHorz.Inflate(Radius(Target), 0);
    RVert.Inflate(0, Radius(Target));
    Result := RHorz.Contains(TargetCenter) or RVert.Contains(TargetCenter) or
      (Sqr(RVert.Width / 2 - Dist.cx) + Sqr(RHorz.Height / 2 - Dist.cy) <= 
        Sqr(Radius(Target)));
  end;

  function CollidesRectangleRectangle: Boolean;
  var
    Dist: TSizeF;
  begin
    Dist.cx := Abs(TargetCenter.X - SourceCenter.X);
    Dist.cy := Abs(TargetCenter.Y - SourceCenter.Y);
    Result := 
      (Dist.cx <= (Source.ShapeRect.Width + Target.ShapeRect.Width) / 2) and
      (Dist.cy <= (Source.ShapeRect.Height + Target.ShapeRect.Height) / 2); 
  end;

  function CollidesCircleRoundRect: Boolean;
  var
    Dist: TSizeF;
    R: TRectF;
  begin
    Dist.cx := Abs(TargetCenter.X - SourceCenter.X);
    Dist.cy := Abs(TargetCenter.Y - SourceCenter.Y);
    R := Target.ShapeRect;
    R.Offset(Target.ParentedRect.TopLeft);
    if R.Width > R.Height then
    begin
      Dist.cx := Dist.cx - (R.Width - R.Height) / 2;
      R.Inflate(-Radius(Target), Radius(Source));
    end
    else
    begin
      Dist.cy := Dist.cy - (R.Height - R.Width) / 2;
      R.Inflate(Radius(Source), -Radius(Target));
    end;
    Result := R.Contains(SourceCenter) or
      (Sqrt(Sqr(Dist.cx) + Sqr(Dist.cy)) <= (Radius(Source) + Radius(Target)));
  end;

  function CollidesRoundRectCircle: Boolean;
  var
    Dist: TSizeF;
    R: TRectF;
  begin
    Dist.cx := Abs(TargetCenter.X - SourceCenter.X);
    Dist.cy := Abs(TargetCenter.Y - SourceCenter.Y);
    R := Source.ShapeRect;
    R.Offset(Source.ParentedRect.TopLeft);
    R.Offset(SourceCenter.Subtract(Source.ParentedRect.CenterPoint));
    if R.Width > R.Height then
    begin
      Dist.cx := Dist.cx - (R.Width - R.Height) / 2;
      R.Inflate(-Radius(Source), Radius(Target));
    end
    else
    begin
      Dist.cy := Dist.cy - (R.Height - R.Width) / 2;
      R.Inflate(Radius(Target), -Radius(Source));
    end;
    Result := R.Contains(TargetCenter) or
      (Sqrt(Sqr(Dist.cx) + Sqr(Dist.cy)) <= (Radius(Source) + Radius(Target)));
  end;

  function CollidesRectangleRoundRect: Boolean;
  begin
    Result := False;
  end;

  function CollidesRoundRectRectangle: Boolean;
  begin
    Result := False;
  end;

  function CollidesRoundRectRoundRect: Boolean;
  begin
    Result := False;
  end;

  function Collides: Boolean;
  begin
    if (Source is TCircle) and (Target is TCircle) then
      Result := CollidesCircleCircle
    else if (Source is TCircle) and (Target is TRectangle) then
      Result := CollidesCircleRectangle
    else if (Source is TRectangle) and (Target is TCircle) then
      Result := CollidesRectangleCircle
    else if (Source is TRectangle) and (Target is TRectangle) then
      Result := CollidesRectangleRectangle
    else if (Source is TCircle) and (Target is TRoundRect) then
      Result := CollidesCircleRoundRect
    else if (Source is TRoundRect) and (Target is TCircle) then
      Result := CollidesRoundRectCircle
    else if (Source is TRectangle) and (Target is TRoundRect) then
      Result := CollidesRectangleRoundRect
    else if (Source is TRoundRect) and (Target is TRectangle) then
      Result := CollidesRoundRectRectangle
    else if (Source is TRoundRect) and (Target is TRoundRect) then
      Result := CollidesRoundRectRoundRect
    else
      Result := False;
  end;

begin
  Result := False;
  for Shape in FShapes do
  begin
    Target := Shape;
    TargetCenter := Target.ParentedRect.CenterPoint;
    Result := (Target <> Source) and Collides;
    if Result then
      Break;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FShapes := TList<TShape>.Create;
  FShapes.AddRange([Circle1, Circle2, Rectangle1, Rectangle2, RoundRect1,
    RoundRect2]);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FShapes.Free;
end;

procedure TForm1.Panel1DragDrop(Sender: TObject; const Data: TDragObject;
  const Point: TPointF);
var
  Source: TShape;
begin
  Source := TShape(Data.Source);
  Source.Position.Point := PointF(Point.X - Source.Width / 2,
    Point.Y - Source.Height / 2);
end;

procedure TForm1.Panel1DragOver(Sender: TObject; const Data: TDragObject;
  const Point: TPointF; var Accept: Boolean);
var
  Source: TShape;
  Target: TShape;
begin
  Source := TShape(Data.Source);
  if CollidesWith(Source, Point, Target) then
    Caption :=  Format('Kisses between %s and %s', [Source.Name, Target.Name])
  else
    Caption := 'No love';
  Accept := True;
end;

end.
person NGLN    schedule 20.09.2013

Думаю, мы должны свернуть свои собственные.

Одним из вариантов для этого является 2D-реализация алгоритма Gilbert-Johnson. - Алгоритм расстояния Кирти.

Реализацию D можно найти здесь: http://code.google.com/p/gjkd/source/browse/

person Johan    schedule 20.09.2013