1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include <drawinglayer/processor2d/hittestprocessor2d.hxx>
21 #include <drawinglayer/primitive2d/drawinglayer_primitivetypes2d.hxx>
22 #include <drawinglayer/primitive2d/transformprimitive2d.hxx>
23 #include <drawinglayer/primitive2d/polygonprimitive2d.hxx>
24 #include <drawinglayer/primitive2d/PolyPolygonColorPrimitive2D.hxx>
25 #include <basegfx/polygon/b2dpolygontools.hxx>
26 #include <basegfx/polygon/b2dpolypolygontools.hxx>
27 #include <drawinglayer/primitive2d/transparenceprimitive2d.hxx>
28 #include <drawinglayer/primitive2d/maskprimitive2d.hxx>
29 #include <drawinglayer/primitive2d/sceneprimitive2d.hxx>
30 #include <drawinglayer/primitive2d/pointarrayprimitive2d.hxx>
31 #include <basegfx/matrix/b3dhommatrix.hxx>
32 #include <drawinglayer/processor3d/cutfindprocessor3d.hxx>
33 #include <drawinglayer/primitive2d/hiddengeometryprimitive2d.hxx>
34 #include <drawinglayer/primitive2d/bitmapprimitive2d.hxx>
35 #include <comphelper/lok.hxx>
36 #include <toolkit/helper/vclunohelper.hxx>
38 namespace drawinglayer::processor2d
40 HitTestProcessor2D::HitTestProcessor2D(const geometry::ViewInformation2D
& rViewInformation
,
41 const basegfx::B2DPoint
& rLogicHitPosition
,
42 double fLogicHitTolerance
,
44 : BaseProcessor2D(rViewInformation
),
45 maDiscreteHitPosition(),
46 mfDiscreteHitTolerance(0.0),
48 mbCollectHitStack(false),
50 mbHitTextOnly(bHitTextOnly
)
53 mfDiscreteHitTolerance
= fLogicHitTolerance
;
55 if(basegfx::fTools::less(mfDiscreteHitTolerance
, 0.0))
57 // ensure input parameter for hit tolerance is >= 0.0
58 mfDiscreteHitTolerance
= 0.0;
60 else if(basegfx::fTools::more(mfDiscreteHitTolerance
, 0.0))
62 // generate discrete hit tolerance
63 mfDiscreteHitTolerance
= (getViewInformation2D().getObjectToViewTransformation()
64 * basegfx::B2DVector(mfDiscreteHitTolerance
, 0.0)).getLength();
67 // generate discrete hit position
68 maDiscreteHitPosition
= getViewInformation2D().getObjectToViewTransformation() * rLogicHitPosition
;
71 HitTestProcessor2D::~HitTestProcessor2D()
75 bool HitTestProcessor2D::checkHairlineHitWithTolerance(
76 const basegfx::B2DPolygon
& rPolygon
,
77 double fDiscreteHitTolerance
) const
79 basegfx::B2DPolygon
aLocalPolygon(rPolygon
);
80 aLocalPolygon
.transform(getViewInformation2D().getObjectToViewTransformation());
83 basegfx::B2DRange
aPolygonRange(aLocalPolygon
.getB2DRange());
85 if(basegfx::fTools::more(fDiscreteHitTolerance
, 0.0))
87 aPolygonRange
.grow(fDiscreteHitTolerance
);
90 // do rough range test first
91 if(aPolygonRange
.isInside(getDiscreteHitPosition()))
93 // check if a polygon edge is hit
94 return basegfx::utils::isInEpsilonRange(
96 getDiscreteHitPosition(),
97 fDiscreteHitTolerance
);
103 bool HitTestProcessor2D::checkFillHitWithTolerance(
104 const basegfx::B2DPolyPolygon
& rPolyPolygon
,
105 double fDiscreteHitTolerance
) const
108 basegfx::B2DPolyPolygon
aLocalPolyPolygon(rPolyPolygon
);
109 aLocalPolyPolygon
.transform(getViewInformation2D().getObjectToViewTransformation());
111 // get discrete range
112 basegfx::B2DRange
aPolygonRange(aLocalPolyPolygon
.getB2DRange());
113 const bool bDiscreteHitToleranceUsed(basegfx::fTools::more(fDiscreteHitTolerance
, 0.0));
115 if(bDiscreteHitToleranceUsed
)
117 aPolygonRange
.grow(fDiscreteHitTolerance
);
120 // do rough range test first
121 if(aPolygonRange
.isInside(getDiscreteHitPosition()))
123 // if a HitTolerance is given, check for polygon edge hit in epsilon first
124 if(bDiscreteHitToleranceUsed
&&
125 basegfx::utils::isInEpsilonRange(
127 getDiscreteHitPosition(),
128 fDiscreteHitTolerance
))
133 // check for hit in filled polyPolygon
134 if(!bRetval
&& basegfx::utils::isInside(
136 getDiscreteHitPosition(),
146 void HitTestProcessor2D::check3DHit(const primitive2d::ScenePrimitive2D
& rCandidate
)
148 // calculate relative point in unified 2D scene
149 const basegfx::B2DPoint
aLogicHitPosition(getViewInformation2D().getInverseObjectToViewTransformation() * getDiscreteHitPosition());
151 // use bitmap check in ScenePrimitive2D
152 bool bTryFastResult(false);
154 if(rCandidate
.tryToCheckLastVisualisationDirectHit(aLogicHitPosition
, bTryFastResult
))
156 mbHit
= bTryFastResult
;
160 basegfx::B2DHomMatrix
aInverseSceneTransform(rCandidate
.getObjectTransformation());
161 aInverseSceneTransform
.invert();
162 const basegfx::B2DPoint
aRelativePoint(aInverseSceneTransform
* aLogicHitPosition
);
164 // check if test point is inside scene's unified area at all
165 if(aRelativePoint
.getX() >= 0.0 && aRelativePoint
.getX() <= 1.0
166 && aRelativePoint
.getY() >= 0.0 && aRelativePoint
.getY() <= 1.0)
168 // get 3D view information
169 const geometry::ViewInformation3D
& rObjectViewInformation3D
= rCandidate
.getViewInformation3D();
171 // create HitPoint Front and Back, transform to object coordinates
172 basegfx::B3DHomMatrix
aViewToObject(rObjectViewInformation3D
.getObjectToView());
173 aViewToObject
.invert();
174 const basegfx::B3DPoint
aFront(aViewToObject
* basegfx::B3DPoint(aRelativePoint
.getX(), aRelativePoint
.getY(), 0.0));
175 const basegfx::B3DPoint
aBack(aViewToObject
* basegfx::B3DPoint(aRelativePoint
.getX(), aRelativePoint
.getY(), 1.0));
177 if(!aFront
.equal(aBack
))
179 const primitive3d::Primitive3DContainer
& rPrimitives
= rCandidate
.getChildren3D();
181 if(!rPrimitives
.empty())
183 // make BoundVolume empty and overlapping test for speedup
184 const basegfx::B3DRange
aObjectRange(
185 rPrimitives
.getB3DRange(rObjectViewInformation3D
));
187 if(!aObjectRange
.isEmpty())
189 const basegfx::B3DRange
aFrontBackRange(aFront
, aBack
);
191 if(aObjectRange
.overlaps(aFrontBackRange
))
193 // bound volumes hit, geometric cut tests needed
194 drawinglayer::processor3d::CutFindProcessor
aCutFindProcessor(
195 rObjectViewInformation3D
,
199 aCutFindProcessor
.process(rPrimitives
);
201 mbHit
= (!aCutFindProcessor
.getCutPoints().empty());
210 // empty 3D scene; Check for border hit
211 basegfx::B2DPolygon
aOutline(basegfx::utils::createUnitPolygon());
212 aOutline
.transform(rCandidate
.getObjectTransformation());
214 mbHit
= checkHairlineHitWithTolerance(aOutline
, getDiscreteHitTolerance());
219 void HitTestProcessor2D::processBasePrimitive2D(const primitive2d::BasePrimitive2D
& rCandidate
)
223 // stop processing as soon as a hit was recognized
227 switch(rCandidate
.getPrimitive2DID())
229 case PRIMITIVE2D_ID_TRANSFORMPRIMITIVE2D
:
231 // remember current ViewInformation2D
232 const primitive2d::TransformPrimitive2D
& rTransformCandidate(static_cast< const primitive2d::TransformPrimitive2D
& >(rCandidate
));
233 const geometry::ViewInformation2D
aLastViewInformation2D(getViewInformation2D());
235 // create new local ViewInformation2D containing transformation
236 const geometry::ViewInformation2D
aViewInformation2D(
237 getViewInformation2D().getObjectTransformation() * rTransformCandidate
.getTransformation(),
238 getViewInformation2D().getViewTransformation(),
239 getViewInformation2D().getViewport(),
240 getViewInformation2D().getVisualizedPage(),
241 getViewInformation2D().getViewTime(),
242 getViewInformation2D().getExtendedInformationSequence());
243 updateViewInformation(aViewInformation2D
);
245 // process child content recursively
246 process(rTransformCandidate
.getChildren());
248 // restore transformations
249 updateViewInformation(aLastViewInformation2D
);
253 case PRIMITIVE2D_ID_POLYGONHAIRLINEPRIMITIVE2D
:
255 if(!getHitTextOnly())
257 // create hairline in discrete coordinates
258 const primitive2d::PolygonHairlinePrimitive2D
& rPolygonCandidate(static_cast< const primitive2d::PolygonHairlinePrimitive2D
& >(rCandidate
));
261 mbHit
= checkHairlineHitWithTolerance(rPolygonCandidate
.getB2DPolygon(), getDiscreteHitTolerance());
266 case PRIMITIVE2D_ID_POLYGONMARKERPRIMITIVE2D
:
268 if(!getHitTextOnly())
270 // handle marker like hairline; no need to decompose in dashes
271 const primitive2d::PolygonMarkerPrimitive2D
& rPolygonCandidate(static_cast< const primitive2d::PolygonMarkerPrimitive2D
& >(rCandidate
));
274 mbHit
= checkHairlineHitWithTolerance(rPolygonCandidate
.getB2DPolygon(), getDiscreteHitTolerance());
279 case PRIMITIVE2D_ID_POLYGONSTROKEPRIMITIVE2D
:
281 if(!getHitTextOnly())
283 // handle stroke evtl. directly; no need to decompose to filled polygon outlines
284 const primitive2d::PolygonStrokePrimitive2D
& rPolygonCandidate(static_cast< const primitive2d::PolygonStrokePrimitive2D
& >(rCandidate
));
285 const attribute::LineAttribute
& rLineAttribute
= rPolygonCandidate
.getLineAttribute();
287 if(basegfx::fTools::more(rLineAttribute
.getWidth(), 0.0))
289 if(basegfx::B2DLineJoin::Miter
== rLineAttribute
.getLineJoin())
291 // if line is mitered, use decomposition since mitered line
292 // geometry may use more space than the geometry grown by half line width
297 // for all other B2DLINEJOIN_* do a hairline HitTest with expanded tolerance
298 const basegfx::B2DVector
aDiscreteHalfLineVector(getViewInformation2D().getObjectToViewTransformation()
299 * basegfx::B2DVector(rLineAttribute
.getWidth() * 0.5, 0.0));
300 mbHit
= checkHairlineHitWithTolerance(
301 rPolygonCandidate
.getB2DPolygon(),
302 getDiscreteHitTolerance() + aDiscreteHalfLineVector
.getLength());
307 // hairline; fallback to hairline test. Do not decompose
308 // since this may decompose the hairline to dashes
309 mbHit
= checkHairlineHitWithTolerance(rPolygonCandidate
.getB2DPolygon(), getDiscreteHitTolerance());
315 case PRIMITIVE2D_ID_POLYGONWAVEPRIMITIVE2D
:
317 if(!getHitTextOnly())
319 // do not use decompose; just handle like a line with width
320 const primitive2d::PolygonWavePrimitive2D
& rPolygonCandidate(static_cast< const primitive2d::PolygonWavePrimitive2D
& >(rCandidate
));
321 double fLogicHitTolerance(0.0);
323 // if WaveHeight, grow by it
324 if(basegfx::fTools::more(rPolygonCandidate
.getWaveHeight(), 0.0))
326 fLogicHitTolerance
+= rPolygonCandidate
.getWaveHeight();
329 // if line width, grow by it
330 if(basegfx::fTools::more(rPolygonCandidate
.getLineAttribute().getWidth(), 0.0))
332 fLogicHitTolerance
+= rPolygonCandidate
.getLineAttribute().getWidth() * 0.5;
335 const basegfx::B2DVector
aDiscreteHalfLineVector(getViewInformation2D().getObjectToViewTransformation()
336 * basegfx::B2DVector(fLogicHitTolerance
, 0.0));
338 mbHit
= checkHairlineHitWithTolerance(
339 rPolygonCandidate
.getB2DPolygon(),
340 getDiscreteHitTolerance() + aDiscreteHalfLineVector
.getLength());
345 case PRIMITIVE2D_ID_POLYPOLYGONCOLORPRIMITIVE2D
:
347 if(!getHitTextOnly())
349 // create filled polyPolygon in discrete coordinates
350 const primitive2d::PolyPolygonColorPrimitive2D
& rPolygonCandidate(static_cast< const primitive2d::PolyPolygonColorPrimitive2D
& >(rCandidate
));
353 mbHit
= checkFillHitWithTolerance(rPolygonCandidate
.getB2DPolyPolygon(), getDiscreteHitTolerance());
358 case PRIMITIVE2D_ID_TRANSPARENCEPRIMITIVE2D
:
360 // sub-transparence group
361 const primitive2d::TransparencePrimitive2D
& rTransCandidate(static_cast< const primitive2d::TransparencePrimitive2D
& >(rCandidate
));
363 // Currently the transparence content is not taken into account; only
364 // the children are recursively checked for hit. This may be refined for
365 // parts where the content is completely transparent if needed.
366 process(rTransCandidate
.getChildren());
370 case PRIMITIVE2D_ID_MASKPRIMITIVE2D
:
372 // create mask in discrete coordinates; only recursively continue
373 // with content when HitTest position is inside the mask
374 const primitive2d::MaskPrimitive2D
& rMaskCandidate(static_cast< const primitive2d::MaskPrimitive2D
& >(rCandidate
));
377 if(checkFillHitWithTolerance(rMaskCandidate
.getMask(), getDiscreteHitTolerance()))
379 // recursively HitTest children
380 process(rMaskCandidate
.getChildren());
385 case PRIMITIVE2D_ID_SCENEPRIMITIVE2D
:
387 if(!getHitTextOnly())
389 const primitive2d::ScenePrimitive2D
& rScenePrimitive2D(
390 static_cast< const primitive2d::ScenePrimitive2D
& >(rCandidate
));
391 check3DHit(rScenePrimitive2D
);
396 case PRIMITIVE2D_ID_WRONGSPELLPRIMITIVE2D
:
397 case PRIMITIVE2D_ID_MARKERARRAYPRIMITIVE2D
:
398 case PRIMITIVE2D_ID_GRIDPRIMITIVE2D
:
399 case PRIMITIVE2D_ID_HELPLINEPRIMITIVE2D
:
401 // ignorable primitives
404 case PRIMITIVE2D_ID_SHADOWPRIMITIVE2D
:
406 // Ignore shadows; we do not want to have shadows hittable.
407 // Remove this one to make shadows hittable on demand.
410 case PRIMITIVE2D_ID_TEXTSIMPLEPORTIONPRIMITIVE2D
:
411 case PRIMITIVE2D_ID_TEXTDECORATEDPORTIONPRIMITIVE2D
:
413 // for text use the BoundRect of the primitive itself
414 const basegfx::B2DRange
aRange(rCandidate
.getB2DRange(getViewInformation2D()));
416 if(!aRange
.isEmpty())
418 const basegfx::B2DPolygon
aOutline(basegfx::utils::createPolygonFromRect(aRange
));
419 mbHit
= checkFillHitWithTolerance(basegfx::B2DPolyPolygon(aOutline
), getDiscreteHitTolerance());
424 case PRIMITIVE2D_ID_BITMAPPRIMITIVE2D
:
426 if(!getHitTextOnly())
428 // The recently added BitmapEx::GetTransparency() makes it easy to extend
429 // the BitmapPrimitive2D HitTest to take the contained BitmapEx and it's
430 // transparency into account
431 const basegfx::B2DRange
aRange(rCandidate
.getB2DRange(getViewInformation2D()));
433 if(!aRange
.isEmpty())
435 const primitive2d::BitmapPrimitive2D
& rBitmapCandidate(static_cast< const primitive2d::BitmapPrimitive2D
& >(rCandidate
));
436 const BitmapEx
aBitmapEx(VCLUnoHelper::GetBitmap(rBitmapCandidate
.getXBitmap()));
437 const Size
& rSizePixel(aBitmapEx
.GetSizePixel());
439 // When tiled rendering, don't bother with the pixel size of the candidate.
440 if(rSizePixel
.Width() && rSizePixel
.Height() && !comphelper::LibreOfficeKit::isActive())
442 basegfx::B2DHomMatrix
aBackTransform(
443 getViewInformation2D().getObjectToViewTransformation() *
444 rBitmapCandidate
.getTransform());
445 aBackTransform
.invert();
447 const basegfx::B2DPoint
aRelativePoint(aBackTransform
* getDiscreteHitPosition());
448 const basegfx::B2DRange
aUnitRange(0.0, 0.0, 1.0, 1.0);
450 if(aUnitRange
.isInside(aRelativePoint
))
452 const sal_Int32
nX(basegfx::fround(aRelativePoint
.getX() * rSizePixel
.Width()));
453 const sal_Int32
nY(basegfx::fround(aRelativePoint
.getY() * rSizePixel
.Height()));
455 mbHit
= (0xff != aBitmapEx
.GetTransparency(nX
, nY
));
460 // fallback to standard HitTest
461 const basegfx::B2DPolygon
aOutline(basegfx::utils::createPolygonFromRect(aRange
));
462 mbHit
= checkFillHitWithTolerance(basegfx::B2DPolyPolygon(aOutline
), getDiscreteHitTolerance());
469 case PRIMITIVE2D_ID_METAFILEPRIMITIVE2D
:
470 case PRIMITIVE2D_ID_CONTROLPRIMITIVE2D
:
471 case PRIMITIVE2D_ID_FILLGRADIENTPRIMITIVE2D
:
472 case PRIMITIVE2D_ID_FILLGRAPHICPRIMITIVE2D
:
473 case PRIMITIVE2D_ID_FILLHATCHPRIMITIVE2D
:
474 case PRIMITIVE2D_ID_PAGEPREVIEWPRIMITIVE2D
:
475 case PRIMITIVE2D_ID_MEDIAPRIMITIVE2D
:
477 if(!getHitTextOnly())
479 // Class of primitives for which just the BoundRect of the primitive itself
480 // will be used for HitTest currently.
482 // This may be refined in the future, e.g:
483 // - For Bitmaps, the mask and/or transparence information may be used
484 // - For MetaFiles, the MetaFile content may be used
485 const basegfx::B2DRange
aRange(rCandidate
.getB2DRange(getViewInformation2D()));
487 if(!aRange
.isEmpty())
489 const basegfx::B2DPolygon
aOutline(basegfx::utils::createPolygonFromRect(aRange
));
490 mbHit
= checkFillHitWithTolerance(basegfx::B2DPolyPolygon(aOutline
), getDiscreteHitTolerance());
496 case PRIMITIVE2D_ID_HIDDENGEOMETRYPRIMITIVE2D
:
498 // HiddenGeometryPrimitive2D; the default decomposition would return an empty sequence,
499 // so force this primitive to process its children directly if the switch is set
500 // (which is the default). Else, ignore invisible content
501 const primitive2d::HiddenGeometryPrimitive2D
& rHiddenGeometry(static_cast< const primitive2d::HiddenGeometryPrimitive2D
& >(rCandidate
));
502 const primitive2d::Primitive2DContainer
& rChildren
= rHiddenGeometry
.getChildren();
504 if(!rChildren
.empty())
511 case PRIMITIVE2D_ID_POINTARRAYPRIMITIVE2D
:
513 if(!getHitTextOnly())
515 const primitive2d::PointArrayPrimitive2D
& rPointArrayCandidate(static_cast< const primitive2d::PointArrayPrimitive2D
& >(rCandidate
));
516 const std::vector
< basegfx::B2DPoint
>& rPositions
= rPointArrayCandidate
.getPositions();
517 const sal_uInt32
nCount(rPositions
.size());
519 for(sal_uInt32
a(0); !getHit() && a
< nCount
; a
++)
521 const basegfx::B2DPoint
aPosition(getViewInformation2D().getObjectToViewTransformation() * rPositions
[a
]);
522 const basegfx::B2DVector
aDistance(aPosition
- getDiscreteHitPosition());
524 if(aDistance
.getLength() <= getDiscreteHitTolerance())
535 // process recursively
542 if (getHit() && getCollectHitStack())
544 /// push candidate to HitStack to create it. This only happens when a hit is found and
545 /// creating the HitStack was requested (see collectHitStack)
546 maHitStack
.append(primitive2d::Primitive2DReference(const_cast< primitive2d::BasePrimitive2D
* >(&rCandidate
)));
550 } // end of namespace
552 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */