blob: 4af240e6d22bd2084aa4eae1ed5c6eb1b38c9660 [file] [log] [blame]
/**************************************************************
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*************************************************************/
// MARKER(update_precomp.py): autogen include statement, do not remove
#include "precompiled_drawinglayer.hxx"
#include <drawinglayer/processor2d/hittestprocessor2d.hxx>
#include <drawinglayer/primitive2d/drawinglayer_primitivetypes2d.hxx>
#include <drawinglayer/primitive2d/transformprimitive2d.hxx>
#include <drawinglayer/primitive2d/polygonprimitive2d.hxx>
#include <drawinglayer/primitive2d/polypolygonprimitive2d.hxx>
#include <basegfx/polygon/b2dpolygontools.hxx>
#include <basegfx/polygon/b2dpolypolygontools.hxx>
#include <drawinglayer/primitive2d/transparenceprimitive2d.hxx>
#include <drawinglayer/primitive2d/maskprimitive2d.hxx>
#include <drawinglayer/primitive2d/sceneprimitive2d.hxx>
#include <drawinglayer/primitive2d/pointarrayprimitive2d.hxx>
#include <basegfx/matrix/b3dhommatrix.hxx>
#include <drawinglayer/processor3d/cutfindprocessor3d.hxx>
#include <drawinglayer/primitive2d/hiddengeometryprimitive2d.hxx>
#include <drawinglayer/primitive2d/bitmapprimitive2d.hxx>
//////////////////////////////////////////////////////////////////////////////
namespace drawinglayer
{
namespace processor2d
{
HitTestProcessor2D::HitTestProcessor2D(const geometry::ViewInformation2D& rViewInformation,
const basegfx::B2DPoint& rLogicHitPosition,
double fLogicHitTolerance,
bool bHitTextOnly)
: BaseProcessor2D(rViewInformation),
maDiscreteHitPosition(),
mfDiscreteHitTolerance(0.0),
mbHit(false),
mbHitToleranceUsed(false),
mbUseInvisiblePrimitiveContent(true),
mbHitTextOnly(bHitTextOnly)
{
// init hit tolerance
mfDiscreteHitTolerance = fLogicHitTolerance;
if(basegfx::fTools::less(mfDiscreteHitTolerance, 0.0))
{
// ensure input parameter for hit tolerance is >= 0.0
mfDiscreteHitTolerance = 0.0;
}
else if(basegfx::fTools::more(mfDiscreteHitTolerance, 0.0))
{
// generate discrete hit tolerance
mfDiscreteHitTolerance = (getViewInformation2D().getObjectToViewTransformation()
* basegfx::B2DVector(mfDiscreteHitTolerance, 0.0)).getLength();
}
// gererate discrete hit position
maDiscreteHitPosition = getViewInformation2D().getObjectToViewTransformation() * rLogicHitPosition;
// check if HitTolerance is used
mbHitToleranceUsed = basegfx::fTools::more(getDiscreteHitTolerance(), 0.0);
}
HitTestProcessor2D::~HitTestProcessor2D()
{
}
bool HitTestProcessor2D::checkHairlineHitWithTolerance(
const basegfx::B2DPolygon& rPolygon,
double fDiscreteHitTolerance)
{
basegfx::B2DPolygon aLocalPolygon(rPolygon);
aLocalPolygon.transform(getViewInformation2D().getObjectToViewTransformation());
// get discrete range
basegfx::B2DRange aPolygonRange(aLocalPolygon.getB2DRange());
if(basegfx::fTools::more(fDiscreteHitTolerance, 0.0))
{
aPolygonRange.grow(fDiscreteHitTolerance);
}
// do rough range test first
if(aPolygonRange.isInside(getDiscreteHitPosition()))
{
// check if a polygon edge is hit
return basegfx::tools::isInEpsilonRange(
aLocalPolygon,
getDiscreteHitPosition(),
fDiscreteHitTolerance);
}
return false;
}
bool HitTestProcessor2D::checkFillHitWithTolerance(
const basegfx::B2DPolyPolygon& rPolyPolygon,
double fDiscreteHitTolerance)
{
bool bRetval(false);
basegfx::B2DPolyPolygon aLocalPolyPolygon(rPolyPolygon);
aLocalPolyPolygon.transform(getViewInformation2D().getObjectToViewTransformation());
// get discrete range
basegfx::B2DRange aPolygonRange(aLocalPolyPolygon.getB2DRange());
const bool bDiscreteHitToleranceUsed(basegfx::fTools::more(fDiscreteHitTolerance, 0.0));
if(bDiscreteHitToleranceUsed)
{
aPolygonRange.grow(fDiscreteHitTolerance);
}
// do rough range test first
if(aPolygonRange.isInside(getDiscreteHitPosition()))
{
// if a HitTolerance is given, check for polygon edge hit in epsilon first
if(bDiscreteHitToleranceUsed &&
basegfx::tools::isInEpsilonRange(
aLocalPolyPolygon,
getDiscreteHitPosition(),
fDiscreteHitTolerance))
{
bRetval = true;
}
// check for hit in filled polyPolygon
if(!bRetval && basegfx::tools::isInside(
aLocalPolyPolygon,
getDiscreteHitPosition(),
true))
{
bRetval = true;
}
}
return bRetval;
}
void HitTestProcessor2D::check3DHit(const primitive2d::ScenePrimitive2D& rCandidate)
{
// calculate relative point in unified 2D scene
const basegfx::B2DPoint aLogicHitPosition(getViewInformation2D().getInverseObjectToViewTransformation() * getDiscreteHitPosition());
// use bitmap check in ScenePrimitive2D
bool bTryFastResult(false);
if(rCandidate.tryToCheckLastVisualisationDirectHit(aLogicHitPosition, bTryFastResult))
{
mbHit = bTryFastResult;
}
else
{
basegfx::B2DHomMatrix aInverseSceneTransform(rCandidate.getObjectTransformation());
aInverseSceneTransform.invert();
const basegfx::B2DPoint aRelativePoint(aInverseSceneTransform * aLogicHitPosition);
// check if test point is inside scene's unified area at all
if(aRelativePoint.getX() >= 0.0 && aRelativePoint.getX() <= 1.0
&& aRelativePoint.getY() >= 0.0 && aRelativePoint.getY() <= 1.0)
{
// get 3D view information
const geometry::ViewInformation3D& rObjectViewInformation3D = rCandidate.getViewInformation3D();
// create HitPoint Front and Back, transform to object coordinates
basegfx::B3DHomMatrix aViewToObject(rObjectViewInformation3D.getObjectToView());
aViewToObject.invert();
const basegfx::B3DPoint aFront(aViewToObject * basegfx::B3DPoint(aRelativePoint.getX(), aRelativePoint.getY(), 0.0));
const basegfx::B3DPoint aBack(aViewToObject * basegfx::B3DPoint(aRelativePoint.getX(), aRelativePoint.getY(), 1.0));
if(!aFront.equal(aBack))
{
const primitive3d::Primitive3DSequence& rPrimitives = rCandidate.getChildren3D();
if(rPrimitives.hasElements())
{
// make BoundVolume empty and overlapping test for speedup
const basegfx::B3DRange aObjectRange(
drawinglayer::primitive3d::getB3DRangeFromPrimitive3DSequence(
rPrimitives, rObjectViewInformation3D));
if(!aObjectRange.isEmpty())
{
const basegfx::B3DRange aFrontBackRange(aFront, aBack);
if(aObjectRange.overlaps(aFrontBackRange))
{
// bound volumes hit, geometric cut tests needed
drawinglayer::processor3d::CutFindProcessor aCutFindProcessor(
rObjectViewInformation3D,
aFront,
aBack,
true);
aCutFindProcessor.process(rPrimitives);
mbHit = (0 != aCutFindProcessor.getCutPoints().size());
}
}
}
}
}
// This is needed to check hit with 3D shadows, too. HitTest is without shadow
// to keep compatible with previous versions. Keeping here as reference
//
// if(!getHit())
// {
// // if scene has shadow, check hit with shadow, too
// const primitive2d::Primitive2DSequence xExtracted2DSceneShadow(rCandidate.getShadow2D(getViewInformation2D()));
//
// if(xExtracted2DSceneShadow.hasElements())
// {
// // proccess extracted 2D content
// process(xExtracted2DSceneShadow);
// }
// }
if(!getHit())
{
// empty 3D scene; Check for border hit
basegfx::B2DPolygon aOutline(basegfx::tools::createUnitPolygon());
aOutline.transform(rCandidate.getObjectTransformation());
mbHit = checkHairlineHitWithTolerance(aOutline, getDiscreteHitTolerance());
}
// This is what the previous version did. Keeping it here for reference
//
// // 2D Scene primitive containing 3D stuff; extract 2D contour in world coordinates
// // This may be refined later to an own 3D HitTest renderer which processes the 3D
// // geometry directly
// const primitive2d::ScenePrimitive2D& rScenePrimitive2DCandidate(static_cast< const primitive2d::ScenePrimitive2D& >(rCandidate));
// const primitive2d::Primitive2DSequence xExtracted2DSceneGeometry(rScenePrimitive2DCandidate.getGeometry2D());
// const primitive2d::Primitive2DSequence xExtracted2DSceneShadow(rScenePrimitive2DCandidate.getShadow2D(getViewInformation2D()));
//
// if(xExtracted2DSceneGeometry.hasElements() || xExtracted2DSceneShadow.hasElements())
// {
// // proccess extracted 2D content
// process(xExtracted2DSceneGeometry);
// process(xExtracted2DSceneShadow);
// }
// else
// {
// // empty 3D scene; Check for border hit
// const basegfx::B2DRange aRange(rCandidate.getB2DRange(getViewInformation2D()));
// if(!aRange.isEmpty())
// {
// const basegfx::B2DPolygon aOutline(basegfx::tools::createPolygonFromRect(aRange));
// mbHit = checkHairlineHitWithTolerance(aOutline, getDiscreteHitTolerance());
// }
// }
}
}
void HitTestProcessor2D::processBasePrimitive2D(const primitive2d::BasePrimitive2D& rCandidate)
{
if(getHit())
{
// stop processing as soon as a hit was recognized
return;
}
switch(rCandidate.getPrimitive2DID())
{
case PRIMITIVE2D_ID_TRANSFORMPRIMITIVE2D :
{
// remember current ViewInformation2D
const primitive2d::TransformPrimitive2D& rTransformCandidate(static_cast< const primitive2d::TransformPrimitive2D& >(rCandidate));
const geometry::ViewInformation2D aLastViewInformation2D(getViewInformation2D());
// create new local ViewInformation2D containing transformation
const geometry::ViewInformation2D aViewInformation2D(
getViewInformation2D().getObjectTransformation() * rTransformCandidate.getTransformation(),
getViewInformation2D().getViewTransformation(),
getViewInformation2D().getViewport(),
getViewInformation2D().getVisualizedPage(),
getViewInformation2D().getViewTime(),
getViewInformation2D().getExtendedInformationSequence());
updateViewInformation(aViewInformation2D);
// proccess child content recursively
process(rTransformCandidate.getChildren());
// restore transformations
updateViewInformation(aLastViewInformation2D);
break;
}
case PRIMITIVE2D_ID_POLYGONHAIRLINEPRIMITIVE2D :
{
if(!getHitTextOnly())
{
// create hairline in discrete coordinates
const primitive2d::PolygonHairlinePrimitive2D& rPolygonCandidate(static_cast< const primitive2d::PolygonHairlinePrimitive2D& >(rCandidate));
// use hairline test
mbHit = checkHairlineHitWithTolerance(rPolygonCandidate.getB2DPolygon(), getDiscreteHitTolerance());
}
break;
}
case PRIMITIVE2D_ID_POLYGONMARKERPRIMITIVE2D :
{
if(!getHitTextOnly())
{
// handle marker like hairline; no need to decompose in dashes
const primitive2d::PolygonMarkerPrimitive2D& rPolygonCandidate(static_cast< const primitive2d::PolygonMarkerPrimitive2D& >(rCandidate));
// use hairline test
mbHit = checkHairlineHitWithTolerance(rPolygonCandidate.getB2DPolygon(), getDiscreteHitTolerance());
}
break;
}
case PRIMITIVE2D_ID_POLYGONSTROKEPRIMITIVE2D :
{
if(!getHitTextOnly())
{
// handle stroke evtl. directly; no need to decompose to filled polygon outlines
const primitive2d::PolygonStrokePrimitive2D& rPolygonCandidate(static_cast< const primitive2d::PolygonStrokePrimitive2D& >(rCandidate));
const attribute::LineAttribute& rLineAttribute = rPolygonCandidate.getLineAttribute();
if(basegfx::fTools::more(rLineAttribute.getWidth(), 0.0))
{
if(basegfx::B2DLINEJOIN_MITER == rLineAttribute.getLineJoin())
{
// if line is mitered, use decomposition since mitered line
// geometry may use more space than the geometry grown by half line width
process(rCandidate.get2DDecomposition(getViewInformation2D()));
}
else
{
// for all other B2DLINEJOIN_* do a hairline HitTest with expanded tolerance
const basegfx::B2DVector aDiscreteHalfLineVector(getViewInformation2D().getObjectToViewTransformation()
* basegfx::B2DVector(rLineAttribute.getWidth() * 0.5, 0.0));
mbHit = checkHairlineHitWithTolerance(
rPolygonCandidate.getB2DPolygon(),
getDiscreteHitTolerance() + aDiscreteHalfLineVector.getLength());
}
}
else
{
// hairline; fallback to hairline test. Do not decompose
// since this may decompose the hairline to dashes
mbHit = checkHairlineHitWithTolerance(rPolygonCandidate.getB2DPolygon(), getDiscreteHitTolerance());
}
}
break;
}
case PRIMITIVE2D_ID_POLYGONWAVEPRIMITIVE2D :
{
if(!getHitTextOnly())
{
// do not use decompose; just handle like a line with width
const primitive2d::PolygonWavePrimitive2D& rPolygonCandidate(static_cast< const primitive2d::PolygonWavePrimitive2D& >(rCandidate));
double fLogicHitTolerance(0.0);
// if WaveHeight, grow by it
if(basegfx::fTools::more(rPolygonCandidate.getWaveHeight(), 0.0))
{
fLogicHitTolerance += rPolygonCandidate.getWaveHeight();
}
// if line width, grow by it
if(basegfx::fTools::more(rPolygonCandidate.getLineAttribute().getWidth(), 0.0))
{
fLogicHitTolerance += rPolygonCandidate.getLineAttribute().getWidth() * 0.5;
}
const basegfx::B2DVector aDiscreteHalfLineVector(getViewInformation2D().getObjectToViewTransformation()
* basegfx::B2DVector(fLogicHitTolerance, 0.0));
mbHit = checkHairlineHitWithTolerance(
rPolygonCandidate.getB2DPolygon(),
getDiscreteHitTolerance() + aDiscreteHalfLineVector.getLength());
}
break;
}
case PRIMITIVE2D_ID_POLYPOLYGONCOLORPRIMITIVE2D :
{
if(!getHitTextOnly())
{
// create filled polyPolygon in discrete coordinates
const primitive2d::PolyPolygonColorPrimitive2D& rPolygonCandidate(static_cast< const primitive2d::PolyPolygonColorPrimitive2D& >(rCandidate));
// use fill hit test
mbHit = checkFillHitWithTolerance(rPolygonCandidate.getB2DPolyPolygon(), getDiscreteHitTolerance());
}
break;
}
case PRIMITIVE2D_ID_TRANSPARENCEPRIMITIVE2D :
{
// sub-transparence group
const primitive2d::TransparencePrimitive2D& rTransCandidate(static_cast< const primitive2d::TransparencePrimitive2D& >(rCandidate));
// Currently the transparence content is not taken into account; only
// the children are recursively checked for hit. This may be refined for
// parts where the content is completely transparent if needed.
process(rTransCandidate.getChildren());
break;
}
case PRIMITIVE2D_ID_MASKPRIMITIVE2D :
{
// create mask in discrete coordinates; only recursively continue
// with content when HitTest position is inside the mask
const primitive2d::MaskPrimitive2D& rMaskCandidate(static_cast< const primitive2d::MaskPrimitive2D& >(rCandidate));
// use fill hit test
if(checkFillHitWithTolerance(rMaskCandidate.getMask(), getDiscreteHitTolerance()))
{
// recursively HitTest children
process(rMaskCandidate.getChildren());
}
break;
}
case PRIMITIVE2D_ID_SCENEPRIMITIVE2D :
{
if(!getHitTextOnly())
{
const primitive2d::ScenePrimitive2D& rScenePrimitive2D(
static_cast< const primitive2d::ScenePrimitive2D& >(rCandidate));
check3DHit(rScenePrimitive2D);
}
break;
}
case PRIMITIVE2D_ID_WRONGSPELLPRIMITIVE2D :
case PRIMITIVE2D_ID_MARKERARRAYPRIMITIVE2D :
case PRIMITIVE2D_ID_GRIDPRIMITIVE2D :
case PRIMITIVE2D_ID_HELPLINEPRIMITIVE2D :
{
// ignorable primitives
break;
}
case PRIMITIVE2D_ID_SHADOWPRIMITIVE2D :
{
// Ignore shadows; we do not want to have shadows hittable.
// Remove this one to make shadows hittable on demand.
break;
}
case PRIMITIVE2D_ID_TEXTSIMPLEPORTIONPRIMITIVE2D :
case PRIMITIVE2D_ID_TEXTDECORATEDPORTIONPRIMITIVE2D :
{
// for text use the BoundRect of the primitive itself
const basegfx::B2DRange aRange(rCandidate.getB2DRange(getViewInformation2D()));
if(!aRange.isEmpty())
{
const basegfx::B2DPolygon aOutline(basegfx::tools::createPolygonFromRect(aRange));
mbHit = checkFillHitWithTolerance(basegfx::B2DPolyPolygon(aOutline), getDiscreteHitTolerance());
}
break;
}
case PRIMITIVE2D_ID_BITMAPPRIMITIVE2D :
{
if(!getHitTextOnly())
{
// The recently added BitmapEx::GetTransparency() makes it easy to extend
// the BitmapPrimitive2D HitTest to take the contained BotmapEx and it's
// transparency into account
const basegfx::B2DRange aRange(rCandidate.getB2DRange(getViewInformation2D()));
if(!aRange.isEmpty())
{
const primitive2d::BitmapPrimitive2D& rBitmapCandidate(static_cast< const primitive2d::BitmapPrimitive2D& >(rCandidate));
const BitmapEx& rBitmapEx = rBitmapCandidate.getBitmapEx();
const Size& rSizePixel(rBitmapEx.GetSizePixel());
if(rSizePixel.Width() && rSizePixel.Height())
{
basegfx::B2DHomMatrix aBackTransform(
getViewInformation2D().getObjectToViewTransformation() *
rBitmapCandidate.getTransform());
aBackTransform.invert();
const basegfx::B2DPoint aRelativePoint(aBackTransform * getDiscreteHitPosition());
const basegfx::B2DRange aUnitRange(0.0, 0.0, 1.0, 1.0);
if(aUnitRange.isInside(aRelativePoint))
{
const sal_Int32 nX(basegfx::fround(aRelativePoint.getX() * rSizePixel.Width()));
const sal_Int32 nY(basegfx::fround(aRelativePoint.getY() * rSizePixel.Height()));
mbHit = (0xff != rBitmapEx.GetTransparency(nX, nY));
}
}
else
{
// fallback to standard HitTest
const basegfx::B2DPolygon aOutline(basegfx::tools::createPolygonFromRect(aRange));
mbHit = checkFillHitWithTolerance(basegfx::B2DPolyPolygon(aOutline), getDiscreteHitTolerance());
}
}
}
break;
}
case PRIMITIVE2D_ID_METAFILEPRIMITIVE2D :
case PRIMITIVE2D_ID_CONTROLPRIMITIVE2D :
case PRIMITIVE2D_ID_FILLGRADIENTPRIMITIVE2D :
case PRIMITIVE2D_ID_FILLHATCHPRIMITIVE2D :
case PRIMITIVE2D_ID_PAGEPREVIEWPRIMITIVE2D :
case PRIMITIVE2D_ID_MEDIAPRIMITIVE2D:
{
if(!getHitTextOnly())
{
// Class of primitives for which just the BoundRect of the primitive itself
// will be used for HitTest currently.
//
// This may be refined in the future, e.g:
// - For Bitamps, the mask and/or transparence information may be used
// - For MetaFiles, the MetaFile content may be used
const basegfx::B2DRange aRange(rCandidate.getB2DRange(getViewInformation2D()));
if(!aRange.isEmpty())
{
const basegfx::B2DPolygon aOutline(basegfx::tools::createPolygonFromRect(aRange));
mbHit = checkFillHitWithTolerance(basegfx::B2DPolyPolygon(aOutline), getDiscreteHitTolerance());
}
}
break;
}
case PRIMITIVE2D_ID_HIDDENGEOMETRYPRIMITIVE2D :
{
// HiddenGeometryPrimitive2D; the default decomposition would return an empty seqence,
// so force this primitive to process it's children directly if the switch is set
// (which is the default). Else, ignore invisible content
const primitive2d::HiddenGeometryPrimitive2D& rHiddenGeometry(static_cast< const primitive2d::HiddenGeometryPrimitive2D& >(rCandidate));
const primitive2d::Primitive2DSequence& rChildren = rHiddenGeometry.getChildren();
if(rChildren.hasElements())
{
if(getUseInvisiblePrimitiveContent())
{
process(rChildren);
}
}
break;
}
case PRIMITIVE2D_ID_POINTARRAYPRIMITIVE2D :
{
if(!getHitTextOnly())
{
const primitive2d::PointArrayPrimitive2D& rPointArrayCandidate(static_cast< const primitive2d::PointArrayPrimitive2D& >(rCandidate));
const std::vector< basegfx::B2DPoint >& rPositions = rPointArrayCandidate.getPositions();
const sal_uInt32 nCount(rPositions.size());
for(sal_uInt32 a(0); !getHit() && a < nCount; a++)
{
const basegfx::B2DPoint aPosition(getViewInformation2D().getObjectToViewTransformation() * rPositions[a]);
const basegfx::B2DVector aDistance(aPosition - getDiscreteHitPosition());
if(aDistance.getLength() <= getDiscreteHitTolerance())
{
mbHit = true;
}
}
}
break;
}
default :
{
// process recursively
process(rCandidate.get2DDecomposition(getViewInformation2D()));
break;
}
}
}
} // end of namespace processor2d
} // end of namespace drawinglayer
//////////////////////////////////////////////////////////////////////////////
// eof