/************************************************************** * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include ////////////////////////////////////////////////////////////////////////////// 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()) // { // // process 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()) // { // // process 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); // process 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