Skip to content

Hit testing

Hit testing answers the question "what is under this point?". Use it when your code decides what can be clicked: select the room a user clicks in a floor plan, show the details of the device under the pointer, or ignore clicks on empty space.

The element under the pointer

ObjectAtPt returns the element under a point on the component, or nil:

procedure TForm1.SVG2Image1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  Obj: ISVGObject;
begin
  Obj := SVG2Image1.ObjectAtPt(SVGPoint(X, Y), False);
  if Assigned(Obj) then
    Caption := Obj.LocalName + ' ' + Obj.ID
  else
    Caption := 'nothing';
end;

The point is in pixels of the component, as the mouse events give it. The zoom, the pan and the fit of the drawing to the component are taken into account. TSVG2WinControl and the FMX components take the point as a TPointF.

Pass False, or set HitTest

The second parameter, aIfHitTest, is True by default. It then only considers elements whose HitTest property is set, and no element has it set until your code sets it. Called with its default, ObjectAtPt therefore finds nothing.

Pass False to consider every element. Or, to make only some elements clickable, set HitTest on those and keep the default:

// Only the three hands of the clock can be picked up
SVG2Image1.SVGRoot.SVGObject['HourHand'].HitTest := True;
SVG2Image1.SVGRoot.SVGObject['MinuteHand'].HitTest := True;
SVG2Image1.SVGRoot.SVGObject['SecondHand'].HitTest := True;

HitTest is kept on the element, so set it again after loading a drawing.

The bounds used for hit testing come from the last render, so the component must have painted at least once. sroEvents is not needed for ObjectAtPt.

What counts as a hit

ObjectAtPt finds what a click at that point would go to, following the rules browsers use:

  • The painted parts. A shape is hit on its fill if it has one, and on its stroke if it has one, including the half of the stroke that lies outside the fill. A circle with fill="none" is only hit on its outline.
  • The topmost element. Where shapes overlap, the one drawn last wins.
  • Visible elements only. An element with visibility="hidden", or with display="none", is not hit, and neither is anything outside its clip-path, on the element itself or on a group around it.
  • pointer-events. An element with pointer-events="none" is skipped, so the element underneath is found.

ObjectAtPt returns the shape itself: a rect, circle, path or text, never the group it is in. A shape drawn through a use element is the exception: it is returned as the use, see Symbols placed with use.

An image is returned as a whole. Its rectangle (x, y, width and height) counts, transparent parts included, as in browsers. When the image shows another SVG drawing, the elements of that drawing are not found; the image element is.

Things a designer can do

Two attributes let the drawing itself decide what the pointer hits. Both are standard SVG, so they also work in a browser:

  • Labels over symbols. Text or a highlight drawn on top of a symbol would catch the clicks meant for the symbol. pointer-events="none" on the label lets them through.
  • Invisible click areas. A shape with fill="none" and no stroke is not hit. With pointer-events="visible" it is, over its whole area, although it is not painted. Put one over a small or complicated symbol to make it easy to click.
<g id="valve-12">
  <path d="..." fill="#888"/>
  <text x="10" y="40" pointer-events="none">V-12</text>
  <rect x="0" y="0" width="40" height="40" fill="none" pointer-events="visible"/>
</g>

From the shape to what it belongs to

A symbol is usually a group of shapes, and the id your application knows is on the group. Walk up from the shape to the first element with an id:

function OwnerWithID(aObj: ISVGObject): ISVGElement;
var
  Node: IXMLNode;
begin
  Result := nil;
  Node := aObj as IXMLNode;
  while Assigned(Node) do
  begin
    if Supports(Node, ISVGElement, Result) and (Result.ID <> '') then
      Exit;
    Node := Node.ParentNode;
  end;
  Result := nil;
end;

A drawing can mark its symbols with a class or a custom attribute instead, and the walk can stop at that. See Finding elements.

Symbols placed with use

A symbol drawn many times, such as a valve, a seat or a sensor, is usually defined once and placed with use:

<symbol id="valve">...</symbol>
<use id="valve-12" href="#valve" x="100" y="40"/>
<use id="valve-13" href="#valve" x="180" y="40"/>

A point on a placement returns that use element, so ObjectAtPt answers valve-12 or valve-13, and the id is the placement's own. This is what browsers do, and it is the element a click bubbles to in the SVG events. When a symbol itself contains a use, the outermost use, the one in the drawing, is returned.

With aIfHitTest set to True, setting HitTest on the use makes that placement findable; setting it on the shape inside the symbol makes every placement of it findable.

Where in the drawing

To know the position rather than the element, convert the pointer with ClientToSVG. It returns the point in the user units of the drawing's viewBox, whatever the zoom and whatever size the drawing is shown at:

procedure TForm1.SVG2Image1MouseMove(Sender: TObject; Shift: TShiftState;
  X, Y: Integer);
var
  P: TSVGPoint;
begin
  P := SVG2Image1.ClientToSVG(SVGPoint(X, Y));
  StatusBar1.SimpleText := Format('x %.1f  y %.1f', [P.X, P.Y]);
end;

SVGToClient goes the other way, for example to place a pop-up or a hint next to a point of the drawing. Both need the component to have painted once.

Without a component

SVGObjectAt in BVE.SVG2Elements does the same for a document rendered with SVGRenderToBitmap or into your own render context. The point is in the pixels of that render:

Obj := SVGObjectAt(Root, SVGPoint(X, Y), False);

Text

Text is hit on its character cells, as in browsers: each character from its start to its advance, and from the font's ascent above the baseline to its descent below it. A point between two letters, or above a lowercase letter, hits the label. Space above the tallest letters of the font, or past the end of the text, does not. For a larger click area, group the label with an unpainted rectangle that has pointer-events="visible", and handle the click on the group.

Reference

ObjectAtPt, ClientToSVG, SVGToClient, ISVGObject.HitTest, SVGObjectAt.