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 withdisplay="none", is not hit, and neither is anything outside itsclip-path, on the element itself or on a group around it. pointer-events. An element withpointer-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. Withpointer-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:
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.